Skip to content

运算符与流程控制 ​

S1-002必学入门约 90 分钟JDK 21编程语言基础 · 语法基础

1. 本节定位 ​

有了数据类型,接下来要让程序会判断和重复。本节讲运算符与流程控制,是写任何有逻辑的程序的基础。

重点不在背语法,而在三个容易出错的地方:短路求值的行为、switch 的演进(Java 14+ 的表达式写法)、以及循环的边界与死循环。

2. 学习目标 ​

完成本节后,你应该能够:

  • 分清算术、关系、逻辑、位、三元、赋值运算符的用途;
  • 说清 i++ 与 ++i 的区别,并预测含自增的表达式结果;
  • 解释短路求值(&& 与 &)的差别及其实用价值;
  • 用 switch 表达式(Java 14+)替代多重 if-else;
  • 根据场景在 for / while / do-while 之间选择;
  • 用 break / continue / 标签控制嵌套循环。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

三个方面最常出错:

第一,短路求值决定了代码会不会崩。 写 if (list != null && list.size() > 0) 是安全的,写成 & 就会在 list 为 null 时抛异常。理解这一点能避免大量 NPE。

第二,循环的边界是"差一错误"(off-by-one)的高发区。 i <= length 与 i < length 差一个字符,结果可能是越界异常或漏掉最后一个元素。

第三,switch 在 Java 14 之后有了更安全的写法。 旧写法的"忘记 break 会穿透"是经典事故,新写法从语法上消除它。

5. 核心概念 ​

5.1 运算符分类 ​

类别运算符说明
算术+ - * / %/ 是整数除法(7 / 2 = 3),% 取余
关系> < >= <= == !=结果是 boolean
逻辑&& || !短路求值
位& | ^ ~ << >> >>>按二进制位运算(HashMap 的下标计算会用到)
三元条件 ? 值1 : 值2简单二选一
赋值= += -= *= /= %=复合赋值隐含类型转换

整数除法的坑:

java
System.out.println(7 / 2);       // 3   不是 3.5
System.out.println(7 / 2.0);     // 3.5 有一个是小数才会得到小数
System.out.println(7 % 2);       // 1

5.2 自增前后缀 ​

java
int a = 1;
int b = a++;      // 先取值再自增:b = 1,a 变成 2
int c = ++a;      // 先自增再取值:a 变成 3,c = 3

记忆法:符号在前,先自增;符号在后,后自增。

实用建议:不要在同一个表达式里多次使用自增(如 i++ + i++),结果依赖求值顺序,可读性极差。分成两行写。

5.3 短路求值(本节最重要的一条) ​

java
false && sideEffect()   // 短路:右边不执行
false & sideEffect()    // 非短路:右边一定执行
运算符行为何时用
&& / ||左边能决定结果就不算右边默认都用它
& / |两边都算位运算,或确实需要"两边都执行"

典型价值:

java
if (user != null && user.getName().startsWith("A")) { ... }
// user 为 null 时,右边不会执行,因此不会 NPE

如果把 && 写成 &,这段代码在 user 为 null 时直接抛异常。

5.4 if 与 switch ​

if-else 链适合范围判断:

java
if (score >= 90) {
    level = "优秀";
} else if (score >= 75) {
    level = "良好";
} else if (score >= 60) {
    level = "及格";
} else {
    level = "不及格";
}

switch 表达式(Java 14+)适合离散值匹配,且可以返回值:

java
String level = switch (score / 10) {
    case 10, 9 -> "优秀";
    case 8, 7 -> "良好";
    case 6 -> "及格";
    default -> "不及格";
};

对比旧写法,新写法有三个好处:不需要 break(不会穿透)、可以赋值给变量、分支多时更清晰。

switch 表达式需要 Java 14+

本知识库基线是 Java 21,可以放心使用。如果你在维护 Java 8 的老项目,就要用传统的 switch + break 写法,并特别注意每个分支都要能正确结束。

5.5 三种循环怎么选 ​

循环适用场景例子
for次数已知遍历数组、跑 100 次
while次数未知,条件满足就继续读文件直到读完
do-while至少执行一次用户输入校验(先问一次再判断)
java
for (int i = 0; i < 5; i++) { }        // 0,1,2,3,4
int i = 0;
while (i < 5) { i++; }                 // 同上,但更啰嗦
int count = 0;
do { count++; } while (false);          // 执行一次,count = 1

