第三部分:循环结构 —— 让代码高效重复执行
循环结构是另一种基本的流程控制结构,用于反复执行特定的代码逻辑块。Java 提供了多种循环实现方式,不同的实现方式,在性能、可读性、适用场景上有非常大的差异。
3.1 while 与 do-while:适用于不确定循环次数的场景
while 与 do-while 是 Java 中最基础的两种循环结构,适用于循环次数不确定的业务场景。
3.1.1 核心语法与执行逻辑
while 和 do-while 的语法结构非常相似,核心区别,在于判断条件和循环体的执行顺序不同:
// 1. while循环:先判断条件,后执行循环体
while (条件表达式) {
  // 循环体逻辑
}
// 2. do-while循环:先执行循环体,后判断条件
do {
  // 循环体逻辑
} while (条件表达式);
这两种循环的执行逻辑,有本质的差异,适用场景也完全不同:
- while 循环:先判断条件表达式的值,如果值为 true,才会执行循环体;如果为 false,循环体一次都不会执行。
- do-while 循环:先执行一次循环体逻辑,再判断条件表达式的值;如果值为 true,继续执行循环体;如果为 false,终止循环。无论条件表达式的值是什么,循环体至少会执行一次(19)。
3.1.2 底层字节码实现
while 与 do-while 的底层实现,都是 JVM 的条件跳转指令。下面通过两个简单的代码示例,看看这两种循环的字节码差异:
1. while 循环的字节码示例
public class WhileLoopBytecodeDemo {
  public static void main(String\\[] args) {
  int i = 0;
  while (i < 10) {
  i++;
  }
  }
}
编译后的核心字节码指令如下:
0: iconst\\_0
1: istore\\_1
2: iload\\_1
3: bipush 10
5: if\\_icmpge 14
8: iload\\_1
9: iconst\\_1
10: iadd
11: istore\\_1
12: goto 2
15: return
可以看到,while 循环的底层执行逻辑,是先执行if_icmpge指令判断循环条件,如果条件满足,就执行循环体的逻辑;执行完成后,通过goto指令,跳转到条件判断的位置,开始下一次循环;如果条件不满足,就跳转到循环结束后的指令地址。
2. do-while 循环的字节码示例
public class DoWhileLoopBytecodeDemo {
  public static void main(String\\[] args) {
  int i = 0;
  do {
  i++;
  } while (i < 10);
  }
}
编译后的核心字节码指令如下:
0: iconst\\_0
1: istore\\_1
2: iload\\_1
3: iconst\\_1
4: iadd
5: istore\\_1
6: iload\\_1
7: bipush 10
9: if\\_icmplt 2
12: return
可以看到,do-while 循环的底层执行逻辑,是先执行循环体的逻辑,再执行if_icmplt指令判断循环条件;如果条件满足,就通过goto指令,跳转到循环体的起始位置,开始下一次循环;如果条件不满足,就终止循环,执行后续的指令。
3.1.3 架构师级避坑指南
在使用 while 和 do-while 循环时,需要重点规避下面这三类隐蔽的坑点:
坑点 1:循环条件处理不当,导致无限循环 ——CPU 利用率飙升的核心原因
这是非常容易犯的低级错误,导致循环变量的状态修改逻辑错误,或循环条件的判断逻辑编写错误,导致循环条件永远为 true,出现无限循环。一旦在生产环境中出现无限循环,会导致当前线程的执行逻辑无法终止,最终可能会导致整个系统的 CPU 利用率飙升,甚至触发操作系统的资源管理机制,直接造成线程崩溃。
坑点 2:循环体中的逻辑,导致条件判断结果没有被及时更新 —— 循环无法终止
如果循环体中有复杂的 continue 逻辑,或条件变量的修改逻辑被遗漏,导致循环条件的判断结果,在循环体中没有被及时更新,那么循环条件就会永远保持为 true,同样会导致无限循环。
坑点 3:do-while 循环的条件判断逻辑,放在循环体的中间位置 —— 逻辑被跳过
do-while 循环的特殊执行顺序,导致它的风险比 while 循环更大:如果将条件判断逻辑放在循环体的中间位置,那么循环体的后续逻辑会被直接跳过,导致业务逻辑出现隐蔽的缺失问题。
3.1.4 生产级最佳实践
想要在生产级场景中用好这两种循环结构,需要遵守下面四条最佳实践:
实践 1:优先使用 while 循环,替代 do-while 循环
do-while 循环的特殊执行顺序,导致它的风险比 while 循环高不少:即使出现异常,循环体也会至少执行一次,可能导致业务逻辑被重复执行。因此,在实际项目中,优先使用 while 循环,完全可以替代 do-while 循环的所有使用场景。
实践 2:将循环条件逻辑提取到循环外,简化单次判断逻辑
如果循环条件中包含复杂的计算逻辑,或方法调用,建议将这部分复杂逻辑,提取到循环体外的预计算部分,将结果缓存到局部变量中,再在循环条件中直接使用该变量。避免在每次循环时,都要重复执行复杂的条件计算逻辑,减少每次循环的执行时间:
// 反例:循环条件中包含复杂的计算逻辑,每次循环都会重复执行
while (userService.isUserEnabled(userId) && orderService.isOrderValid(orderId)) {
  // 循环体逻辑
}
// 正例:将复杂的条件逻辑提取到循环外,只执行一次
boolean isUserEnabled = userService.isUserEnabled(userId);
boolean isOrderValid = orderService.isOrderValid(orderId);
while (isUserEnabled && isOrderValid) {
  // 循环体逻辑
}
实践 3:在循环体中,尽快修改循环条件涉及的变量
在循环体的第一行,或靠前的位置,修改循环条件涉及的变量,保证在下次循环前,条件变量的最新值已经完成更新,避免出现死循环或循环条件判断错误:
// 正例:在循环体的靠前位置,修改循环条件涉及的变量
int i = 0;
while (i < 10) {
  // 先修改条件变量i的值
  i++;
  // 再执行其他业务逻辑
}
实践 4:使用 Boolean 变量封装复杂的循环终止条件
如果循环终止条件的逻辑比较复杂,建议将这部分逻辑,封装到一个独立的 boolean 变量中,让循环逻辑更清晰,可读性更高:
// 正例:使用boolean变量封装复杂的循环终止条件
boolean hasMoreData = true;
while (hasMoreData) {
  // 业务逻辑处理
  if (resultList.isEmpty()) {
  hasMoreData = false;
  }
}
3.2 for:循环次数已知场景下的最优选择
for 循环是 Java 语言中使用频率最高的循环结构,也是性能最优的循环实现方式,适用于循环次数已知的业务场景。
3.2.1 核心语法与执行逻辑
Java 语言支持多种 for 循环的写法,最基础的是传统索引 for 循环,语法格式为:
for (初始化语句; 条件表达式; 迭代语句) {
  // 循环体逻辑
}
- 初始化语句:在循环开始前,只会执行一次,通常用于定义循环的索引变量;
- 条件表达式:在每次循环体执行前执行,如果表达式的值为 true,执行循环体;如果为 false,终止循环;
- 迭代语句:在每次循环体执行完成后执行,通常用于修改循环的索引变量;
执行逻辑的伪代码如下:
执行初始化语句 -> 检查条件表达式 -> 如果为true,执行循环体 -> 执行迭代语句 -> 检查条件表达式 -> … -> 条件表达式为false,终止循环
3.2.2 增强 for 循环(JDK5+):简化集合遍历的语法糖
为了简化数组、集合类的遍历代码,Java 5 引入了增强 for 循环,也称为 “for-each 循环”,允许开发者直接遍历集合或数组,无需手动管理索引变量,代码更简洁紧凑:
// 增强for循环的语法格式
for (元素类型 变量名 : 数组或集合对象) {
  // 循环体逻辑
}
下面通过一个实际的示例,对比传统 for 循环和增强 for 循环的代码差异:
public class ForLoopDemo {
  public static void main(String\\[] args) {
  List\\<String> list = new ArrayList<>();
  list.add("Java");
  list.add("中");
  list.add("高");
  list.add("级");
  list.add("架构");
  list.add("师");
   
  // 1. 传统索引for循环:需要手动管理索引变量
  for (int i = 0; i < list.size(); i++) {
  String item = list.get(i);
  System.out.println(item);
  }
   
  // 2. 增强for循环:语法更简洁,无需手动管理索引变量
  for (String item : list) {
  System.out.println(item);
  }
  }
}
增强 for 循环的底层实现,是迭代器模式—— 编译器会自动将增强 for 循环的语法,转换为使用Iterator迭代器的遍历逻辑,因此在遍历集合类时,增强 for 循环的性能表现,和手动使用迭代器遍历完全一致。
3.2.3 底层字节码实现
不同的 for 循环写法,在字节码层面的实现差异很大,这些差异最终会直接影响执行性能。
1. 传统索引 for 循环的字节码实现
传统索引 for 循环的底层实现,是 C 语言风格的基础循环逻辑,没有任何额外的对象创建或方法调用开销,是所有循环中性能最高的。
编译后的核心字节码指令如下:
0: iconst\\_0
1: istore\\_1
2: iload\\_1
3: aload\\_0
4: invokeinterface #2, 1
6: if\\_icmpge 19
9: aload\\_0
10: iload\\_1
11: invokeinterface #3, 2
13: astore\\_2
14: iinc 1, 1
17: goto 2
20: return
可以看到,传统 for 循环的底层实现中,没有任何额外的对象创建或方法调用,只使用了局部变量、索引自增、条件跳转等基础指令,执行性能可以达到 O (1)。
2. 增强 for 循环的字节码实现
增强 for 循环的底层实现,是迭代器模式。编译器会自动将增强 for 循环的语法,转换为使用Iterator迭代器的遍历逻辑,因此在遍历集合类时,增强 for 循环的性能表现,和手动使用迭代器遍历完全一致。
编译后的核心字节码指令如下:
0: aload\\_0
1: invokeinterface #2, 1
3: astore\\_1
4: aload\\_1
5: invokeinterface #3, 1
7: ifeq 23
10: aload\\_1
11: invokeinterface #4, 1
13: astore\\_2
14: aload\\_2
15: invokevirtual #5
18: goto 4
21: goto 4
24: return
可以看到,增强 for 循环的底层实现中,会额外创建一个迭代器对象,每次遍历都需要调用hasNext()和next()方法,这部分逻辑会带来一定的额外性能开销。
3.2.4 性能对比:各种 for 循环的实测数据
理解了底层实现逻辑,再结合我用 JMH 做的一个实际性能对比测试数据,就可以清楚知道不同场景下,应该选择哪种 for 循环实现了:
| 传统索引 for 循环 | 最优(O (1)) | 极差(O (n^2)) | 数组、ArrayList 等支持随机访问的集合类 |
| 增强 for 循环 | 略低 | 最优 | LinkedList 等不支持随机访问的集合类 |
| Stream.forEach() | 略低 | 略低 | 并行遍历、流式处理、Lambda 表达式场景 |
| 并行流 parallelStream () | 多核 CPU 下性能提升 | 多核 CPU 下性能提升 | 大数据量、耗时计算、无线程安全场景 |
测试结果说明:
- 遍历ArrayList时,传统索引 for 循环的性能最高,因为ArrayList基于数组实现,支持随机访问,get()操作的时间复杂度为 O (1);
- 遍历LinkedList时,增强 for 循环的性能更高,因为LinkedList基于双向链表实现,不支持随机访问,使用传统索引 for 循环时,每次get()操作都需要从头或尾节点开始遍历,时间复杂度为 O (n);
- 不同的遍历方式,性能差异非常明显:同样是遍历包含 100 万元素的ArrayList,传统索引 for 循环的执行时间,是增强 for 循环的三分之一。
3.2.5 架构师级避坑指南
在使用 for 循环时,需要重点规避下面这四类隐蔽的坑点:
坑点 1:循环中修改集合结构,导致并发修改异常(fail-fast)
这是增强 for 循环和迭代器遍历场景下,最容易犯的错误:在遍历集合的过程中,直接修改集合的结构(如新增、删除元素),会触发集合的 fail-fast 快速失败机制,抛出ConcurrentModificationException异常。
坑点 2:LinkedList 使用传统索引 for 循环,导致性能雪崩
LinkedList是基于双向链表实现的,不支持随机访问,使用传统索引 for 循环时,每次get()操作都需要从头或尾节点开始遍历,时间复杂度为 O (n);如果遍历的元素数量稍大,会导致整个循环的执行时间呈指数级上升,出现性能雪崩。
坑点 3:循环中使用 + 拼接字符串,导致大量临时对象创建
在循环中使用+拼接字符串,会在每次循环时,创建一个新的StringBuilder对象和一个新的String对象,产生大量的临时对象,给年轻代 GC 造成极大压力。如果循环次数较多,这部分压力会直接影响系统的吞吐量。
坑点 4:增强 for 循环遍历数组时,出现额外的数组复制操作
增强 for 循环遍历数组时,会自动将数组转换为固定长度的List集合,产生额外的数组复制操作,带来一定的性能开销。
3.2.6 生产级最佳实践
结合 for 循环的底层实现逻辑和性能对比数据,想要在生产级场景中用好 for 循环,必须遵守下面五条最佳实践:
实践 1:根据集合类型选择最优的循环实现方式
- 遍历数组或ArrayList等支持随机访问的集合类时,优先使用传统索引 for 循环;
- 遍历LinkedList等不支持随机访问的集合类时,优先使用增强 for 循环,或手动使用迭代器实现;
- 遍历Set、Map这类不支持随机访问的集合类时,必须使用增强 for 循环,或手动使用迭代器实现。
实践 2:循环内不要做复杂计算或频繁的方法调用
将循环内可以提前计算的逻辑,比如方法调用、集合的大小计算、字符串的拼接逻辑,提取到循环体外,将结果缓存到局部变量中,再在循环体中直接使用,避免在每次循环时,都要重复执行这部分逻辑:
// 反例:循环内重复计算集合的大小值
for (int i = 0; i < list.size(); i++) {
  // 业务逻辑处理
}
// 正例:将集合的大小值预计算,缓存到局部变量中
int size = list.size();
for (int i = 0; i < size; i++) {
  // 业务逻辑处理
}
实践 3:循环中删除集合元素,使用迭代器的 remove 方法
如果需要在遍历集合的过程中,删除集合元素,不要直接调用集合的remove()方法,要使用Iterator迭代器的remove()方法,避免触发 fail-fast 机制,抛出ConcurrentModificationException异常。
实践 4:循环中拼接字符串,优先使用 StringBuilder 或 StringBuffer
在循环中拼接字符串时,手动创建StringBuilder对象,调用append方法执行拼接,循环结束后,再调用toString方法获取最终的拼接结果,避免产生大量临时 String 对象,减少 GC 压力。
实践 5:大数据量且无线程安全问题时,考虑使用并行流
如果循环的迭代次数超过 10000 次,且循环体中的业务逻辑,没有线程安全问题,也不依赖上一次循环的结果,可以考虑使用并行流parallelStream(),利用多核 CPU 的计算能力,提升整个循环的执行效率。
第四部分:流程控制的核心优化原则 —— 从编码规范到性能优化
前面我们已经拆解了所有流程控制相关的语法细节、底层实现逻辑、不同场景下的坑点。接下来,我们将从架构师的视角,总结出生产级场景下的流程控制最佳实践,帮助你写出更高效、更可读、更可维护的代码。
4.1 正确性优先:避免逻辑陷阱
很多生产级的故障,不是由复杂的分布式逻辑导致的,而是由流程控制的细节错误导致的。在使用流程控制语句时,首先要保证逻辑的正确性,而非性能表现。
1. 优先使用卫语句消除分支嵌套
不要写出超过 2 层嵌套的 if-else 代码,使用卫语句提前校验非法条件,消除所有嵌套 —— 这是保证代码可读性的最有效手段。
2. 必须使用大括号包裹代码块
即使分支或循环体中只有一行代码,也必须使用大括号包裹起来,避免后续新增业务逻辑时,出现语法逻辑错误,或代码缩进导致的逻辑歧义。
3. 不要在条件表达式中嵌入赋值逻辑
条件表达式中,只能进行条件判断,不能进行变量赋值、资源初始化这类有副作用的操作,避免条件判断的结果被意外修改,或出现逻辑歧义,导致隐蔽 bug。
4. 循环中必须有能终止循环的逻辑
在循环体中,必须有能修改循环条件变量、保证循环能终止的逻辑,避免出现无限循环。
5. 分支逻辑必须覆盖所有可能的情况
使用 switch 语句时,必须提供 default 分支;使用 if-else 语句时,必须覆盖所有可能的条件分支,避免出现未处理的分支情况,导致业务逻辑被跳过,或抛出异常。
4.2 性能优化:选择最优的实现方式
流程控制的性能优化,核心是 “减少不必要的指令执行”—— 理解底层字节码的执行逻辑,是优化的前提。
1. 优先使用 switch 表达式替代长链 if-else
如果分支数量超过 5 个,且匹配条件是基于固定值的枚举、字符串或整数,优先使用 switch 表达式替代长链 if-else。当分支数量超过 10 个时,switch 的性能表现会明显优于 if-else。
2. 优先使用高效的循环实现方式
根据集合类型选择最优的循环实现方式,核心规则是:
- 遍历数组、ArrayList 时,优先使用传统索引 for 循环;
- 遍历 LinkedList、Set、Map 时,优先使用增强 for 循环;
- 大数据量、无线程安全问题时,优先使用并行流。
3. 将可优化的逻辑提取到循环外
将循环内可以提前计算的逻辑,比如方法调用、集合的大小计算、字符串的拼接逻辑,提取到循环体外,将结果缓存到局部变量中,再在循环体中直接使用,避免在每次循环时,都要重复执行这部分逻辑。
4. 循环内不要做 “昂贵” 的操作
循环内不要做耗时较长的操作,比如远程调用、数据库查询、频繁的 IO 读写操作。如果必须在循环中执行这类操作,尽量将这部分逻辑优化为批量处理,或提取到循环体外,减少执行次数。
5. 尽量减少分支和循环的嵌套层数
分支和循环的嵌套层数越多,执行的指令数越多,性能损耗越大。可以通过卫语句、提前返回、抽取独立方法等方式,减少嵌套层数。
4.3 可读性优化:架构师级的代码编写规范
在实际的项目中,代码的可读性和可维护性,比性能表现更重要 —— 阅读代码的时间,远远大于编写代码的时间,优化可读性的收益,远大于优化性能的收益。
1. 坚决避免 “箭头型代码”
使用卫语句、条件反转、提前返回等方式,将分支逻辑的嵌套层数控制在 1 层或 2 层以内,后续维护成本会呈指数级下降。
2. 复杂条件逻辑必须封装为独立方法
如果条件表达式的逻辑比较复杂,或者多个地方需要重复使用同样的判断逻辑,一定要将这部分条件逻辑抽取到独立的方法中,方法名要见名知意,比如isUserActive()、hasPermission()。
3. 分支和循环的代码块必须缩进一致
使用统一的代码缩进规则,比如缩进 4 个空格,或使用一个制表符,保证同一代码块中的语句,缩进位置完全一致。这是保证代码可读性的最基础手段。
4. 不要省略 break 和 default 分支
在 switch 语句中,不要省略 break 语句(或使用箭头语法),必须提供 default 分支,即使 default 分支中没有任何逻辑,也要写出来,明确覆盖所有可能的分支情况。
5. 循环逻辑过长时,需将循环体封装为独立方法
如果循环体的逻辑超过 10 行,或逻辑比较复杂,将循环体中的逻辑,封装到一个独立的方法中,在循环体中直接调用该方法,让循环逻辑更简洁、更易读。
4.4 高级重构:简化多分支逻辑
如果分支逻辑超过 10 个,且后续存在持续新增的可能,使用 switch 表达式或 if-else,都会导致代码的可维护性下降。这类场景下,需要借助多态设计模式进行重构,彻底消除分支判断逻辑。
重构案例:使用策略模式消除复杂分支
假设我们有一个订单支付场景,根据不同的支付类型,执行不同的支付业务逻辑,原始代码的分支逻辑如下:
// 反例:冗长的分支逻辑,新增支付方式时需要修改核心逻辑
public class PaymentService {
  public void pay(String type, BigDecimal amount) {
  if ("ALIPAY".equals(type)) {
  // 支付宝支付业务逻辑
  } else if ("WECHAT".equals(type)) {
  // 微信支付业务逻辑
  } else if ("UNIONPAY".equals(type)) {
  // 银联支付业务逻辑
  } else if ("PAYPAL".equals(type)) {
  // PayPal支付业务逻辑
  } else {
  throw new IllegalArgumentException("不支持的支付类型:" + type);
  }
  }
}
这段代码的主要问题是,新增支付方式时,必须修改pay方法的核心分支逻辑,违反了开闭原则。下面我们使用策略模式 + 工厂模式,对这段代码进行重构:
// 正例:策略模式 + 工厂模式,消除分支逻辑,完全符合开闭原则
// 1. 定义统一的支付策略接口
public interface PaymentStrategy {
  void pay(BigDecimal amount);
}
// 2. 实现不同支付方式的策略类
public class AlipayStrategy implements PaymentStrategy {
  @Override
  public void pay(BigDecimal amount) {
  // 支付宝支付业务逻辑
  }
}
public class WechatPayStrategy implements PaymentStrategy {
  @Override
  public void pay(BigDecimal amount) {
  // 微信支付业务逻辑
  }
}
// 3. 创建支付策略工厂类,负责根据类型创建对应的策略对象
public class PaymentStrategyFactory {
  private static final Map\\<String, PaymentStrategy> STRATEGY\\_MAP = new HashMap<>();
   
