1974 字
约 6 分钟
3
S04-J07 · Java 语法速通:从变量到递归的 24 个坑

适用读者:有别的语言基础、要快速过一遍 Java 语法的人;或复习时总在细节上翻车的人。
前置:S01-B02(环境)、B01(Java 定位)。
覆盖旧文:2702–2721(零基础教程第 1–20 章)+ 2947–2965(同题重复导出)+ 4427(java 语句的技巧)→ 40 篇 / 约 42 万字符。
扩展点:E-S04-021(语法速通的三层结构:语法 → 语义 → 陷阱)、E-S04-022(类型转换的六条规则与丢失精度)、E-S04-023(控制流的边界条件清单)。
下一篇:S04-J08 · 面向对象六讲。


1. 一句话结论

Java 语法本身不难,难的是"默认值 + 隐式转换 + 求值顺序"这三类沉默行为。 它们不报错,只在边界上给出错误结果——这就是"语法都会,代码还是错"的根源。

出处:A-2702–A-2721(教程第 1–20 章)、A-2947–A-2965(同题重复)、A-4427(语句技巧)。


2. 三层结构(E-S04-021)

第一层 语法:怎么写(声明、运算符、控制流)
第二层 语义:为什么这样写(求值顺序、作用域、类型系统)
第三层 陷阱:什么时候会错(默认值、隐式转换、短路、整数溢出)

判据:只看第一层的人写的代码能编译,但过不了边界用例。


3. 类型系统:四个必答点(E-S04-022)

3.1 八种基本类型与默认值

类型 位宽 默认值(仅字段) 局部变量
byte/short/int/long 8/16/32/64 0 必须显式初始化
float/double 32/64 0.0f / 0.0d 同上
char 16(UTF-16 单元) \u0000 同上
boolean JVM 相关 false 同上

坑 1(最常见):"局部变量有默认值"是错的——字段有默认值,局部变量必须先赋值,否则编译失败。

3.2 隐式转换与丢失精度

int a = 1_000_000_000;
long b = a * 3;        // ❌ 先按 int 计算溢出(-1294967296),再赋给 long
long c = a * 3L;       // ✅ 提升为 long 运算

byte d = (byte) 200;   // ❌ 200 超出 byte → -56(截断,不报错)
float f = 0.1f + 0.2f; // ❌ 0.30000001(二进制无法精确表示 0.1)
if (0.1 + 0.2 == 0.3)  // ❌ false —— 浮点比较要用误差阈值或 BigDecimal

六条转换规则:小 → 大自动(无丢失);大 → 小需强制转换(截断高位);整型 → 浮点可能丢精度;char 与 int 互通;boolean 不参与;装箱/拆箱在 == 与 null 上有坑(见 S04-B02)。

3.3 == / equals / hashCode 的位置

比较对象 用 == 用 equals
基本类型 ✅ 比值 不存在
引用(对象身份) ✅ 比地址 ✅ 比内容(需重写)
包装类型 ⚠️ 缓存区间内为 true(Integer −128~127) ✅
String ⚠️ 常量池内可能为 true ✅

坑 2:Integer a = 127, b = 127; a==b 为 true;128 为 false——这不是 bug,是 Integer.valueOf 缓存。

3.4 整数溢出与位运算

int max = Integer.MAX_VALUE;
max + 1 == Integer.MIN_VALUE   // ❌ true(静默回绕)
Math.addExact(max, 1)          // ✅ 抛 ArithmeticException
-1 >>> 28                      // 无符号右移:15(不是 -1 的符号位扩展)

坑 3:Math.abs(Integer.MIN_VALUE) 仍是负数——边界值不能假设绝对值有效。


4. 控制流:四条边界清单(E-S04-023)

4.1 if / else 与 switch

// 坑 4:switch 漏写 break → 贯穿(fall-through),且不会报错
switch (day) {
    case 1: System.out.println("周一");   // ❌ 漏 break,继续执行 case 2
    case 2: System.out.println("周二"); break;
}
// Java 14+ 箭头语法无贯穿
switch (day) { case 1 -> System.out.println("周一"); default -> {} }

switch 可用类型:byte/short/int/char、其包装类、String(Java 7+)、枚举、int 兼容常量。long 与 boolean 不支持(long 需转 int 或用 if)。

4.2 循环四件的边界

