顺序执行像一条直路:上一句结束,下一句开始。真正的程序却经常遇到岔路——年龄不同,票价不同;订单金额不同,运费不同;账号状态不同,可执行的操作也不同。Java 的决策结构解决的就是这件事:先算出一个明确的真假值,再让控制流沿其中一条路径前进。
这一篇不把 if 和 switch 当成几组孤立语法。我们会从“怎样把规则问成一个布尔问题”开始,逐步处理短路、区间边界、字符串与浮点比较、悬空 else、传统与现代 switch、确定赋值,以及怎样用测试和调试证明每条路径都符合预期。
代码约定:带 public class 的围栏是可以单独保存的完整示例;其他 Java 围栏都是放在当前讲解所指 main 方法或类体中的片段。为了让控制流边界一眼可见,示例即使只有一条分支语句也会保留大括号。
本章采用 Java SE 26 的正式语言规则。模式匹配只说明与普通决策结构直接相关的边界,不展开预览功能,也不提前进入循环、数组和完整异常处理。
程序遇到决策时,会经历三个动作:求值条件、选择路径、在分支结束后继续。条件只能产生 true 或 false。如果有 else,两条路径恰好执行一条;如果没有 else,条件为假时就跳过分支体。
假设某展馆采用一条练习规则:未满 18 岁票价 20 元,其余访客票价 35 元。完整程序可以这样写:
public class TicketDecision {
public static void main(String[] args) {
int age = 17;
int price;
if (age < 18) {
price = 20;
} else {
price = 35;
}
System.out.println("票价:" + price + " 元");
}
}执行到 if 时,Java 先计算 age < 18。当前结果为 true,所以只执行第一块;else 块被跳过。无论走哪条路径,控制流最后都会到达 println。