do-while 的特殊之处:条件在末尾判断,所以循环体至少执行一次——这不是语法细节,而是它的语义价值。

5.6 break、continue 与标签 ​

java
for (int i = 1; i <= 8; i++) {
    if (i % 2 == 0) {
        continue;          // 跳过本次,继续下一次
    }
    if (i == 7) {
        break;             // 结束整个循环
    }
    System.out.print(i + " ");
}
// 输出:1 3 5

嵌套循环里 break 只跳出最内层。要跳出多层可以用标签:

java
outer:
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        if (i + j == 3) {
            break outer;       // 直接跳出外层循环
        }
    }
}

标签可读性一般,能用方法内的 return 表达时优先用 return。

6. 递进示例 ​

6.1 最小示例:自增与短路 ​

java
int a = 1;
System.out.println("a=" + a + " b=" + (a++) + " c=" + (++a));   // a 最终是 3
System.out.println("短路: " + (false && sideEffect()));          // sideEffect 不执行

6.2 业务示例:成绩等级 + 循环聚合 ​

java
static String grade(int score) {
    return switch (score / 10) {
        case 10, 9 -> "优秀";
        case 8, 7 -> "良好";
        case 6 -> "及格";
        default -> "不及格";
    };
}

switch 表达式把"分数 → 等级"的映射写得像一张表,比 if-else 链更易读、也不容易漏分支(编译器会提示 default 缺失的枚举场景)。

6.3 反例:三个高频事故 ​

java
// 事故一:字符串用 == 比较引用地址
String a = new String("java");
String b = new String("java");
if (a == b) { }              // false!应该用 a.equals(b)

// 事故二:浮点数控制循环次数(误差累积导致多跑或少跑一次)
for (double d = 0.0; d != 1.0; d += 0.1) { }   // 危险写法

// 事故三:循环体内修改循环变量
for (int i = 0; i < 10; i++) {
    i++;                     // 实际只跑 5 次,且意图不明
}

修复原则:字符串比内容用 equals;循环计数用整数;循环变量只由 for 语句维护。

7. 完整可运行代码 ​

文件:labs/java-syntax/src/main/java/cn/kb/learn/syntax/ControlFlow.java

java
package cn.kb.learn.syntax;

public class ControlFlow {
    /** 刻意演示短路求值:&& 右侧是死代码(不会执行) */
    @SuppressWarnings("unused")
    public static void main(String[] args) {
        // 1. 自增的前后缀
        int a = 1;
        int b = a++;
        int c = ++a;
        System.out.println("a=" + a + " b=" + b + " c=" + c);

        // 2. 短路求值:&& 右边不会执行
        System.out.println("短路 && 结果: " + (false && sideEffect()));
        System.out.println("非短路 & 结果: " + (false & sideEffect()));

        // 3. switch 表达式(Java 14+)
        System.out.println("87 分等级: " + grade(87));
        System.out.println("59 分等级: " + grade(59));

        // 4. 循环
        System.out.println("1..5 的平方: " + squares(5));
        System.out.println("do-while 至少执行一次: " + doWhileCount(0));

        // 5. break 与 continue
        System.out.println("跳过偶数: " + skipEven(1, 8));
        System.out.println("遇到 4 就停: " + stopAt(1, 8));
    }

    static boolean sideEffect() {
        System.out.println("        (sideEffect 被调用)");
        return true;
    }

    static String grade(int score) {
        return switch (score / 10) {
            case 10, 9 -> "优秀";
            case 8, 7 -> "良好";
            case 6 -> "及格";
            default -> "不及格";
        };
    }

    static String squares(int max) {
        StringBuilder builder = new StringBuilder();
        for (int i = 1; i <= max; i++) {
            builder.append(i * i).append(' ');
        }
        return builder.toString().trim();
    }

    static int doWhileCount(int value) {
        int count = 0;
        do {
            count++;
        } while (value > 0);
        return count;
    }

    static String skipEven(int from, int to) {
        StringBuilder builder = new StringBuilder();
        for (int i = from; i <= to; i++) {
            if (i % 2 == 0) {
                continue;
            }
            builder.append(i).append(' ');
        }
        return builder.toString().trim();
    }