结构 边界要点
for 三段都可空(for(;;) = 死循环);循环变量作用域仅在循环内
while / do-while do-while 至少执行一次
for-each 底层迭代器;遍历中删除元素会 ConcurrentModificationException(见 S04-J01)
break / continue 带标签可跳出多层(outer:)

坑 5:for-each 里 list.remove(x) 抛 ConcurrentModificationException,而 removeIf / 迭代器 remove 才安全。

4.3 短路与求值顺序

if (obj != null && obj.getX() > 0)   // ✅ 短路保护
int i = 0; int[] a = new int[3];
a[i] = i++;    // ❌ 结果取决于求值顺序:先算左边索引 0,再自增 → a[0]=0

Java 求值顺序:从左到右(JLS §15.7)。a[i] = i++ 中 a[i] 的 i 先求值为 0,右侧 i++ 返回 0 并自增——所以 a[0]=0、i=1。但可读性极差,工程规约上直接禁止。

4.4 输入输出与 Scanner

Scanner sc = new Scanner(System.in);
int n = sc.nextInt();          // 坑 6:混用 nextInt 与 nextLine → 残留换行被读走
sc.nextLine();                 // 需额外消费掉换行

判据:Scanner 混用 nextX 与 nextLine 是新手最常踩的 IO 坑;生产代码用 BufferedReader + readLine 更稳。


5. 数组与递归

5.1 数组的四个事实

① 长度固定;length 是字段不是方法
② 下标从 0 开始,越界抛 ArrayIndexOutOfBoundsException(运行时)
③ 数组是对象,引用可 null;默认元素值 0/false/null
④ 多维是"数组的数组"(锯齿可不等长)

坑 7:int[2][3] 的 length 是 2(第一维),[i].length 才是 3。

int[][] g = new int[3][];      // 只分配第一维
g[0] = new int[]{1,2,3};       // 第二维逐个分配(锯齿合法)
int[][] h = new int[3][4];     // 一次性分配(矩形)

5.2 递归的两个必答项

int fib(int n) { return n < 2 ? n : fib(n-1) + fib(n-2); }   // ❌ O(2^n) 重复计算
static int fact(int n) { return n <= 1 ? 1 : n * fact(n - 1); } // 深度 = n
问题 答法
栈溢出边界 默认栈 512 KB~1 MB,深度约 1 万层;深递归改迭代或加大 -Xss
尾递归 Java 不做尾调用优化(HotSpot 明确不优化),别照搬函数式语言的结论

6. 补充:main、注释与包

public static void main(String[] args)   // 必须是 public static void + String[]
项 要点
注释 //、/* */、/** */(Javadoc,可被 javadoc 提取)
包 目录结构应与包名一致;package 首行;import 不递归
命名 类 UpperCamel、方法与变量 lowerCamel、常量 UPPER_SNAKE
final 变量不可重新赋值;引用不可变不等于对象不可变

7. 反例:六个"编译通过但行为错"的片段

片段 结果 原因
long b = a * 3;(a 为 int) 溢出后再提升 运算按最大操作数类型
if (0.1+0.2 == 0.3) false 二进制浮点
switch 漏 break 贯穿执行 fall-through 是语言特性
for-each 中 remove 抛 CME 迭代器 modCount 校验
int[][] m = new int[2][3]; m.length 2 多维是数组的数组
Scanner.nextInt() 后 nextLine() 读到空串 换行残留

8. 自测

  1. 局部变量有默认值吗?哪类变量有?
  2. long b = a * 3;(a 是 int)为什么可能错?正确写法?
  3. switch 支持哪些类型?long 可以吗?
  4. for-each 中删除元素为什么抛异常?安全写法?
  5. Java 做尾递归优化吗?
答案
  1. 没有;只有字段/数组元素有默认值。
  2. 先按 int 运算再提升,溢出;写 a * 3L。
  3. 整型(含包装)、char、String、枚举;long 不支持。
  4. 迭代器 modCount 与列表修改计数不一致;用 removeIf 或迭代器 remove。
  5. 不做(HotSpot 无 TCO)。

9. 下一篇

S04-J08 · 面向对象六讲 —— 类与对象、构造与初始化顺序、this/super、访问修饰符、重写与重载、多态与 final/instanceof、抽象类与接口、内部类。

S04-J07 · Java 语法速通:从变量到递归的 24 个坑
https://www.clxhxhhr.top/posts/5230/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。