  // 工厂类的静态代码块,初始化所有策略类的实例,缓存到map中
  static {
  STRATEGY\\_MAP.put("ALIPAY", new AlipayStrategy());
  STRATEGY\\_MAP.put("WECHAT", new WechatPayStrategy());
  }
   
  // 对外提供统一的获取策略实例的方法
  public static PaymentStrategy getStrategy(String type) {
  PaymentStrategy strategy = STRATEGY\\_MAP.get(type);
  if (strategy == null) {
  throw new IllegalArgumentException("不支持的支付类型:" + type);
  }
  return strategy;
  }
}
// 4. 重构后的PaymentService类,消除了所有分支逻辑
public class PaymentService {
  public void pay(String type, BigDecimal amount) {
  // 从工厂中获取对应的策略实例
  PaymentStrategy strategy = PaymentStrategyFactory.getStrategy(type);
  // 调用统一的支付方法,执行具体的业务逻辑
  strategy.pay(amount);
  }
}
重构后的核心逻辑中,没有任何分支判断代码,所有的分支逻辑都被策略类 + 工厂类的组合替代。后续需要新增支付方式时,只需要新增一个PaymentStrategy接口的实现类,再修改工厂类中的缓存逻辑,不需要修改PaymentService中的核心业务代码,完全符合开闭原则。
这种方案的另一个核心优势,是将每个分支的业务逻辑,从原来的一个大文件中拆解到独立的策略类中,进一步提升了代码的可维护性。在很多主流开源框架中,都有类似的策略模式实现逻辑。



