主题
界面
主题
有了数据类型,接下来要让程序会判断和重复。本节讲运算符与流程控制,是写任何有逻辑的程序的基础。
重点不在背语法,而在三个容易出错的地方:短路求值的行为、switch 的演进(Java 14+ 的表达式写法)、以及循环的边界与死循环。
完成本节后,你应该能够:
i++ 与 ++i 的区别,并预测含自增的表达式结果;&& 与 &)的差别及其实用价值;switch 表达式(Java 14+)替代多重 if-else;for / while / do-while 之间选择;break / continue / 标签控制嵌套循环。三个方面最常出错:
第一,短路求值决定了代码会不会崩。 写 if (list != null && list.size() > 0) 是安全的,写成 & 就会在 list 为 null 时抛异常。理解这一点能避免大量 NPE。
第二,循环的边界是"差一错误"(off-by-one)的高发区。 i <= length 与 i < length 差一个字符,结果可能是越界异常或漏掉最后一个元素。
第三,switch 在 Java 14 之后有了更安全的写法。 旧写法的"忘记 break 会穿透"是经典事故,新写法从语法上消除它。
| 类别 | 运算符 | 说明 |
|---|---|---|
| 算术 | + - * / % | / 是整数除法(7 / 2 = 3),% 取余 |
| 关系 | > < >= <= == != | 结果是 boolean |
| 逻辑 | && || ! | 短路求值 |
| 位 | & | ^ ~ << >> >>> | 按二进制位运算(HashMap 的下标计算会用到) |
| 三元 | 条件 ? 值1 : 值2 | 简单二选一 |
| 赋值 | = += -= *= /= %= | 复合赋值隐含类型转换 |
整数除法的坑:
System.out.println(7 / 2); // 3 不是 3.5
System.out.println(7 / 2.0); // 3.5 有一个是小数才会得到小数
System.out.println(7 % 2); // 1int a = 1;
int b = a++; // 先取值再自增:b = 1,a 变成 2
int c = ++a; // 先自增再取值:a 变成 3,c = 3记忆法:符号在前,先自增;符号在后,后自增。
实用建议:不要在同一个表达式里多次使用自增(如 i++ + i++),结果依赖求值顺序,可读性极差。分成两行写。
false && sideEffect() // 短路:右边不执行
false & sideEffect() // 非短路:右边一定执行| 运算符 | 行为 | 何时用 |
|---|---|---|
&& / || | 左边能决定结果就不算右边 | 默认都用它 |
& / | | 两边都算 | 位运算,或确实需要"两边都执行" |
典型价值:
if (user != null && user.getName().startsWith("A")) { ... }
// user 为 null 时,右边不会执行,因此不会 NPE如果把 && 写成 &,这段代码在 user 为 null 时直接抛异常。
if-else 链适合范围判断:
if (score >= 90) {
level = "优秀";
} else if (score >= 75) {
level = "良好";
} else if (score >= 60) {
level = "及格";
} else {
level = "不及格";
}switch 表达式(Java 14+)适合离散值匹配,且可以返回值:
String level = switch (score / 10) {
case 10, 9 -> "优秀";
case 8, 7 -> "良好";
case 6 -> "及格";
default -> "不及格";
};对比旧写法,新写法有三个好处:不需要 break(不会穿透)、可以赋值给变量、分支多时更清晰。
switch 表达式需要 Java 14+
本知识库基线是 Java 21,可以放心使用。如果你在维护 Java 8 的老项目,就要用传统的 switch + break 写法,并特别注意每个分支都要能正确结束。
| 循环 | 适用场景 | 例子 |
|---|---|---|
for | 次数已知 | 遍历数组、跑 100 次 |
while | 次数未知,条件满足就继续 | 读文件直到读完 |
do-while | 至少执行一次 | 用户输入校验(先问一次再判断) |
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 = 1do-while 的特殊之处:条件在末尾判断,所以循环体至少执行一次——这不是语法细节,而是它的语义价值。
for (int i = 1; i <= 8; i++) {
if (i % 2 == 0) {
continue; // 跳过本次,继续下一次
}
if (i == 7) {
break; // 结束整个循环
}
System.out.print(i + " ");
}
// 输出:1 3 5嵌套循环里 break 只跳出最内层。要跳出多层可以用标签:
outer:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i + j == 3) {
break outer; // 直接跳出外层循环
}
}
}标签可读性一般,能用方法内的 return 表达时优先用 return。
int a = 1;
System.out.println("a=" + a + " b=" + (a++) + " c=" + (++a)); // a 最终是 3
System.out.println("短路: " + (false && sideEffect())); // sideEffect 不执行static String grade(int score) {
return switch (score / 10) {
case 10, 9 -> "优秀";
case 8, 7 -> "良好";
case 6 -> "及格";
default -> "不及格";
};
}switch 表达式把"分数 → 等级"的映射写得像一张表,比 if-else 链更易读、也不容易漏分支(编译器会提示 default 缺失的枚举场景)。
// 事故一:字符串用 == 比较引用地址
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 语句维护。
文件:labs/java-syntax/src/main/java/cn/kb/learn/syntax/ControlFlow.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();
}
}运行命令:
cd labs
./mvnw -pl java-syntax compile
java -cp java-syntax/target/classes cn.kb.learn.syntax.ControlFlow预期输出:
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,而 & 调用了(打印了那行提示)。
int b = a++; 与 int c = ++a; 各下一个断点,逐步执行,观察 a 在两行之间的变化顺序;false && sideEffect() 改成 true || sideEffect(),验证短路同样生效(右边不执行);for (int i = 1; i <= max; i++) 下条件断点 i == 4,观察循环变量在每次迭代后的值;stopAt 的 break 改成 continue,观察输出差异(从"1 2 3"变成"1 2 3 5 6 7 8")——这是理解两者区别的最快方式。switch 忘记 break(旧写法) switch (day) {
case 1:
System.out.println("周一");
case 2: // 缺少 break,会继续执行这里
System.out.println("周二");
break;
}
// 输入 1 时会把两行都打印出来修复:改用 switch 表达式(-> 写法不会穿透),或在每个分支末尾加 break。
== 比较字符串 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)。
int[] data = {1, 2, 3};
for (int i = 0; i <= data.length; i++) { // <= 会越界
System.out.println(data[i]);
}记忆法:数组与集合用 < length;需要包含上界的整数循环用 <=。
int i = 0;
while (i < 10) {
System.out.println(i); // 忘了 i++
}修复:写 while 时先写"让条件变化的语句";IDE 会提示"条件永远为真"。
目标:用循环和分支写一个真实的小工具。
步骤:
String fizzBuzz(int max),返回 1..max 的 FizzBuzz 序列(3 的倍数是 Fizz,5 的倍数是 Buzz,同时是则 FizzBuzz,其他输出数字);switch 表达式改写 grade 方法,支持 score 为负数时返回"无效分数";break 实现"在二维数组里找到目标值就停止搜索";doWhileCount(0) 改成用 while 实现,观察输出差异并解释原因。验收方式:能解释第 5 步"用 while 实现会输出 0"的原因(条件在开头判断)。
扩展任务:用 while + Scanner 写一个"猜数字"程序:程序随机生成 1-100 的数,用户输入猜测,提示"大了/小了",猜中后输出次数。(提示:new Random().nextInt(100) + 1)
基础题
&& 与 & 在任何情况下结果都相同。(对 / 错)do-while 的特点是循环体至少执行 ______ 次。switch 表达式相比传统 switch 语句有哪两个好处?代码阅读题
int i = 0;
System.out.println(i++ + ++i);
System.out.println(i);编程题
int countDigits(String text),统计字符串中数字字符的个数。要求:text 为 null 时返回 0。综合任务
{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 题的"出现次数最多"需要双重循环(对每个数字再遍历一遍统计次数),或用两个数组分别记录值和次数。
&& 会短路(左边为假时不执行右边),& 两边都执行。只有当右边没有副作用且条件判断不依赖 null 检查时,结果才恰好相同。break);② 可以作为表达式使用,直接把结果赋给变量或 return。输出:
2
2过程(从左到右):
i++:先取当前值 0,然后 i 变成 1;++i:先把 i 从 1 变成 2,再取值 2;0 + 2 = 2,打印 2;i 是 2,第二行打印 2。这也说明为什么不要在同一个表达式里混用多次自增——虽然结果是确定的,但可读性很差。
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));
}
}预期输出:
3
0
0package 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。
预期输出:
数据: [12, 7, 9, 100, 7, 3]
总和: 138
平均值: 23.0
最大值: 100
最小值: 3
出现最多: 7(2 次)switch 表达式替代多重 if-else;switch 表达式正式版说明《数组》:把多个同类型数据组织在一起,开始接触引用类型的内存行为。