Java 流程控制:从入门到面试的全方位指南
在 Java 编程中,流程控制是构建代码逻辑的 "骨架"------ 它决定了程序中语句的执行顺序。无论是简单的条件判断,还是复杂的循环迭代,流程控制都是 Java 开发者必须扎实掌握的基础。本文将从入门级知识点出发,逐步深入进阶细节,结合实际开发场景与高频面试题,帮你彻底吃透 Java 流程控制。
一、入门:Java 流程控制的三大核心结构
Java 流程控制遵循 "结构化程序设计" 思想,核心分为三大结构:顺序结构 、分支结构 、循环结构。
1. 顺序结构:程序的默认执行方式
顺序结构是最基础的流程,程序会按照代码的书写顺序 "自上而下" 依次执行,没有任何跳转。
特点:无判断、无循环,逻辑线性。
示例:
public class SequenceDemo {
public static void main(String[] args) {
// 第一步:定义变量
int a = 10;
int b = 20;
// 第二步:计算和
int sum = a + b;
// 第三步:打印结果
System.out.println("两数之和:" + sum); // 输出:两数之和:30
}
}
注意:顺序结构是所有复杂逻辑的基础,分支和循环本质上是 "在顺序执行中插入跳转";实际开发中,变量初始化、对象创建等基础操作均依赖顺序结构。
2. 分支结构:根据条件执行不同逻辑
分支结构用于 "根据条件判断,选择执行不同代码块",Java 中主要有两种实现:if-else 和 switch,此外还有简化条件判断的三元运算符。
(1)if-else:灵活的条件判断
if-else 适用于布尔条件判断(结果为true或false),可根据需求嵌套或组合。
语法格式:
-
基础版(单条件):
if (布尔表达式) {
// 表达式为true时执行
} -
标准版(二选一):
if (布尔表达式) {
// 表达式为true时执行
} else {
// 表达式为false时执行
} -
进阶版(多条件):
if (布尔表达式1) {
// 表达式1为true时执行
} else if (布尔表达式2) {
// 表达式1为false、表达式2为true时执行
} else {
// 所有表达式均为false时执行
}
示例 1:判断学生成绩等级
public class IfElseDemo {
public static void main(String[] args) {
int score = 85;
if (score >= 90) {
System.out.println("等级:优秀");
} else if (score >= 80) {
System.out.println("等级:良好"); // 输出:等级:良好
} else if (score >= 60) {
System.out.println("等级:及格");
} else {
System.out.println("等级:不及格");
}
}
}
示例 2:实际开发场景 ------ 用户登录权限判断
// 模拟用户登录:判断用户名、密码是否正确,且账号是否激活
public class AuthDemo {
public static void main(String[] args) {
String username = "admin";
String password = "123456";
boolean isActive = true;
if (username == null || password == null) {
System.out.println("用户名或密码不能为空");
} else if (!"admin".equals(username) || !"123456".equals(password)) {
System.out.println("用户名或密码错误");
} else if (!isActive) {
System.out.println("账号未激活,请先激活");
} else {
System.out.println("登录成功"); // 输出:登录成功
}
}
}
注意事项:
-
若代码块只有一行语句,大括号{}可省略,但建议保留(避免逻辑漏洞,如后续新增代码时);
-
if-else 具有 "短路特性":if (a && b) 中若a为false,则b不会执行;if (a || b) 中若a为true,则b不会执行;
-
避免 "悬空 else":若if后省略大括号,else会默认匹配最近的if(如if(a>0) if(b>0) doA(); else doB();中,else匹配if(b>0),而非if(a>0))。
(2)三元运算符:简化的二选一判断
三元运算符(条件表达式 ? 表达式1 : 表达式2)是if-else的简化形式,适用于 "二选一" 的简单条件判断,可直接返回结果。
语法格式:
变量 = 布尔表达式 ? 表达式1(true时执行) : 表达式2(false时执行);
示例:
public class TernaryDemo {
public static void main(String[] args) {
int a = 10, b = 20;
// 求两数中的较大值
int max = a > b ? a : b;
System.out.println("最大值:" + max); // 输出:最大值:20
// 简化if-else的字符串拼接
String result = (a % 2 == 0) ? a + "是偶数" : a + "是奇数";
System.out.println(result); // 输出:10是偶数
}
}
与 if-else 的区别:
维度 | 三元运算符 | if-else |
---|---|---|
返回值 | 必须有返回值(可直接赋值给变量) | 无返回值(需手动在代码块中赋值) |
适用场景 | 简单二选一判断(如赋值、简单计算) | 复杂逻辑(如多语句执行、嵌套判断) |
可读性 | 简单场景下更简洁 | 复杂场景下更清晰 |
注意:三元运算符的 "表达式 1" 和 "表达式 2" 需返回相同类型(或可自动转换的类型,如int和long),否则编译报错。
(3)switch:多值匹配的分支
switch 适用于单个变量与多个固定值匹配的场景(如根据枚举、整数、字符串匹配逻辑),Java 7 + 支持String,Java 14 + 支持 "增强 switch"(更简洁)。
传统语法格式:
switch (表达式) { // 表达式类型:byte、short、int、char、枚举、String(Java7+)
case 常量1:
// 表达式等于常量1时执行
break; // 跳出switch(若无break,会发生"case穿透")
case 常量2:
// 表达式等于常量2时执行
break;
default:
// 表达式不匹配任何case时执行(可选)
}
增强 switch 语法(Java14+):
支持箭头语法->,无需手动写break,且可通过yield返回值,更简洁。
public class SwitchDemo {
public static void main(String[] args) {
String season = "夏季";
// 增强switch(箭头语法)
String result = switch (season) {
case "春季" -> "春暖花开,适合踏青";
case "夏季" -> "夏日炎炎,注意防暑"; // 匹配此分支
case "秋季" -> "秋高气爽,适合登山";
case "冬季" -> "冬雪皑皑,注意保暖";
default -> "未知季节,请检查输入";
};
System.out.println(result); // 输出:夏日炎炎,注意防暑
}
}
进阶实践:switch 与枚举的结合(实际开发高频场景)
枚举(enum)的核心优势是 "限定取值范围",配合switch使用可避免非法值,代码更健壮。
示例:订单状态流转判断
// 定义订单状态枚举
enum OrderStatus {
PENDING_PAYMENT, // 待支付
PAID, // 已支付
SHIPPED, // 已发货
DELIVERED, // 已送达
CANCELLED // 已取消
}
public class EnumSwitchDemo {
// 根据当前状态,判断是否允许取消订单
public static boolean canCancel(OrderStatus status) {
return switch (status) {
// 仅待支付状态可取消
case PENDING_PAYMENT -> true;
// 其他状态均不可取消
case PAID, SHIPPED, DELIVERED, CANCELLED -> false;
};
}
public static void main(String[] args) {
OrderStatus currentStatus = OrderStatus.PAID;
System.out.println("当前订单是否可取消:" + canCancel(currentStatus)); // 输出:false
}
}
注意事项:
-
传统switch中,case后必须是 "常量表达式"(如10、"abc",不能是变量);
-
若传统switch省略break,会触发 "case 穿透"(即匹配成功后,继续执行后续所有case代码,直到遇到break或switch结束);
-
增强switch的yield可用于返回值(类似return),适用于需要从switch中获取结果的场景;
-
switch表达式为null时,会抛出NullPointerException(需提前判空,尤其处理String类型时)。
3. 循环结构:重复执行代码块
循环结构用于 "满足条件时,重复执行某段代码",Java 中主要有三种实现:for、while、do-while,此外还有 Java 8 + 的Stream流(简化集合循环,但本质依赖流程控制)。
(1)for 循环:明确循环次数的场景
for循环适用于已知循环次数的场景(如遍历数组、集合),语法简洁,将 "初始化、条件判断、更新变量" 集中在一处。
语法格式:
for (初始化表达式; 条件判断表达式; 更新表达式) {
// 循环体(条件为true时执行)
}
-
初始化表达式:循环前执行一次(如int i = 0);
-
条件判断表达式:每次循环前判断(结果为true则执行循环体);
-
更新表达式:每次循环体执行后执行(如i++)。
示例 1:遍历数组(传统 for 与增强 for 对比)
public class ForDemo {
public static void main(String[] args) {
int[] nums = {1, 2, 3, 4, 5};
// 传统for循环:需索引,可修改元素
System.out.println("传统for循环(修改前):");
for (int i = 0; i < nums.length; i++) {
nums[i] *= 2; // 修改数组元素
System.out.print(nums[i] + " "); // 输出:2 4 6 8 10
}
// 增强for循环(foreach):无需索引,不可修改基本类型元素
System.out.println("\n增强for循环(遍历修改后):");
for (int num : nums) {
// num = num + 1; // 仅修改临时变量,不影响原数组
System.out.print(num + " "); // 输出:2 4 6 8 10
}
}
}
示例 2:实际开发场景 ------ 批量处理数据库查询结果
import java.util.ArrayList;
import java.util.List;
// 模拟从数据库查询用户列表,批量更新用户状态
public class ForDBDemo {
// 模拟数据库查询:返回用户ID列表
public static List<Long> getUserIdList() {
List<Long> ids = new ArrayList<>();
ids.add(1001L);
ids.add(1002L);
ids.add(1003L);
return ids;
}
// 模拟更新用户状态
public static void updateUserStatus(Long userId, String status) {
System.out.println("更新用户" + userId + "状态为:" + status);
}
public static void main(String[] args) {
List<Long> userIdList = getUserIdList();
// 批量更新状态为"已激活"
for (Long userId : userIdList) {
updateUserStatus(userId, "已激活");
}
// 输出:
// 更新用户1001状态为:已激活
// 更新用户1002状态为:已激活
// 更新用户1003状态为:已激活
}
}
注意:
-
增强for循环(foreach)底层依赖Iterator,适用于 "仅遍历" 场景;若需修改数组 / 集合元素(基本类型)或获取索引,需用传统for循环;
-
for循环的初始化、条件判断、更新表达式均可省略(如for(;;)会变成死循环,需在循环体内通过break终止)。
(2)while 循环:未知循环次数,先判断后执行
while循环适用于未知循环次数的场景,语法中仅包含 "条件判断",初始化和更新需在外部手动处理。
语法格式:
// 初始化变量
while (条件判断表达式) {
// 循环体(条件为true时执行)
// 更新变量(避免死循环)
}
示例 1:计算 1~100 的和
public class WhileDemo {
public static void main(String[] args) {
int sum = 0;
int i = 1; // 初始化
while (i <= 100) { // 条件判断
sum += i;
i++; // 更新变量
}
System.out.println("1~100的和:" + sum); // 输出:5050
}
}
示例 2:实际开发场景 ------ 服务器监听客户端连接(死循环的合理应用)
import java.net.ServerSocket;
import java.net.Socket;
// 模拟TCP服务器:持续监听客户端连接(死循环+break终止)
public class WhileServerDemo {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(8080);
System.out.println("服务器已启动,监听8080端口...");
// 死循环:持续等待客户端连接
while (true) {
Socket clientSocket = serverSocket.accept(); // 阻塞等待连接
System.out.println("新客户端连接:" + clientSocket.getInetAddress());
// 模拟处理逻辑(实际开发中会启线程处理)
// handleClient(clientSocket);
// 若满足终止条件(如收到关闭指令),跳出循环
if (isShutdownCommandReceived()) {
System.out.println("收到关闭指令,服务器停止监听");
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (serverSocket != null) serverSocket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 模拟判断是否收到关闭指令
private static boolean isShutdownCommandReceived() {
// 实际开发中会监听控制台输入或远程指令
return false; // 此处返回false,模拟持续运行
}
}
注意:while循环的 "条件判断" 在循环体之前,若初始条件为false,循环体一次都不会执行。
(3)do-while 循环:未知循环次数,先执行后判断
do-while循环与while类似,但先执行一次循环体,再判断条件------ 无论初始条件是否成立,循环体至少执行一次。
语法格式:
// 初始化变量
do {
// 循环体(至少执行一次)
// 更新变量
} while (条件判断表达式); // 注意末尾的分号
示例:实际开发场景 ------ 用户输入验证(确保至少获取一次输入)
import java.util.Scanner;
// 要求用户输入1~100的整数,直到输入合法为止
public class DoWhileDemo {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int input;
do {
System.out.print("请输入1~100的整数:");
// 检查输入是否为整数
while (!scanner.hasNextInt()) {
System.out.print("输入不是整数,请重新输入:");
scanner.next(); // 清空非法输入
}
input = scanner.nextInt();
// 检查输入范围
if (input < 1 || input > 100) {
System.out.println("输入超出范围(1~100),请重新输入");
}
} while (input < 1 || input > 100); // 输入非法则继续循环
System.out.println("输入合法:" + input);
}
}
4. 循环跳转语句:控制循环的执行流程
在循环中,可通过break、continue、return控制执行流程,避免不必要的循环,提升效率。此外,还可通过 "带标签的跳转" 控制嵌套循环。
语句 | 作用 |
---|---|
break | 跳出当前循环(或switch),直接执行循环后的代码 |
continue | 跳过当前循环的剩余代码,直接进入下一次循环的条件判断 |
return | 直接结束当前方法(无论是否在循环中),方法内后续代码均不执行 |
带标签跳转 | 跳出 / 跳过指定标签的循环(适用于嵌套循环) |
示例 1:break 与 continue 的基础使用
public class JumpDemo {
public static void main(String[] args) {
System.out.println("break示例:");
for (int i = 1; i <= 5; i++) {
if (i == 3) {
break; // 跳出循环
}
System.out.print(i + " "); // 输出:1 2
}
System.out.println("\ncontinue示例:");
for (int i = 1; i <= 5; i++) {
if (i == 3) {
continue; // 跳过当前迭代
}
System.out.print(i + " "); // 输出:1 2 4 5
}
}
}
示例 2:带标签的 continue(嵌套循环场景)
// 嵌套循环:查找二维数组中的偶数,跳过当前行的剩余元素
public class LabelContinueDemo {
public static void main(String[] args) {
int[][] matrix = {{1, 3, 4}, {2, 5, 7}, {6, 8, 9}};
// 外层循环标签:rowLoop
rowLoop:
for (int i = 0; i < matrix.length; i++) {
System.out.print("第" + (i+1) + "行的偶数:");
for (int j = 0; j < matrix[i].length; j++) {
if (matrix[i][j] % 2 != 0) {
continue; // 跳过当前列(奇数)
}
System.out.print(matrix[i][j] + " ");
// 找到当前行第一个偶数后,跳过当前行剩余元素(进入外层循环下一行)
continue rowLoop;
}
System.out.println();
}
// 输出:
// 第1行的偶数:4
// 第2行的偶数:2
// 第3行的偶数:6
}
}
示例 3:return 在循环中的使用
// 查找数组中的目标元素,找到后直接返回索引(无需继续循环)
public class ReturnInLoopDemo {
public static int findIndex(int[] arr, int target) {
if (arr == null || arr.length == 0) {
return -1;
}
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i; // 找到目标,直接返回(终止方法)
}
}
return -1; // 未找到
}
public static void main(String[] args) {
int[] nums = {10, 20, 30, 40};
int target = 30;
System.out.println("目标元素索引:" + findIndex(nums, target)); // 输出:2
}
}
二、进阶:流程控制的关键细节与避坑
掌握基础后,需关注实际开发中的 "进阶细节",避免踩坑,同时结合 Java 新特性提升代码质量。
1. switch 的核心细节与扩展
- 支持的数据类型底层原理:
Java 7 前switch仅支持byte、short、int、char,本质是因为这些类型可通过 "自动类型提升" 转为int(如char通过 ASCII 码转int);Java 7 + 支持String,底层是通过String.hashCode()将字符串转为整数,再结合equals()避免哈希冲突(因此case中的字符串需与表达式字符串 "equals 且 hashCode 相等");Java 5 + 支持枚举,底层是通过枚举的ordinal()方法获取索引(int类型)匹配。
- case 穿透的合理利用与风险:
传统switch中,若多个case逻辑相同,可省略break实现 "穿透",简化代码(如判断星期几是否为工作日);但需注意 "意外穿透"------ 若忘记写break,会导致后续case被执行(如case 1无break,会继续执行case 2的代码)。
示例(合理利用穿透):
// 判断月份所属季度
public static String getQuarter(int month) {
switch (month) {
case 1:
case 2:
case 3:
return "第一季度";
case 4:
case 5:
case 6:
return "第二季度";
case 7:
case 8:
case 9:
return "第三季度";
case 10:
case 11:
case 12:
return "第四季度";
default:
return "无效月份";
}
}
2. 循环的效率优化与避坑
(1)效率优化技巧
- 避免循环内创建对象:
循环内频繁创建对象会导致 JVM 垃圾回收(GC)频繁触发,影响性能。例如:
// 优化前:循环内创建StringBuilder
for (int i = 0; i < 1000; i++) {
StringBuilder sb = new StringBuilder();
sb.append("第").append(i).append("次循环");
}
// 优化后:循环外创建对象,循环内复用
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.setLength(0); // 清空内容(复用对象)
sb.append("第").append(i).append("次循环");
}
- 减少循环内的重复计算:
循环条件或循环体内的重复计算(如list.size()、Math.sqrt(x))会增加开销,应提前计算并缓存。例如:
// 优化前:每次循环都调用list.size()
List<String> list = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
// 业务逻辑
}
// 优化后:提前缓存size
int size = list.size();
for (int i = 0; i < size; i++) {
// 业务逻辑
}
- 循环展开(Loop Unrolling):
对于循环次数固定且较大的场景,可通过 "循环展开" 减少循环判断和更新的次数(Java 编译器会自动优化,但手动优化可进一步提升性能)。例如:
// 优化前:循环100次,每次判断1次
for (int i = 0; i < 100; i++) {
process(i);
}
// 优化后:循环25次,每次处理4个元素(减少75%的循环判断)
for (int i = 0; i < 100; i += 4) {
process(i);
process(i+1);
process(i+2);
process(i+3);
}
(2)常见循环陷阱
- foreach 的并发修改异常(ConcurrentModificationException):
遍历非线程安全的集合(如ArrayList)时,若在循环中修改集合(添加 / 删除元素),会触发ConcurrentModificationException(因为foreach底层依赖Iterator,修改集合会导致迭代器的 "修改次数" 与集合不一致)。
解决方案:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ForeachModifyDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
// 方案1:使用迭代器remove()
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("B".equals(item)) {
iterator.remove(); // 安全删除
}
}
System.out.println(list); // 输出:[A, C]
// 方案2:使用CopyOnWriteArrayList
List<String> safeList = new java.util.concurrent.CopyOnWriteArrayList<>();
safeList.add("X");
safeList.add("Y");
for (String item : safeList) {
if ("Y".equals(item)) {
safeList.remove(item); // 安全删除
}
}
System.out.println(safeList); // 输出:[X]
}
}
-
- 使用迭代器的remove()方法(而非集合的remove());
-
- 使用线程安全的集合(如CopyOnWriteArrayList);
-
- 遍历前创建集合的副本(如new ArrayList<>(list))。
示例(正确写法):
- 死循环的排查与避免:
死循环的常见原因:
-
- 循环条件永远为true(如while (true)无break);
-
- 循环变量未更新(如for (int i = 0; i < 10; ) { ... }忘记i++);
-
- 循环变量更新方向错误(如for (int i = 10; i > 0; i++) { ... },i永远递增)。
排查方法:
-
- 在循环体内打印循环变量(如System.out.println("i=" + i)),观察变量变化;
-
- 使用调试工具(如 IDEA Debug)断点跟踪循环流程;
-
- 为循环添加 "最大执行次数限制"(如int maxCount = 1000; while (condition && --maxCount > 0) { ... }),避免程序卡死。
3. 流程控制与函数式编程的结合(Java 8+)
Java 8 引入的函数式接口(如Predicate、Consumer)可简化流程控制逻辑,让代码更简洁、易读。
(1)用 Predicate 简化复杂 if 条件
Predicate是 "条件判断接口",可将多个条件封装为 Predicate 对象,通过and()、or()、negate()组合,避免if-else嵌套过深。
示例:用户注册条件校验
import java.util.function.Predicate;
// 校验用户注册信息:用户名非空、密码长度>=6、手机号格式正确
public class PredicateDemo {
// 用户名非空
private static Predicate<String> usernameCheck = (username) -> username != null && !username.trim().isEmpty();
// 密码长度>=6
private static Predicate<String> passwordCheck = (password) -> password != null && password.length() >= 6;
// 手机号格式(简单校验:11位数字)
private static Predicate<String> phoneCheck = (phone) -> phone != null && phone.matches("^1[3-9]\\d{9}$");
// 组合校验:所有条件需满足
public static boolean validateUser(String username, String password, String phone) {
return usernameCheck.test(username)
&& passwordCheck.test(password)
&& phoneCheck.test(phone);
}
public static void main(String[] args) {
String username = "zhangsan";
String password = "123456";
String phone = "13800138000";
if (validateUser(username, password, phone)) {
System.out.println("注册信息校验通过");
} else {
System.out.println("注册信息校验失败");
}
}
}
(2)用 Consumer 简化循环逻辑
Consumer是 "消费接口",可将循环体内的业务逻辑封装为 Consumer 对象,配合forEach()遍历集合,替代传统循环。
示例:批量处理用户列表
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
class User {
private Long id;
private String name;
private int age;
// 构造器、getter、setter省略
public User(Long id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "'}";
}
}
public class ConsumerDemo {
public static void main(String[] args) {
List<User> userList = new ArrayList<>();
userList.add(new User(1L, "张三", 20));
userList.add(new User(2L, "李四", 25));
userList.add(new User(3L, "王五", 17));
// 封装"打印成年用户"的逻辑
Consumer<User> adultUserPrinter = (user) -> {
if (user.getAge() >= 18) {
System.out.println("成年用户:" + user);
}
};
// 封装"更新用户年龄+1"的逻辑
Consumer<User> ageIncrementer = (user) -> user.setAge(user.getAge() + 1);
// 1. 遍历并打印成年用户
System.out.println("成年用户列表:");
userList.forEach(adultUserPrinter);
// 2. 遍历并更新所有用户年龄(链式调用)
userList.forEach(ageIncrementer.andThen(adultUserPrinter));
// 输出:
// 成年用户列表:
// 成年用户:User{id=1, name='张三'}
// 成年用户:User{id=2, name='李四'}
// 成年用户:User{id=1, name='张三'}
// 成年用户:User{id=2, name='李四'}
// 成年用户:User{id=3, name='王五'}(年龄已变为18)
}
}
三、面试:Java 流程控制高频题及解析
流程控制是 Java 面试的 "基础必考题",以下覆盖语法、原理、场景应用,帮你应对面试。
面试题 1:switch 支持哪些数据类型?为什么不支持 long?
答案:
-
支持的类型:byte、short、int、char、枚举(Java5+)、String(Java7+)。
-
不支持long的原因:switch底层依赖 JVM 的tableswitch或lookupswitch指令,这两种指令仅支持int类型的操作数。byte、short、char会通过 "自动类型提升" 转为int(如byte b=10→int 10,char 'A'→int 65),而long的取值范围(-9223372036854775808~9223372036854775807)远超int,无法直接转为int(会丢失精度),因此不支持。
-
扩展:若需用long匹配,可手动将long转为int(需确保值在int范围内),或用if-else判断。
面试题 2:if-else 和三元运算符的区别?在什么场景下选择使用?
答案:
两者核心区别体现在 "返回值" 和 "适用场景":
对比维度 | 三元运算符 | if-else |
---|---|---|
返回值要求 | 必须有返回值(可直接赋值给变量) | 无返回值(需在代码块中手动处理逻辑) |
执行逻辑复杂度 | 仅支持 "简单二选一"(单条表达式) | 支持复杂逻辑(多条语句、嵌套判断) |
可读性 | 简单场景下更简洁(一行代码) | 复杂场景下更清晰(代码块分层) |
副作用风险 | 若表达式 1/2 有副作用(如i++),可能导致逻辑混淆 | 副作用更可控(代码块内逻辑明确) |
选择场景:
-
用三元运算符:简单的 "二选一赋值" 场景(如求两数最大值、判断奇偶并返回字符串),例如int max = a > b ? a : b;;
-
用 if-else:复杂逻辑场景(如多语句执行、嵌套判断、无返回值操作),例如 "判断用户权限并执行不同业务逻辑(如跳转页面、打印日志)"。
注意:三元运算符的表达式 1 和表达式 2 需返回相同类型(或可自动转换的类型),否则编译报错(如String result = (a>0) ? "正数" : 0;会报错,因为String和int无法兼容)。
面试题 3:foreach 遍历集合时,为什么不能直接修改集合元素(添加 / 删除)?如何解决?
答案:
(1)不能修改的原因:并发修改异常(ConcurrentModificationException)
foreach底层依赖Iterator(迭代器)实现,迭代器在创建时会记录集合的 "修改次数"(modCount)。遍历过程中,若通过集合的add()/remove()方法修改集合,会导致集合的modCount增加,而迭代器的 "预期修改次数"(expectedModCount)未更新,两者不一致时,迭代器会抛出ConcurrentModificationException,以避免 "迭代器遍历的元素与集合实际元素不一致" 的问题。
(2)解决方案(3 种常见方式):
-
**使用迭代器的****remove()**方法:迭代器的remove()会同步更新expectedModCount和modCount,避免异常(仅支持删除,不支持添加);
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("B".equals(item)) {
iterator.remove(); // 安全删除
}
} -
使用线程安全的集合:如CopyOnWriteArrayList(读写分离,修改时复制新数组,遍历旧数组,因此不会触发异常),适合读多写少的场景;
List<String> list = new java.util.concurrent.CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
for (String item : list) {
if ("B".equals(item)) {
list.remove(item); // 安全删除
}
} -
遍历前创建集合副本:遍历副本,修改原集合(副本与原集合独立,迭代器不会感知原集合的修改);
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
// 遍历副本
for (String item : new ArrayList<>(list)) {
if ("B".equals(item)) {
list.remove(item); // 安全删除原集合元素
}
}
面试题 4:如何用循环实现 "冒泡排序"?并优化冒泡排序的效率?
答案:
(1)冒泡排序的核心思想
通过相邻元素的比较与交换,将 "最大元素" 逐步 "冒泡" 到数组末尾(或 "最小元素" 冒泡到数组开头),每一轮循环确定一个元素的最终位置。
(2)基础实现(未优化)
// 基础冒泡排序:升序排列
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
int n = arr.length;
// 外层循环:控制排序轮次(n个元素需n-1轮)
for (int i = 0; i < n - 1; i++) {
// 内层循环:每轮比较相邻元素,将最大元素冒泡到末尾
// 每轮后,末尾i个元素已排序,无需再比较
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换相邻元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
(3)效率优化(2 个关键优化点)
-
添加 "有序标记":若某一轮循环中未发生任何交换,说明数组已有序,可直接退出循环(避免后续无效轮次);
-
记录 "最后交换位置":若数组后半部分已有序,可通过记录最后一次交换的位置,减少内层循环的比较次数(无需比较已有序的部分)。
优化后的代码:
public static void optimizedBubbleSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
int n = arr.length;
int lastSwapIndex = 0; // 记录最后一次交换的位置
int border = n - 1; // 无序区域的边界(边界外为有序)
boolean isSorted = false; // 标记数组是否已有序
for (int i = 0; i < n - 1 && !isSorted; i++) {
isSorted = true; // 假设本轮有序
for (int j = 0; j < border; j++) {
if (arr[j] > arr[j + 1]) {
// 交换元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
isSorted = false; // 发生交换,数组未有序
lastSwapIndex = j; // 更新最后交换位置
}
}
border = lastSwapIndex; // 更新无序区域边界
}
}
优化效果:
-
最好情况(数组已有序):时间复杂度从 O (n²) 降至 O (n)(仅需 1 轮循环);
-
平均情况:时间复杂度仍为 O (n²),但实际执行次数减少(减少无效比较)。
面试题 5:Java 14 的增强 switch 相比传统 switch,有哪些核心改进?请举例说明 yield 的使用场景。
答案:
Java 14 引入的 "增强 switch"(预览特性,Java 17 正式转正)主要有 3 点核心改进,解决了传统 switch 的痛点:
(1)核心改进
- 箭头语法 **->替代 case:****+**break:
传统 switch 需手动写break避免 "case 穿透",增强 switch 的->会自动跳出 switch(无需break),代码更简洁,减少 "忘记写 break" 的错误。
- 支持 "多 case 合并":
传统 switch 需重复写case 1:、case 2:,增强 switch 可通过case 1,2,3 -> ...合并多个 case,逻辑更清晰。
- 支持yield返回值:
传统 switch 无法直接返回值(需在 case 中手动赋值给外部变量),增强 switch 可通过yield返回值,可直接作为表达式赋值给变量(类似三元运算符)。
(2)yield 的使用场景
yield用于 "从 switch 中返回值",适用于 "根据条件匹配,返回不同结果" 的场景(如计算折扣、转换状态码)。
示例:根据用户等级计算折扣
// 用户等级枚举
enum UserLevel {
VIP1, VIP2, VIP3, NORMAL
}
public class EnhancedSwitchYieldDemo {
// 根据用户等级计算折扣(返回0~1的小数)
public static double calculateDiscount(UserLevel level) {
return switch (level) {
case VIP1 -> {
System.out.println("VIP1用户,折扣10%");
yield 0.9; // 返回折扣值
}
case VIP2 -> {
System.out.println("VIP2用户,折扣20%");
yield 0.8;
}
case VIP3 -> {
System.out.println("VIP3用户,折扣30%");
yield 0.7;
}
case NORMAL -> {
System.out.println("普通用户,无折扣");
yield 1.0;
}
};
}
public static void main(String[] args) {
UserLevel userLevel = UserLevel.VIP2;
double discount = calculateDiscount(userLevel);
System.out.println("最终折扣:" + discount);
// 输出:
// VIP2用户,折扣20%
// 最终折扣:0.8
}
}
注意:yield仅在增强 switch 的 "代码块模式"(case ... -> { ... })中使用,若为 "表达式模式"(case ... -> 表达式),则直接返回表达式结果(无需yield),如case VIP1 -> 0.9。
面试题 6:break、continue、return 在循环中的区别?请用代码示例说明。
答案:
三者的核心区别在于 "作用范围" 和 "终止程度",具体如下:
(1)break:终止当前循环 /switch,执行后续代码
-
作用范围:当前循环(或 switch);
-
效果:跳出当前循环,继续执行循环外的代码;
-
示例:
public static void breakDemo() {
System.out.println("break示例:");
for (int i = 1; i <= 5; i++) {
if (i == 3) {
break; // 跳出当前for循环
}
System.out.print(i + " "); // 执行到i=3时停止
}
System.out.println("\n循环外代码"); // 会执行
// 输出:
// break示例:
// 1 2
// 循环外代码
}
(2)continue:跳过当前迭代,进入下一次循环判断
-
作用范围:当前循环的当前迭代;
-
效果:跳过当前迭代的剩余代码,直接进入下一次循环的条件判断(循环不终止);
-
示例:
public static void continueDemo() {
System.out.println("continue示例:");
for (int i = 1; i <= 5; i++) {
if (i == 3) {
continue; // 跳过i=3的迭代,进入i=4的判断
}
System.out.print(i + " "); // 不输出i=3
}
System.out.println("\n循环外代码"); // 会执行
// 输出:
// continue示例:
// 1 2 4 5
// 循环外代码
}
(3)return:终止当前方法,无论是否在循环中
-
作用范围:当前方法;
-
效果:直接结束当前方法,方法内后续代码(包括循环外的代码)均不执行;
-
示例:
public static void returnDemo() {
System.out.println("return示例:");
for (int i = 1; i <= 5; i++) {
if (i == 3) {
return; // 终止当前方法
}
System.out.print(i + " "); // 执行到i=3时停止
}
System.out.println("\n循环外代码"); // 不会执行
// 输出:
// return示例:
// 1 2
}
总结:
-
想 "跳出当前循环,继续执行方法"→用break;
-
想 "跳过当前迭代,继续循环"→用continue;
-
想 "直接结束方法,不再执行任何代码"→用return。
四、总结
Java 流程控制是构建代码逻辑的基础,核心在于 "根据场景选择合适的结构",并结合进阶细节与新特性提升代码质量:
- 结构选择原则:
-
- 简单线性逻辑→顺序结构;
-
- 条件判断:简单二选一→三元运算符 ,复杂条件→if-else ,固定值匹配→switch(尤其配合枚举);
-
- 循环执行:已知次数→for (传统 / 增强),未知次数→while (先判断)或do-while(先执行);
-
- 流程控制:跳出循环→break ,跳过迭代→continue ,终止方法→return。
- 进阶避坑要点:
-
- switch:避免 "case 穿透"(增强 switch 无需手动 break),注意null风险;
-
- 循环:避免循环内创建对象,处理ConcurrentModificationException,排查死循环;
-
- 新特性:结合Predicate/Consumer简化条件判断与循环逻辑(Java 8+)。
- 面试准备建议:
-
- 掌握底层原理(如 switch 支持类型的底层原因);
-
- 熟悉实际场景(如 foreach 并发修改问题、死循环的合理应用);
-
- 能手写经典算法(如冒泡排序、斐波那契数列),并优化效率。
流程控制的本质是 "让代码按预期顺序执行",只有扎实掌握基础,结合实际场景灵活运用,才能写出高效、健壮、易维护的 Java 代码。