一次布尔判断只会选择一条路径,分支完成后再汇合并继续执行。
if 后面语法上需要的是一条语句。块 { ... } 本身也算一条结构化语句,因此可以把声明、赋值、输出等多条语句一起纳入分支:
if (age < 18) {
int saved = 15;
System.out.println("已减免:" + saved + " 元");
System.out.println("使用学生价");
}变量 saved 的作用域只到这个块的右大括号。块外不能继续使用它。缩进帮助人看结构,真正决定边界的是大括号。
下面两段看起来只差一行,含义却不同:
// 两条输出都受条件控制
if (age < 18) {
System.out.println("学生价");
System.out.println("请出示有效凭证");
}
// 只有第一条输出受条件控制
if (age < 18)
System.out.println("学生价");
System.out.println("请出示有效凭证");第二段中,最后一行总会执行。代码以后增加一行时,省略大括号尤其容易制造这种语义错误。
if (condition); 中分号是一条“什么也不做”的空语句。此时 if 控制的是空语句,后面的块反而会无条件执行。看到条件右括号后紧跟分号,应优先检查它是不是误写。
写决策代码时,最费脑筋的通常不是 if 这个单词,而是括号里的问题是否准确。一个好条件应该能被明确回答为“真”或“假”,并且它的边界能用具体输入验证。
数值关系运算符都会产生 boolean:
条件可以先保存为有业务含义的变量:
int age = 20;
boolean isAdult = age >= 18;
if (isAdult) {
System.out.println("进入成人票流程");
}boolean 是基本类型,值只有 true 和 false。包装类型 Boolean 还可能为 null。把一个为 null 的 Boolean 直接放进 if,Java 需要拆箱,运行时会失败。因此,纯真假状态优先使用 boolean;如果数据确实有“未知”第三种状态,就要先明确处理 null。
Boolean approved = null;
if (Boolean.TRUE.equals(approved)) {
System.out.println("已批准");
} else {
System.out.println("未批准或状态未知");
}某活动要求“年龄至少 12 岁,并且身高不低于 1.4 米”,可以逐句翻译:
int age = 14;
double height = 1.55;
boolean oldEnough = age >= 12;
boolean tallEnough = height >= 1.4;
boolean allowed = oldEnough && tallEnough;常用布尔运算符是:
&&:两侧都为真,整体才为真。||:至少一侧为真,整体就为真。!:把真假反转。比如“普通账号且没有被锁定”可写成 isRegular && !isLocked。如果条件已经是布尔变量,直接写 if (isLocked) 或 if (!isLocked),比 isLocked == true、isLocked == false 更直接。
! 的优先级高于关系运算,关系运算高于 ==,&& 又高于 ||。不过,业务条件不值得让人靠背优先级猜:
boolean canEdit = isOwner || (isEditor && !isLocked);这句表示:所有者可以编辑;或者同时是编辑者且资源没有锁定。若去掉括号,当前结果仍相同,但读代码的人要额外推导分组。条件一旦出现两种逻辑连接,主动加括号通常更清楚。
char 可以按其数值参与 <、> 等比较,但这只是字符编码顺序,不等于适合所有语言的字典排序。字符串内容比较和排序应使用字符串提供的方法或专门的排序规则。
Java 保证表达式的操作数按从左到右的顺序求值。&& 和 || 还有更具体的规则:结果一旦能由左侧确定,右侧就不会执行。
left && right:左侧为 false 时,整体必为 false,跳过右侧。left || right:左侧为 true 时,整体必为 true,跳过右侧。
短路求值遵循先左后右:左侧结果会决定右侧表达式是否执行。
这个规则最常见的用途,是先检查“能不能安全求值”,再执行真正的判断:
String userName = null;
if (userName != null && !userName.isBlank()) {
System.out.println("用户名有效");
}当前左侧为 false,所以 isBlank() 不会被调用,避免了对 null 调用实例方法。两侧顺序不能随意交换:
// 错误顺序:userName 为 null 时,左侧调用已经失败
if (!userName.isBlank() && userName != null) {
System.out.println("用户名有效");
}除法也能用同样思路保护:
int total = 120;
int count = 0;
if (count != 0 && total / count >= 60) {
System.out.println("平均值达标");
}右侧可能包含方法调用、计数、自增、日志或其他副作用。短路意味着这些动作不保证发生:
boolean cached = true;
if (cached || refreshFromServer()) {
System.out.println("数据可用");
}当 cached 为 true 时,refreshFromServer() 根本不会执行。如果刷新本身是必须动作,就不该把它藏在 || 右侧。更稳妥的写法是先单独调用,再组合结果。
& 和 | 在两个布尔操作数上也能计算真假,但它们总会计算两侧。日常条件判断优先使用 && 和 ||;只有明确需要两侧都执行时,才考虑非短路形式,而且应让意图非常清楚。
短路可以保护危险操作,却不应掩盖数据设计问题。若一个值允许为 null,要说明 null 代表什么;若分母可能为零,要说明这类输入应被拒绝、跳过还是产生单独结果。
决策结构的形状应贴合规则本身。先判断规则是“做或不做”“二选一”,还是“从多个互斥类别中选一个”,再决定用哪种结构。
权限检查中,只有管理员需要显示管理入口:
if (isAdmin) {
System.out.println("显示管理入口");
}条件不满足时什么也不做,这就是简单 if 的适用场景。
订单满 99 元免基础运费,否则收取 8 元:
int orderAmount = 128;
int shippingFee;
if (orderAmount >= 99) {
shippingFee = 0;
} else {
shippingFee = 8;
}两条路径都给 shippingFee 赋值,分支结束后可以安全使用。
成绩分档必须从更严格的上界开始:
int score = 86;
String level;
if (score >= 90) {
level = "优秀";
} else if (score >= 80) {
level = "良好";
} else if (score >= 60) {
level = "合格";
} else {
level =
86 同时满足 >= 80 和 >= 60,但 Java 在第一个真条件处停止,后面的条件不再求值。若把 score >= 60 放在最前面,所有及格分数都会过早进入“合格”,更高档位永远到不了。
嵌套适合“先进入一个大类,再判断类内细节”:
if (accountActive) {
if (isAdmin) {
System.out.println("允许进入管理台");
} else {
System.out.println("进入普通工作台");
}
} else {
System.out.println("账号不可用");
}省略大括号时,else 总是归属于语法上最近、仍能接收 else 的 if。这就是悬空 else:
if (accountActive)
if (isAdmin)
System.out.println("管理台");
else
System.out.println("普通工作台");这里的 else 属于 if (isAdmin),与缩进暗示无关。实践中最简单的处理不是背匹配规则,而是始终用大括号明确归属。
选择结构时可以先问:所有情况是否互斥?是否必须得到一个结果?条件是否有明显优先级?这三个问题比“能不能用 if 写出来”更有用。
多分支代码最常见的错误不是语法错,而是区间有空洞、重叠,或非法输入悄悄落进某个正常类别。解决办法是先设计规则,再写条件。
假设成绩必须在 0 到 100 之间,可以先把规则写成表:
这张表表达了两个信息:非法输入优先处理;正常档位从高到低排列。翻译成代码后,每个输入只会落入一行:
int score = 101;
String result;
if (score < 0 || score > 100) {
result = "输入无效";
} else if (score >= 90) {
result = "优秀";
} else if (score >= 80) {
result = "良好";
}

边界三点测试同时覆盖阈值前、阈值本身和阈值后,确认区间互斥且完整。
把每个条件单独写成上下界时,最容易出现重叠:
// 80 会同时满足前两个独立条件
if (score >= 60 && score <= 80) {
System.out.println("合格");
}
if (score >= 80 && score <= 100) {
System.out.println("良好或优秀");
}如果目标是二选一,就应明确 80 属于哪边,例如使用半开区间:
if (score >= 60 && score < 80) {
System.out.println("合格");
} else if (score >= 80 && score <= 100) {
System.out.println("良好或优秀");
}对整数阈值 60,至少测试 59、60、61;对 90,测试 89、90、91。对总体范围,还要测试 -1、0、100、101。这组输入比随手挑 75、86 更容易暴露 < 与 <= 的错误。
当方法出现多层嵌套,可以先处理失败条件并退出,让正常路径保持平直。下面是类体中的完整静态方法:
static int calculateDiscount(int amount, boolean member) {
if (amount < 0) {
return 0;
}
if (!member) {
return 0;
}
if (amount >= 500) {
return 80;
}
return 30;
}前两个 if 是守卫:金额非法或不是会员时,方法立即结束。剩下的代码可以在“金额合法且是会员”的前提下阅读。
先写清输入域,包括合法范围和可能的缺失状态。没有输入域,后续的“全部情况”就无从判断。
再列互斥结果,并为每个结果写出精确条件。逐对检查是否可能同时为真。
检查覆盖:是否存在合法输入不属于任何结果。必要时用最终 else 承接剩余正常情况,或明确报出非法输入。
最后围绕每个阈值选择边界点,验证阈值本身以及紧邻两侧的输入。
== 看起来像一个通用的“相等”符号,但它对基本类型与引用类型回答的是不同问题。浮点数还带有近似表示和特殊值,不能把所有比较都当成整数比较。
对于 String 引用,== 比较两侧是否指向同一个对象;equals 比较字符序列是否相同:
String input = new String("VIP");
System.out.println(input == "VIP"); // 通常为 false:不是同一对象
System.out.println(input.equals("VIP")); // true:内容相同若变量可能为 null,把已知非空常量放在左边:
String level = null;
if ("VIP".equals(level)) {
System.out.println("使用会员价");
}当两侧都可能为空,并且两个 null 应被视为相等,可使用 Objects.equals:
import java.util.Objects;
public class SafeStringCompare {
public static void main(String[] args) {
String expected = null;
String actual = null;
System.out.println(Objects.equals(expected, actual)); // true
}
}equalsIgnoreCase 适合不区分大小写的简单标识比较,但它不处理所有地区化排序规则。若输入是有限业务状态,更稳的做法通常是先校验并转换成 enum,而不是让任意字符串一路流进决策代码。
二进制浮点不能精确表示很多十进制小数:
double total = 0.1 + 0.2;
System.out.println(total == 0.3); // false测量值是否达到目标,应先定义业务容差:
double measured = 9.9997;
double target = 10.0;
double tolerance = 0.001;
boolean closeEnough = Math.abs(measured - target) <= tolerance;容差不是固定魔法数字。温度允许误差 0.1 摄氏度,与芯片尺寸允许误差 0.000001 米,显然不是同一个规则。跨度很大的科学计算还要结合相对误差;当前阶段至少要做到:由业务或测量精度决定容差,不要为了让测试通过随手写一个值。
货币规则更适合用整数最小单位,例如用“分”保存金额,或者在需要十进制定点和舍入策略时使用 BigDecimal,不应靠一个随意容差决定两笔钱是否相等。
Double.NaN 表示“非数值”。任何 NaN 与数值做 == 都是 false,NaN 与自身做 == 也为 false;相应的 != 为 true。检查它应使用:
double result = Math.sqrt(-1);
if (Double.isNaN(result)) {
System.out.println("结果不是有效实数");
}0.0 == -0.0 的结果为 true。这提醒我们:在写条件前,先问业务需要的是引用相同、内容相同、数值精确相同,还是落在允许误差内。
不要用 text == "OK" 判断字符串内容,也不要把 measured == target 当成测量值到达目标的通用规则。这两种写法有时碰巧得到预期结果,反而更容易把问题带进正式代码。
条件运算符 condition ? valueWhenTrue : valueWhenFalse 是表达式,会产生一个值。它只求值两个候选表达式中的一个:
boolean member = true;
int price = member ? 80 : 100;
System.out.println("应付:" + price + " 元");它与下面的 if-else 表达同一个二选一结果:
int price;
if (member) {
price = 80;
} else {
price = 100;
}适合使用 ?: 的情况有三个特征:条件短、两个候选值短、结果立即赋给变量或传给方法。比如购物车摘要:
int itemCount = 1;
String summary = itemCount == 0 ? "购物车为空" : itemCount + " 件商品";
System.out.println(summary);两个候选结果应当有真实的信息差。如果两边最终得到相同文本或相同数值,直接使用那个结果更清楚。另一个常见例子是消息角标:
int unread = 3;
String badge = unread == 0 ? "无新消息" : unread + " 条新消息";当每条路径要执行多步、记录日志或处理错误时,块结构更清楚:
if (paymentSucceeded) {
status = "PAID";
System.out.println("记录支付成功");
} else {
status = "PENDING";
System.out.println("等待再次支付");
}嵌套条件运算符虽然合法,却很快变成难读的右结合表达式。两个以上业务类别优先使用 else-if 或 switch。
?: 的第二、第三部分必须能形成一个合法结果类型。先让两个候选值类型一致,能避开不必要的数值提升、装箱和目标类型推导问题。
当决策依据是“一个值等于哪个固定常量”时,switch 往往比很长的相等比较链更直观。传统冒号形式的重点不在跳到哪个 case,而在跳入以后何时停下。
标准的经典选择器包括 byte、short、int、char,它们的包装类型,String 和枚举类型。boolean、long、float、double 不能作为这种 switch 的选择器。
String channel = "EXPRESS";
int fee;
switch (channel) {
case "PICKUP":
fee = 0;
break;
case "STANDARD":
fee = 8;
break;
case "EXPRESS":
fee = 18;
break;
default:
fee = -
选择器先求值一次,再寻找匹配标签。case 后必须是兼容的常量或枚举常量,不能写运行时变量范围。default 在没有其他标签匹配时执行。
冒号形式允许贯穿(fall-through):进入一个标签后,会继续执行后续语句和标签,直到遇到 break、方法退出或整个 switch 结束。
int level = 2;
switch (level) {
case 1:
System.out.println("基础权限");
case 2:
System.out.println("编辑权限");
case 3:
System.out.println("管理权限");
}输出是“编辑权限”和“管理权限”,因为从 case 2 进入后没有停止。这可能是刻意设计的累积权限,也可能是遗漏 break 的错误。若确实需要贯穿,应加注释并让路径短小;一般映射优先使用箭头形式。
多个常量执行同一动作时,可以把标签并列,故意利用“空分支”汇合:
char command = 'Y';
switch (command) {
case 'y':
case 'Y':
System.out.println("确认");
break;
case 'n':
case 'N':
System.out.println("取消");
break;
default:
System.out.println("无法识别");
普通的 String 或枚举选择器若为 null,且没有显式的 case null 处理,执行 switch 会抛出 NullPointerException。default 不会自动接住 null。进入 switch 前做空值守卫,或在现代形式中明确写出 case null,两者都比假设 default 会兜底可靠。
现代 switch 有两项很实用的改进:箭头规则不会贯穿;switch 可以作为表达式直接产生值。先看同一个运费映射:
String channel = "EXPRESS";
int fee = switch (channel) {
case "PICKUP" -> 0;
case "STANDARD" -> 8;
case "EXPRESS" -> 18;
default -> -1;
};这里整个 switch 是赋值号右侧的表达式,所以右大括号后还有一个分号。每个箭头右侧是该分支的结果,不写 break,也不会落入下一分支。
箭头右侧需要先做计算再给出结果时,使用块,并用 yield 交出这个分支的值:
int orderAmount = 260;
String channel = "EXPRESS";
int fee = switch (channel) {
case "PICKUP" -> 0;
case "STANDARD" -> orderAmount >= 199 ? 0 : 8;
case "EXPRESS" -> {
int baseFee = 18;
int discount =
yield 结束当前 switch 表达式分支并提供值;它不是从整个方法返回。箭头块不能沿正常路径走到末尾却不给值。
switch 表达式一定要对每个可能输入产生结果。String 的可能值无限,通常需要 default。枚举的常量集合有限,列出全部常量后可以不写 default:
enum OrderState {
CREATED, PAID, SHIPPED, CANCELLED
}
public class OrderStateLabel {
public static void main(String[] args) {
OrderState state = OrderState.PAID;
String label = switch (state) {
case CREATED -> "待支付";
case PAID -> "待发货"
省略 default 反而有一个优点:以后给枚举增加新常量,重新编译时会提醒这里尚未处理新状态。若写了宽泛 default,新状态可能被悄悄吞进兜底路径。
现代 switch 可以显式处理 null:
String status = null;
String label = switch (status) {
case null -> "状态缺失";
case "PAID" -> "已支付";
case "SHIPPED" -> "已发货";
default -> "未知状态";
};从 Java 21 起,switch 还能用类型模式判断任意引用类型,并用 when 添加守卫。只需先知道边界:一旦使用模式或 case null,它属于增强 switch,分支必须穷尽;类型模式还要按从具体到一般排列,避免前面的宽模式遮住后面的窄模式。
Object value = "Java";
String description = switch (value) {
case null -> "空值";
case String text when text.isBlank() -> "空白字符串";
case String text -> "字符串,长度为 " + text.length();
case Integer number -> "整数 " + number;
default -> "其他对象";
};当前重点仍是常量分支。模式匹配解决的是“这个对象属于哪种形状”,不要为了追求新语法,把一个简单枚举映射改成复杂模式树。
固定值到固定结果的映射,优先考虑箭头 switch 表达式;范围、多个字段组合、带复杂优先级的规则,通常仍由 if/else-if 表达得更自然。
局部变量声明后不会像字段那样自动得到默认值。读取局部变量前,编译器必须能沿控制流证明:所有能够到达读取点的路径都已经给它赋值。这叫确定赋值。
int temperature = 28;
String advice;
if (temperature >= 30) {
advice = "注意补水";
} else {
advice = "温度尚可";
}
System.out.println(advice); // 可以编译if-else 无论选择哪边都会赋值,因此读取点安全。改成只有 if 就不成立:
int temperature = 28;
String advice;
if (temperature >= 30) {
advice = "注意补水";
}
// 编译错误:条件为 false 的路径没有给 advice 赋值
// System.out.println(advice);
确定赋值检查关注每条可达路径:只要有一条路径在读取 label 前没有完成赋值,编译器就不会允许读取。
下面两个独立 if 从数学上似乎覆盖了所有整数,但编译器不会把它们当成一个保证二选一的整体:
int x = 5;
String sign;
if (x >= 0) {
sign = "非负";
}
if (x < 0) {
sign = "负数";
}
// 编译错误:编译器无法据此证明所有路径都已赋值
// System.out.println(sign);把互斥关系直接写成 if-else,既表达意图,也让路径证明成立:
String sign;
if (x >= 0) {
sign = "非负";
} else {
sign = "负数";
}
System.out.println(sign);更紧凑的办法是让表达式直接产生值:
String sign = x >= 0 ? "非负" : "负数";或者对固定状态使用穷尽的 switch 表达式。因为表达式本身必须给出结果,不容易留下“声明了但某条路径没赋值”的缺口。
守卫子句会结束方法,后面的代码只可能来自通过守卫的路径:
static String shippingLabel(int fee) {
if (fee < 0) {
return "运费无效";
}
String label;
if (fee == 0) {
label = "免运费";
} else {
label = fee + " 元";
}
return label;
}理解确定赋值时,不要只数赋值语句。应画出所有能到达读取点的箭头,逐条检查变量是否已经得到值。
给变量随手填一个空字符串或 0,确实可能让编译错误消失,但也可能把“遗漏分支”伪装成合法默认值。只有业务真的定义了默认结果时才初始化默认值;否则应补齐路径。
决策代码是否可靠,要看所有有意义的路径,而不是看一个示例刚好输出正确。可以把开发过程压缩成一条闭环:规则明确化、决策表、代码、路径测试、定位第一处偏差。
以配送费为例,模糊需求是“会员和大额订单有优惠”。它无法直接编码,因为优惠条件、优先级和异常输入都不清楚。把它细化为:
再写伪代码:
如果金额非法:结果为输入无效
否则如果是自提:运费为 0
否则如果金额达到 199:运费为 0
否则如果是会员:运费为 5
否则:运费为 8这时才翻译成 if-else-if。伪代码的价值是先暴露规则顺序,不让括号、分号和变量名打断业务推理。
针对上面的规则,至少准备这些输入:
如果两个条件可能同时为真,还要专门测试交叉点。例如“自提且满 199”能验证优先级是否符合设计。
发现结果错误后,在决策前设置断点,依次观察原始输入和每个子条件:
boolean validAmount = amount >= 0;
boolean freeByPickup = pickup;
boolean freeByAmount = amount >= 199;
boolean discounted = member;然后单步执行,记录程序实际进入的第一个分支。我们要找的是“状态第一次与预期不同”的位置:
临时输出也能做同样的事,但要标明来源并在修复后移除:
System.out.printf(
"amount=%d, pickup=%b, member=%b, freeByAmount=%b%n",
amount, pickup, member, freeByAmount);遇到多个编译错误时,先修最前面的一个再重新编译。缺少大括号或分号可能让后面的诊断位置全部偏移。遇到语义错误时,不要反复猜改;固定一个能复现的输入,逐步跟踪控制流,通常更快。
调试的终点不只是“这个输入好了”,还要把导致错误的输入加入回归测试。这样以后调整分支顺序或阈值时,旧问题不会悄悄回来。
先完成快速检查,再尝试四个完整任务。建议先画决策表和控制流路径,最后才写 Java 代码。
某展馆的练习规则如下:年龄必须在 0 到 120 之间;0 到未满 6 岁免费;6 到未满 18 岁 20 元;18 到未满 65 岁 35 元;65 岁及以上 18 元。请写一个完整程序,根据给定年龄输出票价或“年龄无效”。
给定 TicketType { OUTAGE, SECURITY, BILLING, QUESTION },把它们依次映射为优先级 1、1、2、3。要求多个同优先级常量写在同一个箭头标签中,不使用 default。
下面的代码想在用户名非空且角色为 ADMIN 时开放管理台,并把温度测量值在目标值上下 0.05 以内视为稳定。请找出所有问题:
String userName = null;
String role = new String("ADMIN");
double measured = 20.0 - 0.1 - 0.1;
double target = 19.8;
if (!userName.isBlank() && userName != null && role == "ADMIN") {
System.out.println(
判断下面哪一段可以在末尾打印 label,并说明原因:
// 方案 A
String label;
if (score >= 60) {
label = "通过";
}
if (score < 60) {
label = "未通过";
}
// System.out.println(label);// 方案 B
String label;
if (score >= 60) {
label = "通过";
} else {
label = "未通过";
}
System.out.println(label);至少测试 -1、0、5、6、17、18、64、65、120、121。这些输入覆盖每个阈值的两侧和总体合法范围。
四个枚举常量全部出现,表达式已经穷尽。以后新增常量时,这里会要求补充分支。
这里 userName 为 null,第一个条件立即为假,后两项不会执行。温度比较的容差来自题目规则,而不是为了迁就某个计算结果临时猜出的数。