    static String stopAt(int from, int to) {
        StringBuilder builder = new StringBuilder();
        for (int i = from; i <= to; i++) {
            if (i == 4) {
                break;
            }
            builder.append(i).append(' ');
        }
        return builder.toString().trim();
    }
}

运行命令:

powershell
cd labs
./mvnw -pl java-syntax compile
java -cp java-syntax/target/classes cn.kb.learn.syntax.ControlFlow

预期输出:

text
a=3 b=1 c=3
短路 && 结果: false
        (sideEffect 被调用)
非短路 & 结果: false
87 分等级: 良好
59 分等级: 不及格
1..5 的平方: 1 4 9 16 25
do-while 至少执行一次: 1
跳过偶数: 1 3 5 7
遇到 4 就停: 1 2 3

注意输出的第三行:这就是短路求值的直接证据——&& 没有调用 sideEffect,而 & 调用了(打印了那行提示)。

8. Debug 观察点 ​

  1. 在 int b = a++; 与 int c = ++a; 各下一个断点,逐步执行,观察 a 在两行之间的变化顺序;
  2. 把 false && sideEffect() 改成 true || sideEffect(),验证短路同样生效(右边不执行);
  3. 在 for (int i = 1; i <= max; i++) 下条件断点 i == 4,观察循环变量在每次迭代后的值;
  4. 把 stopAt 的 break 改成 continue,观察输出差异(从"1 2 3"变成"1 2 3 5 6 7 8")——这是理解两者区别的最快方式。

9. 常见错误 ​

错误 1:switch 忘记 break(旧写法) ​

java
switch (day) {
    case 1:
        System.out.println("周一");
    case 2:                          // 缺少 break,会继续执行这里
        System.out.println("周二");
        break;
}
// 输入 1 时会把两行都打印出来

修复:改用 switch 表达式(-> 写法不会穿透),或在每个分支末尾加 break。

错误 2:== 比较字符串 ​

java
String a = new String("abc");
String b = new String("abc");
System.out.println(a == b);          // false
System.out.println(a.equals(b));     // true

修复:内容比较统一用 equals;常量写在前面避免 NPE:"abc".equals(value)。

错误 3:循环边界差一 ​

java
int[] data = {1, 2, 3};
for (int i = 0; i <= data.length; i++) {    // <= 会越界
    System.out.println(data[i]);
}

记忆法:数组与集合用 < length;需要包含上界的整数循环用 <=。

错误 4:死循环 ​

java
int i = 0;
while (i < 10) {
    System.out.println(i);      // 忘了 i++
}

修复:写 while 时先写"让条件变化的语句";IDE 会提示"条件永远为真"。

10. 动手实验 ​

目标:用循环和分支写一个真实的小工具。

步骤:

  1. 运行第 7 节代码,核对输出(尤其是短路那两行);
  2. 写一个方法 String fizzBuzz(int max),返回 1..max 的 FizzBuzz 序列(3 的倍数是 Fizz,5 的倍数是 Buzz,同时是则 FizzBuzz,其他输出数字);
  3. 用 switch 表达式改写 grade 方法,支持 score 为负数时返回"无效分数";
  4. 写一个嵌套循环,用标签 break 实现"在二维数组里找到目标值就停止搜索";
  5. 把 doWhileCount(0) 改成用 while 实现,观察输出差异并解释原因。

验收方式:能解释第 5 步"用 while 实现会输出 0"的原因(条件在开头判断)。

扩展任务:用 while + Scanner 写一个"猜数字"程序:程序随机生成 1-100 的数,用户输入猜测,提示"大了/小了",猜中后输出次数。(提示:new Random().nextInt(100) + 1)

11. 练习题 ​

基础题

  1. 判断:&& 与 & 在任何情况下结果都相同。(对 / 错)
  2. 填空:do-while 的特点是循环体至少执行 ______ 次。
  3. 简答:switch 表达式相比传统 switch 语句有哪两个好处?

代码阅读题

java
int i = 0;
System.out.println(i++ + ++i);
System.out.println(i);
  1. 输出是什么?请逐步说明求值过程。

编程题

  1. 写一个方法 int countDigits(String text),统计字符串中数字字符的个数。要求:text 为 null 时返回 0。

