第 3 章 · 选择结构程序设计

程序第一次「长出脑子」是在这一章:根据条件走不同的路。if/else 和 switch 是所有语言共享的积木,但 Java 有几个值得专门讲的点:短路求值是工程武器而不只是考点、字符串比较的 == 陷阱(全 Java 最著名的初学者坑)、以及 JDK 14 之后的箭头 switch 为什么值得直接当主力用。这一章篇幅不大,但那两个坑的杀伤力是长尾的,值得慢读。

3.1 短路求值:不只是省事

比较运算符(== != > <)产出 boolean,逻辑运算符组合它们:&& 与、|| 或、! 非。关键性质是短路:

// && 左边为 false,右边不执行;|| 左边为 true,右边不执行
if (arr != null && arr.length > 0) { ... }     // 安全判空的标准写法
if (denom != 0 && a / denom > 2) { ... }       // 先挡住除零

这不是「少算几步」的优化,是依赖顺序的正确性工具:右边依赖左边「活着」的前提(空指针检查、除零检查、越界检查),顺序写反程序就炸。顺便记住和它相对的 &、|(非短路版,两边都算),日常逻辑判断用短路版,位运算才是 & 的本职(第 5 章讲)。

三目运算符 条件 ? a : b 是 if/else 的表达式版,适合「给变量挑值」这种单行场景:

int max = a > b ? a : b;      // 比 if-else 四行清爽

嵌套三目(a ? b : c ? d : e)请直接判死刑,没人想读懂它。

3.2 if/else:大括号纪律

if (score >= 90) {
    grade = 'A';
} else if (score >= 80) {      // else if 是「else 里再套 if」的缩进糖
    grade = 'B';
} else {
    grade = 'C';
}

三条纪律,每条都有事故背书。永远写大括号,哪怕只有一句:Java 没有 Python 的缩进语义,if (x) doA(); doB(); 里 doB() 根本不在 if 里(悬挂语句),加上大括号这类事故消失。条件区间别写重叠:if (x > 80) 后再 if (x > 90),第二个永远进不去(90 的分支被 80 拦截),升序 if-else 要保证区间互斥,从大到小或从小到大一以贯之。else 配对看最近未匹配的 if(悬挂 else 问题),缩进会骗人,大括号不会。

3.3 字符串比较:== 是全 Java 最著名的坑

先看事故现场:

String a = "hello";
String b = new String("hello");
System.out.println(a == b);            // false!看着相等,比出来不等
System.out.println(a.equals(b));       // true,这才是比内容

原理一句话:Java 里 == 比的是引用(门牌号,计组课第 10 章的语言),equals 比的才是内容。a 和 b 是两个不同的 String 对象(两张门牌,里面住着一样的字),== 判 false。更迷惑的是有时 == 又「碰巧」true(字符串字面量常量池复用,第 9 章讲),这种「有时对有时错」的行为让它成为 Java 面试和事故清单的双料冠军。

工程纪律只有一条:比较字符串内容,永远用 equals:

if (input.equals("quit")) { ... }      // 对
if ("quit".equals(input)) { ... }      // 更稳的写法:字面量在前,input 为 null 也不炸

第二种写法(常量在前)是防 null 的技巧:null.equals 会抛 NullPointerException,反过来则安全返回 false。C 语言比字符串要手动 strcmp(第 6 章),Java 的 == 陷阱本质是「长得像值类型,实际是引用」的语言设计代价,这个伏笔到第 8 章(引用语义)正式收线。

3.4 switch:从老古董到现代形态

传统 switch(和 C 同款语义):

switch (day) {
    case 1:
    case 2:
    case 3:
        name = "工作日";
        break;                     // 忘了 break?贯穿(fall-through)到下一个 case!
    default:
        name = "休息日";
}

fall-through 是 C 遗产里的地雷:漏一个 break,逻辑静默串台。JDK 14 起的箭头语法直接判了 break 的死刑,而且 switch 升级成了表达式:

String name = switch (day) {
    case 1, 2, 3, 4, 5 -> "工作日";
    case 6, 7 -> "休息日";
    default -> "未知";
};    // 箭头分支无贯穿、必须穷尽(编译器强制 default 或覆盖全部可能)

新代码一律用箭头版:无贯穿、可当表达式、编译器帮查漏,老代码的三宗罪一次清零。JDK 21 再进一步支持模式匹配(case Integer i -> ...),那是后话。

常见错误

  • 字符串用 == 比较。3.3 的主角,_intelliJ IDEA 会标黄警告,别的 IDE 未必,形成肌肉记忆最可靠。
  • 赋值写进条件。C 的 if (x = 1) 经典 bug,在 Java 里对 int 直接编译错误(boolean 类型独立),但 boolean 变量 if (flag = true) 依然合法,警惕。
  • 忘了大括号的悬挂 if。3.2 的第一条纪律,格式化工具(IDE 的 reformat)能当场暴露。
  • switch 比较 String 用了老式语法且忘 break。直接换箭头版,一劳永逸。

练习

  1. 动手:验证字符串陷阱:分别用字面量赋值(a = "hi"; b = "hi")和 new String 构造,打印 a == b 与 a.equals(b) 的四种组合结果,并猜想字面量那次 == 为 true 的原因(常量池,先猜,第 9 章对答案)。
  2. 实现:BMI 计算器:输入身高体重,输出 BMI 值和分级(低于 18.5 偏瘦 / 低于 24 正常 / 低于 28 超重 / 其余肥胖),用箭头 switch 或 if-else 各写一版,检查区间边界(18.5 到底归谁)。
  3. 手算:短路求值实验:int[] a = null; if (a != null && a.length > 0) 和把条件顺序反过来的版本各会发生什么?写出原因。
  4. 思考:Java 21 的 switch 模式匹配已经接近函数式语言的 match 表达式。结合计组课第 11 章「函数指针表」的跳转表思想,猜猜编译器会把 switch 编译成什么(提示:连续 case 常用跳转表,稀疏 case 用比较链)。