综合任务

  1. 写一个"简易统计器":
    • 输入:一个整数数组(如 {12, 7, 9, 100, 7, 3});
    • 输出:总和、平均值(保留一位小数)、最大值、最小值、出现次数最多的数字;
    • 要求:全程用循环实现,不使用任何集合类;
    • 说明你在哪里用了 break / continue,以及为什么。
分级提示

提示 1(方向):第 4 题按"后缀先取值、前缀先自增"的顺序逐步展开:i++ 取 0,i 变 1;++i 先把 1 变 2 再取值,最终 0 + 2 = 2。

提示 2(关键点):第 5 题判断数字字符用 Character.isDigit(ch),注意先判 null。

提示 3(接近答案):第 6 题的"出现次数最多"需要双重循环(对每个数字再遍历一遍统计次数),或用两个数组分别记录值和次数。

12. 参考答案 ​

基础题
  1. 错。&& 会短路(左边为假时不执行右边),& 两边都执行。只有当右边没有副作用且条件判断不依赖 null 检查时,结果才恰好相同。
  2. 一。
  3. ① 不会穿透(不需要 break);② 可以作为表达式使用,直接把结果赋给变量或 return。
代码阅读题(第 4 题)

输出:

text
2
2

过程(从左到右):

  1. i++:先取当前值 0,然后 i 变成 1;
  2. ++i:先把 i 从 1 变成 2,再取值 2;
  3. 0 + 2 = 2,打印 2;
  4. 此时 i 是 2,第二行打印 2。

这也说明为什么不要在同一个表达式里混用多次自增——虽然结果是确定的,但可读性很差。

编程题(第 5 题)
java
package cn.kb.learn.syntax;

public class DigitCounter {
    static int countDigits(String text) {
        if (text == null) {
            return 0;
        }
        int count = 0;
        for (int i = 0; i < text.length(); i++) {
            if (Character.isDigit(text.charAt(i))) {
                count++;
            }
        }
        return count;
    }

    public static void main(String[] args) {
        System.out.println(countDigits("abc123"));
        System.out.println(countDigits("no digits"));
        System.out.println(countDigits(null));
    }
}

预期输出:

text
3
0
0
综合任务(第 6 题)
java
package cn.kb.learn.syntax;

import java.util.Arrays;

public class Statistics {
    public static void main(String[] args) {
        int[] data = {12, 7, 9, 100, 7, 3};

        int total = 0;
        int max = data[0];
        int min = data[0];
        for (int value : data) {
            total += value;
            if (value > max) {
                max = value;
            }
            if (value < min) {
                min = value;
            }
        }

        // 找出出现次数最多的数字
        int mostFrequent = data[0];
        int mostCount = 0;
        for (int i = 0; i < data.length; i++) {
            int count = 0;
            for (int j = 0; j < data.length; j++) {
                if (data[i] == data[j]) {
                    count++;
                }
            }
            if (count > mostCount) {
                mostCount = count;
                mostFrequent = data[i];
            }
        }

        System.out.println("数据: " + Arrays.toString(data));
        System.out.printf("总和: %d%n", total);
        System.out.printf("平均值: %.1f%n", (double) total / data.length);
        System.out.println("最大值: " + max);
        System.out.println("最小值: " + min);
        System.out.printf("出现最多: %d(%d 次)%n", mostFrequent, mostCount);
    }
}

这里刻意没用到 break / continue:所有循环都是完整遍历。经验法则是——能用完整遍历表达的逻辑就不要提前跳出,因为提前跳出会引入"漏处理"的可能。只有当数据量很大、提前终止能显著提升性能(如有序数组里查找),才值得用 break。

预期输出:

text
数据: [12, 7, 9, 100, 7, 3]
总和: 138
平均值: 23.0
最大值: 100
最小值: 3
出现最多: 7(2 次)

13. 掌握标准 ​

  • [ ] 能预测含自增的简单表达式的输出;
  • [ ] 能解释短路求值,并知道它为什么能避免 NPE;
  • [ ] 会用 switch 表达式替代多重 if-else;
  • [ ] 能按"次数已知 / 未知 / 至少一次"选择循环;
  • [ ] 能识别并修复差一错误与死循环。

14. 扩展资源 ​

15. 下一步学习 ​

《数组》:把多个同类型数据组织在一起,开始接触引用类型的内存行为。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引