第41章 Java 初学者常见坑大全(按类别整理)

第四十一章 Java 初学者常见坑大全(按类别整理)

“Java 的坑,比你想的多;初学者的泪,比你想的咸。”

本章汇集了 Java 初学者最容易踩的 100+ 个坑,按类别整理,配有完整可运行的代码示例。学完本章,你可以骄傲地说:“这些坑,我都踩过!”


41.1 语法层面的坑(30个)

坑1:分号才是真正的王者

你以为代码写得漂亮就能运行?不好意思,忘掉一个分号,Java 就会让你知道什么叫"一丝不苟"。

1
2
3
4
5
6
7
// 错误示例
public class MissingSemicolon {
    public static void main(String[] args) {
        System.out.println("Hello")
        System.out.println("World")
    }
}
1
2
3
4
5
6
7
// 正确示例
public class CorrectSemicolon {
    public static void main(String[] args) {
        System.out.println("Hello");
        System.out.println("World");
    }
}

教训:Java 不是 Python,别指望它会根据缩进猜测你的意图。


坑2:字符串比较请用 equals(),别用 ==

== 比较的是引用(内存地址),equals() 比较的是内容。搞混了,你可能会在登录系统里遇到"密码对但不让进"的诡异情况。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class StringComparison {
    public static void main(String[] args) {
        String a = new String("java");
        String b = new String("java");

        // == 比较的是引用地址,a 和 b 是两个不同的对象
        System.out.println(a == b);             // false
        System.out.println(a.equals(b));       // true

        // 字符串字面量会进入字符串常量池,可能共享引用
        String c = "python";
        String d = "python";
        System.out.println(c == d);             // true(常量池复用)
        System.out.println(c.equals(d));       // true
    }
}

建议:永远用 equals() 比较字符串内容,== 留给基本类型。


坑3:i++++i 傻傻分不清

前置++返回增加后的值,后置++返回增加前的值。在表达式里混用,一不小心就翻车。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class IncrementDemo {
    public static void main(String[] args) {
        int i = 5;

        // 后置++:先返回值,再自增
        int post = i++;
        System.out.println("post = " + post + ", i = " + i);  // post = 5, i = 6

        // 前置++:先自增,再返回值
        i = 5;
        int pre = ++i;
        System.out.println("pre = " + pre + ", i = " + i);    // pre = 6, i = 6

        // 在表达式中混用?恭喜你获得了玄学代码
        i = 5;
        int mystery = 2 * ++i + 2 * i++;
        System.out.println("mystery = " + mystery);  // 2*6 + 2*6 = 24? 不对!
        // 实际: 前半: 2*6=12(i变成6), 后半: 2*6=12(i变成7) = 24
    }
}

建议:单独一行用谁都行,在表达式里用请三思。


坑4:除法取整,丢失精度

整数相除结果还是整数,小数部分直接被"腰斩"。你以为算出来是 0.5,实际是 0。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class IntegerDivision {
    public static void main(String[] args) {
        int a = 5;
        int b = 2;

        // 整数除法:丢失小数部分
        System.out.println(a / b);        // 2,不是 2.5!

        // 解决方法:用 double
        System.out.println((double) a / b);  // 2.5
        System.out.println(a * 1.0 / b);     // 2.5
    }
}

坑5:&|&&|| 的区别

&| 总是执行两边表达式,&&|| 才是短路操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class LogicalOperators {
    public static void main(String[] args) {
        int x = 10;

        // && 短路:左边 false,右边不执行
        if (x < 5 && (x = 100) > 0) {
            System.out.println("true");
        }
        System.out.println("x = " + x);  // x = 10,左边已经 false,右边没执行

        // & 不短路:两边都执行
        x = 10;
        if (x < 5 & (x = 100) > 0) {
            System.out.println("true");
        }
        System.out.println("x = " + x);  // x = 100,赋值语句执行了
    }
}

忠告:大多数情况下用 &&||,除非你有特殊需求。


坑6:浮点数比较别用 ==

0.1 + 0.2 在二进制浮点世界里不等于 0.3,这是一个著名的"历史遗留问题"。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class FloatComparison {
    public static void main(String[] args) {
        double a = 0.1;
        double b = 0.2;
        double sum = a + b;

        System.out.println(sum == 0.3);           // false!!
        System.out.println(sum);                 // 0.30000000000000004

        // 正确做法:使用 BigDecimal 或允许误差
        // 方法1:BigDecimal(精确)
        System.out.println(new java.math.BigDecimal("0.1")
            .add(new java.math.BigDecimal("0.2"))
            .compareTo(new java.math.BigDecimal("0.3")) == 0);  // true

        // 方法2:允许误差范围
        double epsilon = 0.0001;
        System.out.println(Math.abs(sum - 0.3) < epsilon);  // true
    }
}

坑7:变量命名踩雷

Java 有保留字(keyword),不能用 classintif 等作为变量名。

1
2
3
4
5
6
7
8
9
// 这些都是非法的
// int class = 10;
// String if = "hello";
// double for = 3.14;

// 正确做法
int classCount = 10;
String condition = "hello";
double forRatio = 3.14;

坑8:类名和文件名必须匹配

public class Hello 必须放在 Hello.java 文件里,否则编译报错"类名与文件名不匹配"。

1
2
3
4
5
6
// 文件名: Hello.java
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, Java!");
    }
}

坑9:main 方法写错一个字都不行

main 方法是 Java 程序的入口,签名必须是 public static void main(String[] args)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 这些都是错的
// public static main(String[] args) {}      // 缺少 void
// public void main(String[] args) {}        // 不是 static
// public static void Main(String[] args) {}  // 大写 M 不行
// public static void main(String args) {}    // 少了[]

// 正确写法
public class MainMethodDemo {
    public static void main(String[] args) {
        System.out.println("程序入口必须是这个签名!");
    }
}

坑10:switch 的 case 要加 break

忘了 break?那就恭喜你喜提"case 穿透"大礼包,程序会"义无反顾"地执行下一个 case。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class SwitchFallThrough {
    public static void main(String[] args) {
        int score = 85;

        switch (score / 10) {
            case 10:
            case 9:
                System.out.println("优秀");  // 90-100分都是优秀
                break;
            case 8:
                System.out.println("良好");
                break;
            case 7:
            case 6:
                System.out.println("及格");  // 60-79分都是及格
                break;
            default:
                System.out.println("不及格");
        }
    }
}

提示:故意利用穿透可以简化代码(如上例),但要加注释说明。


坑11:二维数组初始化要小心

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class TwoDArrayInit {
    public static void main(String[] args) {
        // 创建一个 3x3 的二维数组
        int[][] arr = new int[3][3];

        // 下面这种创建方式只创建了外层数组,内层数组都是 null
        int[][] arr2 = new int[3][];
        // System.out.println(arr2[0][0]); // NullPointerException!

        // 正确做法:先创建外层,再逐行创建内层
        for (int i = 0; i < 3; i++) {
            arr2[i] = new int[3];
        }
        arr2[0][0] = 1;
        System.out.println("arr2[0][0] = " + arr2[0][0]);  // 1
    }
}

坑12:方法重载不看返回类型

方法重载(Overload)只看参数列表,和返回类型无关。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class OverloadDemo {
    // 这两个不是重载,是重复声明!编译错误
    // int add(int a, int b) { return a + b; }
    // double add(int a, int b) { return a + b; }

    // 正确重载:参数列表不同
    public static int add(int a, int b) {
        return a + b;
    }

    public static double add(double a, double b) {
        return a + b;
    }

    public static int add(int a, int b, int c) {
        return a + b + c;
    }

    public static void main(String[] args) {
        System.out.println(add(1, 2));       // 调用 int 版本
        System.out.println(add(1.5, 2.5));   // 调用 double 版本
        System.out.println(add(1, 2, 3));    // 调用三参数版本
    }
}

坑13:bytechar 不是一回事

byte 是有符号字节(-128127),char 是无符号字符(065535),一字之差,天壤之别。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class ByteVsChar {
    public static void main(String[] args) {
        byte b = (byte) 200;  // 需要强制转型
        System.out.println("byte: " + b);    // -56(溢出)

        char c = 200;  // char 是 Unicode 字符
        System.out.println("char: " + c);    // Ę

        // byte[] 用于二进制数据,char[] 用于字符文本
        byte[] data = {65, 66, 67};
        System.out.println(new String(data));  // ABC
    }
}

坑14:static 方法不能访问实例成员

static 方法属于类,不依赖实例,所以不能访问 thisinstanceVar 等实例成员。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StaticMethodAccess {
    private int instanceVar = 100;  // 实例变量

    public static void staticMethod() {
        // System.out.println(instanceVar);  // 编译错误!
        // System.out.println(this);          // 编译错误!

        // 只能访问静态成员
        System.out.println("static method");
    }

    public void instanceMethod() {
        // 实例方法可以访问一切
        System.out.println(this.instanceVar);  // OK
        staticMethod();                         // OK
    }

    public static void main(String[] args) {
        staticMethod();
        // instanceMethod();  // 编译错误!静态上下文不能直接调用实例方法
        new StaticMethodAccess().instanceMethod();  // 需要创建实例
    }
}

坑15:别在 for 循环里改集合

一边遍历一边删?ConcurrentModificationException 在向你招手。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ConcurrentModificationDemo {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("Alice");
        names.add("Bob");
        names.add("Charlie");

        // 错误做法:直接 remove 会抛异常
        // for (String name : names) {
        //     if (name.equals("Bob")) {
        //         names.remove(name);  // ConcurrentModificationException!
        //     }
        // }

        // 正确做法1:使用 Iterator
        Iterator<String> it = names.iterator();
        while (it.hasNext()) {
            String name = it.next();
            if (name.equals("Bob")) {
                it.remove();  // 用 Iterator 的 remove,不影响遍历
            }
        }
        System.out.println("方法1: " + names);  // [Alice, Charlie]

        // 正确做法2:倒序遍历
        names.clear();
        names.add("Alice");
        names.add("Bob");
        names.add("Charlie");
        for (int i = names.size() - 1; i >= 0; i--) {
            if (names.get(i).equals("Bob")) {
                names.remove(i);
            }
        }
        System.out.println("方法2: " + names);  // [Alice, Charlie]
    }
}

坑16:finalize() 已经deprecated

别依赖 finalize() 做资源清理,它可能根本不会执行,而且已经被标记为 @Deprecated。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class FinalizeDemo {
    private static int count = 0;

    @Override
    @SuppressWarnings("deprecation")
    protected void finalize() throws Throwable {
        count++;
        System.out.println("finalize 被调用了");
    }

    public static void main(String[] args) throws InterruptedException {
        FinalizeDemo obj = new FinalizeDemo();
        obj = null;
        System.gc();  // 建议 GC 回收,但不保证立即执行
        Thread.sleep(1000);
        System.out.println("finalize 被调用次数: " + count);
    }
}

替代方案:使用 try-with-resources 或手动 close()。


坑17:数字字面量的下划线和类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class NumericLiterals {
    public static void main(String[] args) {
        // Java 7+ 支持数字字面量加下划线(增加可读性)
        int billion = 1_000_000_000;
        System.out.println("十亿: " + billion);

        // 注意:下划线不能在开头、结尾、紧邻小数点
        // 1_000.5_5  // 错误
        // _100       // 错误

        // long 类型要加 L/l,否则会当作 int 处理
        // long big = 3000000000;  // 编译错误!超出 int 范围
        long big = 3000000000L;  // 正确

        // float 字面量要加 F/f
        float pi = 3.14F;  // 正确
        // float pi2 = 3.14;  // 错误!double 不能直接赋给 float
    }
}

坑18:位运算符优先级低

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class BitOperatorPrecedence {
    public static void main(String[] args) {
        int x = 1;
        int y = 2;

        // 表达式  x & y == 1  被解析为  x & (y == 1)
        System.out.println(x & y == 1);   // false,不是 0!

        // 应该加括号
        System.out.println((x & y) == 1);  // false

        // 验证:x & y 的值
        System.out.println("x & y = " + (x & y));  // 0

        // 正确写法
        System.out.println((x & y) == 0);  // true
    }
}

坑19:浮点数的 NaN 和无穷大

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class FloatNaNInfinity {
    public static void main(String[] args) {
        double d1 = 0.0 / 0.0;  // NaN
        double d2 = 1.0 / 0.0;  // Infinity
        double d3 = -1.0 / 0.0; // -Infinity

        System.out.println("d1 = " + d1);  // NaN
        System.out.println("d2 = " + d2);  // Infinity
        System.out.println("d3 = " + d3);  // -Infinity

        // NaN 非常特殊:它不等于任何值,包括自己
        System.out.println(d1 == d1);        // false!
        System.out.println(Double.isNaN(d1)); // true

        // Infinity 的比较
        System.out.println(d2 > 1000);  // true
        System.out.println(d2 == Double.POSITIVE_INFINITY);  // true
    }
}

坑20:short 和 byte 的自增要小心

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class ShortByteIncrement {
    public static void main(String[] args) {
        short s = 1;
        // s = s + 1;    // 编译错误!int + short -> int,需要强制转型
        s = (short)(s + 1);  // 正确
        s += 1;              // 正确!+= 自动强制转型
        System.out.println("s = " + s);

        byte b = 1;
        // b = b + 1;    // 编译错误!
        b = (byte)(b + 1);  // 正确
        b += 1;             // 正确!+= 自动处理
        System.out.println("b = " + b);

        // 但++不一样!byte++ 和 short++ 是合法的
        byte bb = 127;
        bb++;  // 不报错,但会溢出变成 -128
        System.out.println("bb = " + bb);
    }
}

坑21:静态初始化块和实例初始化块

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class InitializationOrder {
    static {
        System.out.println("静态初始化块 1");
    }

    {
        System.out.println("实例初始化块 1");
    }

    public InitializationOrder() {
        System.out.println("构造函数");
    }

    static {
        System.out.println("静态初始化块 2");
    }

    {
        System.out.println("实例初始化块 2");
    }

    public static void main(String[] args) {
        System.out.println("=== 创建第一个对象 ===");
        new InitializationOrder();
        System.out.println("=== 创建第二个对象 ===");
        new InitializationOrder();
    }
}

输出顺序:静态块(按出现顺序)→ 主方法 → 实例块+构造函数(每次 new 都执行)


坑22:可变参数要放最后

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class VarargsOrder {
    // 正确:可变参数放最后
    public static void printAll(String prefix, int... numbers) {
        System.out.print(prefix);
        for (int n : numbers) {
            System.out.print(n + " ");
        }
        System.out.println();
    }

    // 编译错误:可变参数不能在中间
    // public static void error(String... args, int x) {}

    public static void main(String[] args) {
        printAll("数字: ", 1, 2, 3, 4, 5);
        printAll("结果: ");
    }
}

坑23:goto?Java 没有这回事

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Java 没有 goto 语句
// goto;

public class LabelDemo {
    public static void main(String[] args) {
        // 但 Java 有标签(Label),配合 break/continue 使用
        outer:
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 5; j++) {
                if (j == 2) {
                    break outer;  // 直接跳出外层循环
                }
                System.out.println("i=" + i + ", j=" + j);
            }
        }
        System.out.println("循环结束");
    }
}

坑24:transient 变量不参与序列化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.*;

public class TransientDemo implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private transient String password;  // 不会被序列化

    public TransientDemo(String name, String password) {
        this.name = name;
        this.password = password;
    }

    @Override
    public String toString() {
        return "name='" + name + "', password='" + password + "'";
    }

    public static void main(String[] args) throws Exception {
        TransientDemo obj = new TransientDemo("张三", "secret123");

        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
        oos.close();

        // 反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        TransientDemo restored = (TransientDemo) ois.readObject();
        ois.close();

        System.out.println("原对象: " + obj);
        System.out.println("恢复后: " + restored);
        // password 变成 null 了!
    }
}

坑25:Integer 缓存池的坑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class IntegerCacheDemo {
    public static void main(String[] args) {
        // Integer 缓存了 -128 到 127 的整数
        Integer a = 127;
        Integer b = 127;
        System.out.println(a == b);  // true(缓存)

        Integer c = 128;
        Integer d = 128;
        System.out.println(c == d);  // false(超出缓存范围)

        // 自动装箱的陷阱
        Integer e = 100;
        Integer f = 100;
        System.out.println(e == f);  // true

        // 但运算会拆箱
        System.out.println(e + f);   // 200,自动拆箱
    }
}

坑26:三元运算符的自动拆箱

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class TernaryUnbox {
    public static void main(String[] args) {
        Integer a = null;

        // 三元运算符会把 null 自动拆箱,引发 NullPointerException
        // System.out.println(true ? a : 0);  // NPE!

        // 编译器看到的是:a.intValue(),但 a 是 null
        Integer b = 5;
        // 这行是安全的
        System.out.println(false ? b : 0);  // 0,但不推荐这样写
    }
}

坑27:this 和 super 在构造函数中的位置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class ThisSuperDemo {
    private int value;

    public ThisSuperDemo() {
        // super() 或 this() 必须放在构造函数第一行
        // 下面的写法是错误的:
        // System.out.println("在 super 之前打印");
        // super();

        // 正确做法
        this(0);  // 调用另一个构造函数,也必须第一行
    }

    public ThisSuperDemo(int value) {
        super();  // 默认调用 super()
        this.value = value;
    }
}

坑28:匿名内部类访问外部变量必须是 final

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class AnonymousClassDemo {
    private int outerVar = 10;

    public void test() {
        final int localVar = 20;  // 必须加 final(Java 8+ 可省略但仍是隐式 final)
        int mutableVar = 30;     // Java 8+ 可以省略 final,但不建议改

        new Thread(new Runnable() {
            @Override
            public void run() {
                // 内部类访问外部变量
                System.out.println("outerVar = " + outerVar);  // OK,外部类实例字段
                System.out.println("localVar = " + localVar);   // OK,final 变量
                // localVar = 999;  // 编译错误,不能修改
            }
        }).start();
    }

    public static void main(String[] args) {
        new AnonymousClassDemo().test();
    }
}

坑29:枚举可以switch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class EnumSwitchDemo {
    enum Color {
        RED, GREEN, BLUE
    }

    public static void main(String[] args) {
        Color c = Color.GREEN;

        // 枚举可以直接用在 switch 里
        switch (c) {
            case RED:
                System.out.println("红色");
                break;
            case GREEN:
                System.out.println("绿色");
                break;
            case BLUE:
                System.out.println("蓝色");
                break;
        }
    }
}

坑30:boolean 类型只有 true 和 false

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class BooleanDemo {
    public static void main(String[] args) {
        // Java 的 boolean 只有 true 和 false
        boolean isJavaFun = true;
        boolean isFishTasty = false;

        // 没有 "1" 或 "0",没有 null(作为字面量)
        // boolean notBool = 1;   // 编译错误!

        // 不要用 Boolean.TRUE/FALSE 比较,直接用 == 或 !=
        Boolean flag = Boolean.TRUE;
        if (flag == Boolean.TRUE) {  // 可以,但不推荐
            System.out.println("flag 是 true");
        }
        if (flag) {  // 推荐写法
            System.out.println("flag 是 true(推荐写法)");
        }
    }
}

41.2 OOP 层面的坑(20个)

坑31:子类构造函数必须调用父类构造函数

如果父类没有无参构造函数,子类必须显式调用有参构造函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class ConstructorChainDemo {
    public static void main(String[] args) {
        new Child();
    }
}

class Parent {
    Parent() {
        System.out.println("Parent() 无参构造");
    }
}

class Child extends Parent {
    Child() {
        // super() 会自动调用父类的无参构造函数
        // 如果父类没有无参构造函数,子类必须显式调用 super(参数)
        System.out.println("Child() 构造");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 没有无参构造函数的父类
class Parent2 {
    private String name;

    Parent2(String name) {
        this.name = name;
    }
}

// 子类必须显式调用 super
class Child2 extends Parent2 {
    Child2() {
        super("默认名称");  // 必须调用!
        System.out.println("Child2()");
    }
}

坑32:重写方法不能缩小访问权限

子类重写父类方法时,访问修饰符必须大于等于父类的可见性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class AccessModifierDemo {
    public static void main(String[] args) {
        Parent p = new Child();
        p.say();  // 输出什么?取决于实际类型
    }
}

class Parent {
    public void say() {
        System.out.println("Parent: 你好");
    }
}

class Child extends Parent {
    // 不能缩小访问权限!下面会编译错误:
    // private void say() {}     // 错误!比 public 更严格
    // void say() {}              // 错误!默认是 package-private,比 public 严格

    public void say() {
        System.out.println("Child: 你好");
    }
}

坑33:static 方法不能被重写,只能被隐藏

子类定义与父类 static 方法签名相同的方法,不是重写,是"隐藏"。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StaticOverrideDemo {
    public static void main(String[] args) {
        Parent p = new Child();

        // 调用的是父类的 static 方法(因为引用类型是 Parent)
        p.hello();  // Parent: hello

        // 实际上调用的取决于编译时类型,不是运行时类型
    }
}

class Parent {
    public static void hello() {
        System.out.println("Parent: hello");
    }
}

class Child extends Parent {
    // 这不是重写,是隐藏(Override 是运行时多态,隐藏是编译时绑定)
    public static void hello() {
        System.out.println("Child: hello");
    }
}

坑34:构造函数的陷阱——不要在构造函数里调用可被重写的方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class ConstructorOverrideTrap {
    public static void main(String[] args) {
        SubClass obj = new SubClass();
        // 输出顺序:
        // 1. SubClass 构造函数调用 super()
        // 2. Parent 构造函数调用 doSomething()
        // 3. 因为多态,实际调用的是 SubClass 的 doSomething()
        // 4. 此时 SubClass 的字段还没初始化!
    }
}

class Parent {
    Parent() {
        System.out.println("Parent 构造开始");
        doSomething();  // 陷阱:调用了可被重写的方法
        System.out.println("Parent 构造结束");
    }

    public void doSomething() {
        System.out.println("Parent.doSomething()");
    }
}

class SubClass extends Parent {
    private int value = printAndReturn(100);

    public SubClass() {
        System.out.println("SubClass 构造");
    }

    @Override
    public void doSomething() {
        // 此时 value 还是 0!因为父类构造函数先于子类字段初始化执行
        System.out.println("SubClass.doSomething(), value = " + value);
    }

    private static int printAndReturn(int v) {
        System.out.println("SubClass 字段初始化: " + v);
        return v;
    }
}

坑35:instanceof 和强制转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class InstanceofDemo {
    public static void main(String[] args) {
        Object obj = "Hello";

        // instanceof 检查类型
        if (obj instanceof String) {
            String s = (String) obj;  // 安全转换
            System.out.println("字符串长度: " + s.length());
        }

        // Java 16+ 支持 instanceof 模式匹配(不需要显式转型)
        if (obj instanceof String s) {
            System.out.println("字符串长度(模式匹配): " + s.length());
        }
    }
}

坑36:equals 和 hashCode 必须配对使用

如果两个对象 equals,它们必须有相同的 hashCode。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.util.HashSet;
import java.util.Set;

public class EqualsHashCodeDemo {
    public static void main(String[] args) {
        Set<Point> points = new HashSet<>();
        Point p1 = new Point(1, 2);
        Point p2 = new Point(1, 2);

        points.add(p1);
        System.out.println(points.contains(p2));  // false!如果没有正确实现 hashCode

        // 正确实现后
        points.clear();
        points.add(new GoodPoint(1, 2));
        System.out.println(points.contains(new GoodPoint(1, 2)));  // true
    }
}

class Point {
    int x, y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // 没有重写 equals 和 hashCode
}

// 正确实现
class GoodPoint {
    int x, y;

    GoodPoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        GoodPoint that = (GoodPoint) o;
        return x == that.x && y == that.y;
    }

    @Override
    public int hashCode() {
        return 31 * x + y;
    }
}

坑37:组合优先于继承

继承是强耦合,用组合更灵活。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 不推荐:继承用于"是"的关系,不是用于"有"的关系
class Dog extends Animal {
    // Dog is Animal,可以
}

class Car extends Animal {  // 这就不合理了
    // Car has Engine,应该用组合
}

// 推荐:组合
class Car {
    private Engine engine;  // Car has Engine
    private Wheel[] wheels;

    public void start() {
        engine.start();
    }
}

坑38:接口和抽象类的选择

接口用于行为抽象,抽象类用于代码复用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 接口:支持多实现
interface Flyable {
    void fly();
}

interface Swimmable {
    void swim();
}

// 类:单继承
abstract class Animal {
    void eat() {
        System.out.println("吃东西");
    }
}

// 可以同时实现多个接口,但只能继承一个类
class Duck extends Animal implements Flyable, Swimmable {
    @Override
    public void fly() {
        System.out.println("鸭子飞");
    }

    @Override
    public void swim() {
        System.out.println("鸭子游泳");
    }
}

坑39:内部类和外部类的访问

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class InnerClassAccess {
    private int outerField = 10;
    private static int staticField = 20;

    public static void main(String[] args) {
        // 静态内部类可以直接访问外部类的静态成员
        System.out.println(InnerClassAccess.staticField);
    }

    public void instanceMethod() {
        // 实例内部类可以访问外部类的一切成员
        class LocalClass {
            void display() {
                System.out.println(outerField);  // OK
                System.out.println(staticField);  // OK
            }
        }
    }

    // 非静态内部类持有外部类引用
    public class Inner {
        int innerField = 30;

        void accessOuter() {
            System.out.println(outerField);  // 自动持有外部类引用
            System.out.println(this.innerField);  // this 是内部类的
            System.out.println(InnerClassAccess.this.outerField);  // 外部类引用
        }
    }

    public static void main(String[] args) {
        InnerClassAccess outer = new InnerClassAccess();
        Inner inner = outer.new Inner();
        inner.accessOuter();
    }
}

坑40:序列化会破坏单例

反序列化会创建新对象,破坏单例模式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.*;

public class SerializableSingleton implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final SerializableSingleton INSTANCE = new SerializableSingleton();

    private SerializableSingleton() {
        // 私有构造函数
    }

    public static SerializableSingleton getInstance() {
        return INSTANCE;
    }

    // 防止反序列化创建新对象
    protected Object readResolve() {
        return INSTANCE;
    }

    public static void main(String[] args) throws Exception {
        SerializableSingleton s1 = SerializableSingleton.getInstance();
        SerializableSingleton s2 = SerializableSingleton.getInstance();
        System.out.println(s1 == s2);  // true

        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(s1);

        // 反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        SerializableSingleton s3 = (SerializableSingleton) ois.readObject();

        System.out.println(s1 == s3);  // 有 readResolve 返回 true,否则 false
    }
}

坑41:可变对象作为参数传入后被修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.Date;

public class MutableParamDemo {
    private Date birthDate;

    public MutableParamDemo(Date birthDate) {
        // 危险!外部的 Date 对象和内部引用指向同一个对象
        this.birthDate = birthDate;
    }

    public static void main(String[] args) {
        Date d = new Date(2000, 1, 1);
        MutableParamDemo obj = new MutableParamDemo(d);

        // 外部修改 Date
        d.setYear(3000);

        // 内部的值也被改了!
        System.out.println("birthDate: " + obj.birthDate);
    }
}

正确做法:防御性拷贝

1
2
3
public MutableParamDemo(Date birthDate) {
    this.birthDate = new Date(birthDate.getTime());  // 拷贝
}

坑42:equals 的对称性和传递性

自定义 equals 要注意数学性质。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class EqualsSymmetry {
    public static void main(String[] args) {
        // 违反对称性
        CaseInsensitiveString s1 = new CaseInsensitiveString("java");
        String s2 = new String("JAVA");

        // s1.equals(s2) 可能不等于 s2.equals(s1)
    }
}

class CaseInsensitiveString {
    private String s;

    public CaseInsensitiveString(String s) {
        this.s = s;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof CaseInsensitiveString) {
            return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
        }
        // 违反对称性!String 不认识我们
        if (obj instanceof String) {
            return s.equalsIgnoreCase((String) obj);
        }
        return false;
    }
}

坑43:lambda 表达式里的 this

lambda 表达式里的 this 指的是包含它的外部类实例,不是 lambda 本身。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class LambdaThisDemo {
    private String field = "外部类字段";

    public void method() {
        Runnable r = () -> {
            // 这里的 this 是 LambdaThisDemo,不是 Runnable
            System.out.println(this.field);
        };
        r.run();
    }

    public static void main(String[] args) {
        new LambdaThisDemo().method();  // 输出:外部类字段
    }
}

坑44:方法引用和 lambda 的区别

方法引用和 lambda 都能实现函数式接口,但方法引用更简洁。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public class MethodReferenceDemo {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

        // Lambda 写法
        names.forEach(name -> System.out.println(name));

        // 方法引用写法(更简洁)
        names.forEach(System.out::println);

        // 四种方法引用
        // 1. 静态方法引用: ClassName::staticMethod
        Function<String, Integer> parser = Integer::parseInt;

        // 2. 实例方法引用: instance::instanceMethod
        String str = "Hello";
        Supplier<Integer> length = str::length;

        // 3. 对象方法引用: ClassName::instanceMethod
        Function<String, String> upper = String::toUpperCase;

        // 4. 构造函数引用: ClassName::new
        Supplier<StringBuilder> sbSupplier = StringBuilder::new;
    }
}

坑45:序列化版本 UID

每个可序列化类都应该声明 serialVersionUID,否则类结构变化后旧对象无法反序列化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import java.io.*;

public class SerialVersionUIDDemo implements Serializable {
    // 推荐显式声明
    private static final long serialVersionUID = 1L;

    private String name;

    public SerialVersionUIDDemo(String name) {
        this.name = name;
    }
}

坑46:final 字段初始化时机

final 字段必须在构造函数或初始化块中初始化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class FinalFieldInit {
    private final int a;
    private final String b = "初始化";

    public FinalFieldInit(int a) {
        this.a = a;  // 必须在构造函数中初始化
    }

    // 错误:final 字段没有初始化
    // private final int c;

    public static void main(String[] args) {
        FinalFieldInit obj = new FinalFieldInit(10);
        System.out.println("a = " + obj.a + ", b = " + obj.b);
    }
}

坑47:接口里的常量

接口里的字段默认是 public static final,可以直接使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public interface MathConstants {
    // 等价于 public static final double PI = 3.14159;
    double PI = 3.14159;

    // 等价于 public static final double E = 2.71828;
    double E = 2.71828;
}

class Circle {
    public double area(double radius) {
        return MathConstants.PI * radius * radius;
    }
}

坑48:枚举的构造函数是 private

枚举的构造函数隐式是 private,不用写也能生效。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public enum Weekday {
    MONDAY("周一"),
    TUESDAY("周二"),
    WEDNESDAY("周三"),
    THURSDAY("周四"),
    FRIDAY("周五"),
    SATURDAY("周六"),
    SUNDAY("周日");

    private final String chinese;

    // 枚举构造函数必须是 private(隐式)
    Weekday(String chinese) {
        this.chinese = chinese;
    }

    public String getChinese() {
        return chinese;
    }
}

坑49:返回 null 还是空集合?

方法返回集合时,避免返回 null,用空集合代替。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ReturnEmptyVsNull {
    // 反面教材
    public List<String> getBadList() {
        return null;  // 调用方必须检查 null
    }

    // 正面教材
    public List<String> getGoodList() {
        return Collections.emptyList();  // 返回不可变的空列表
    }

    // 如果要返回可变列表
    public List<String> getEmptyMutableList() {
        return new ArrayList<>();  // 返回空的可变列表
    }
}

坑50:Cloneable 接口是破损的设计

Cloneable 没有定义 clone() 方法,Object 的 clone() 是 protected 的,克隆很麻烦。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class CloneableDemo {
    public static void main(String[] args) throws CloneNotSupportedException {
        Student s1 = new Student("张三", 20);
        Student s2 = (Student) s1.clone();  // 需要类型转换

        s2.name = "李四";
        System.out.println("s1: " + s1);  // 不变
        System.out.println("s2: " + s2);
    }
}

class Student implements Cloneable {
    String name;
    int age;

    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();  // Object 的 clone 是浅拷贝
    }

    @Override
    public String toString() {
        return name + ", " + age;
    }
}

推荐:使用拷贝构造函数或工厂方法。


41.3 集合层面的坑(20个)

坑51:ArrayList 初始化容量

ArrayList 默认容量是 10,不断 add 会触发扩容,指定初始容量可以减少扩容开销。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import java.util.ArrayList;

public class ArrayListInit {
    public static void main(String[] args) {
        // 不指定容量,默认空数组,第一次 add 时扩容到 10
        ArrayList<String> list1 = new ArrayList<>();

        // 预知大小,指定初始容量
        ArrayList<String> list2 = new ArrayList<>(100);

        // 使用 List.of() 创建不可变列表(Java 9+)
        var immutable = java.util.List.of("A", "B", "C");
        // immutable.add("D");  // UnsupportedOperationException!
    }
}

坑52:HashMap 的 key 要重写 equals 和 hashCode

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.util.HashMap;
import java.util.Map;

public class HashMapKeyDemo {
    public static void main(String[] args) {
        Map<PointKey, String> map = new HashMap<>();
        PointKey key = new PointKey(1, 2);

        map.put(key, "第一个点");
        System.out.println(map.get(key));  // 正常获取

        // 如果 key 的 hashCode/equals 实现有问题
        System.out.println(map.get(new PointKey(1, 2)));  // 可能 null!
    }
}

class PointKey {
    int x, y;

    PointKey(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // 错误实现:只基于 x
    @Override
    public int hashCode() {
        return x;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PointKey that = (PointKey) o;
        return x == that.x;  // 忽略了 y!
    }
}

坑53:HashMap 的死循环(Java 7 及之前)

在多线程环境下,Java 7 的 HashMap 扩容时可能导致环形链表,造成死循环。Java 8 修复了这个问题,但仍不是线程安全。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 危险代码示例(仅供理解,不要在实际代码中使用)
import java.util.HashMap;
import java.util.Map;

public class HashMapThreadUnsafe {
    // 不要在多线程环境下共享 HashMap
    private static final Map<Integer, Integer> map = new HashMap<>();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                map.put(i, i);
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                map.put(i, i);
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("map.size() = " + map.size());
        // 可能有数据丢失,或抛出 ConcurrentModificationException
    }
}

正确做法:使用 ConcurrentHashMap


坑54:Arrays.asList() 返回的列表不支持结构性修改

Arrays.asList() 返回的是固定大小的视图,不能 add/remove。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import java.util.Arrays;
import java.util.List;

public class ArraysAsListDemo {
    public static void main(String[] args) {
        String[] array = {"A", "B", "C"};
        List<String> list = Arrays.asList(array);

        list.set(0, "X");  // OK,可以修改
        System.out.println("array[0] = " + array[0]);  // 数组也被改了!

        // list.add("D");  // UnsupportedOperationException!
        // list.remove(0); // UnsupportedOperationException!

        // 如果需要可变列表
        java.util.ArrayList<String> mutableList = new java.util.ArrayList<>(list);
        mutableList.add("D");  // OK
    }
}

坑55:subList 不是独立的新列表

List.subList() 返回的是原列表的视图,对 subList 的修改会影响原列表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.ArrayList;
import java.util.List;

public class SubListDemo {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }

        List<Integer> sub = list.subList(2, 5);  // [2, 3, 4]
        System.out.println("sub: " + sub);

        // 修改 subList
        sub.set(0, 99);  // 修改会影响原列表
        System.out.println("list: " + list);  // list[2] 变成 99

        // subList 的操作范围不能超出原列表
        // sub.clear();  // 清空 subList 会清空原列表对应范围!
    }
}

坑56:Iterator.remove() 的正确姿势

必须先 next()remove(),不能连续 remove。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class IteratorRemoveDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        Iterator<String> it = list.iterator();

        // 正确做法:先 next,再 remove
        while (it.hasNext()) {
            String s = it.next();
            if (s.equals("B")) {
                it.remove();  // OK
            }
        }
        System.out.println("删除 B 后: " + list);

        // 错误做法:没有 next 就 remove
        // Iterator<String> it2 = list.iterator();
        // it2.remove();  // IllegalStateException!
    }
}

坑57:Set 的 add 返回值

Set.add() 返回 boolean,表示是否添加成功(元素不存在则添加成功返回 true)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import java.util.HashSet;
import java.util.Set;

public class SetAddReturnDemo {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();

        System.out.println(set.add("A"));  // true
        System.out.println(set.add("A"));  // false,已存在

        System.out.println("set = " + set);  // [A]

        // 利用返回值检测重复
        if (!set.add("B")) {
            System.out.println("B 已存在!");
        }
    }
}

坑58:TreeSet 的自然排序和自定义排序

TreeSet 元素必须可比较,要么实现 Comparable,要么提供 Comparator。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Set;
import java.util.TreeSet;

public class TreeSetDemo {
    public static void main(String[] args) {
        // 自然排序:元素实现 Comparable
        Set<Integer> numSet = new TreeSet<>();
        numSet.add(3);
        numSet.add(1);
        numSet.add(2);
        System.out.println(numSet);  // [1, 2, 3]

        // 自定义排序
        Set<String> strSet = new TreeSet<>((a, b) -> b.compareTo(a));  // 逆序
        strSet.add("Apple");
        strSet.add("Banana");
        strSet.add("Cherry");
        System.out.println(strSet);  // [Cherry, Banana, Apple]
    }
}

坑59:LinkedList 不是 List 的最佳实现

ArrayList 随机访问 O(1),LinkedList 随机访问 O(n)。大多数场景下 ArrayList 更高效。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class ArrayListVsLinkedList {
    public static void main(String[] args) {
        int n = 100000;

        // ArrayList 随机访问快
        List<Integer> arrayList = new ArrayList<>();
        for (int i = 0; i < n; i++) arrayList.add(i);
        long start = System.nanoTime();
        arrayList.get(n / 2);  // O(1)
        System.out.println("ArrayList get: " + (System.nanoTime() - start) + " ns");

        // LinkedList 随机访问慢
        List<Integer> linkedList = new LinkedList<>();
        for (int i = 0; i < n; i++) linkedList.add(i);
        start = System.nanoTime();
        linkedList.get(n / 2);  // O(n),要遍历一半
        System.out.println("LinkedList get: " + (System.nanoTime() - start) + " ns");
    }
}

坑60:HashSet 底层是 HashMap

HashSet 内部用一个 HashMap 存储元素,value 是固定的 PRESENT 对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class HashSetInternal {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        set.add("Hello");
        set.add("World");

        // HashSet 底层是 HashMap,添加的元素是 key
        // value 是一个固定的对象(private static final Object PRESENT = new Object())

        // 如果需要同时存 key 和 value,用 HashMap
        Map<String, Integer> map = new HashMap<>();
        map.put("Alice", 25);
        map.put("Bob", 30);
    }
}

坑61:Collections.sort() 会修改原列表

Collections.sort() 是原地排序,直接修改列表顺序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CollectionsSortDemo {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(3);
        list.add(1);
        list.add(2);

        List<Integer> backup = new ArrayList<>(list);  // 先备份

        Collections.sort(list);  // 原地排序
        System.out.println("排序后: " + list);  // [1, 2, 3]

        // 如果需要保留原列表,先拷贝
        List<Integer> sorted = new ArrayList<>(backup);
        Collections.sort(sorted);
        System.out.println("原列表: " + backup);
    }
}

坑62:Comparable 和 Comparator 的区别

Comparable 是自然排序(一目了然),Comparator 是自定义排序(灵活多样)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class ComparableVsComparator {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("张三", 85));
        students.add(new Student("李四", 92));
        students.add(new Student("王五", 78));

        // Student 已实现 Comparable(按分数降序)
        // students.sort(null);  // 使用 Comparable

        // 也可以用 Comparator 覆盖
        students.sort(Comparator.comparing(Student::getName));
        System.out.println("按姓名排序: " + students);

        students.sort(Comparator.comparingInt(Student::getScore).reversed());
        System.out.println("按分数降序: " + students);
    }
}

class Student implements Comparable<Student> {
    private String name;
    private int score;

    Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    public String getName() { return name; }
    public int getScore() { return score; }

    @Override
    public int compareTo(Student other) {
        return Integer.compare(other.score, this.score);  // 按分数降序
    }

    @Override
    public String toString() {
        return name + "(" + score + ")";
    }
}

坑63:ConcurrentHashMap 不允许 null

HashMap 可以存 null key 和 null value,但 ConcurrentHashMap 不允许。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import java.util.ConcurrentHashMap;
import java.util.HashMap;

public class NullInConcurrentHashMap {
    public static void main(String[] args) {
        // HashMap 可以
        HashMap<String, String> map = new HashMap<>();
        map.put(null, "value");
        map.put("key", null);
        System.out.println("HashMap: " + map);

        // ConcurrentHashMap 不行
        ConcurrentHashMap<String, String> chm = new ConcurrentHashMap<>();
        // chm.put(null, "value");  // NullPointerException!
        // chm.put("key", null);   // NullPointerException!
        chm.put("key", "value");  // OK
    }
}

坑64:fail-fast 机制

迭代时修改集合会触发 fail-fast,快速失败。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.ArrayList;
import java.util.List;

public class FailFastDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");

        for (String s : list) {
            if (s.equals("B")) {
                list.remove(s);  // 可能触发 ConcurrentModificationException
            }
        }

        // 正确做法:用 Iterator
        for (java.util.Iterator<String> it = list.iterator(); it.hasNext(); ) {
            String s = it.next();
            if (s.equals("B")) {
                it.remove();
            }
        }
        System.out.println(list);
    }
}

坑65:EnumSet 的高效实现

EnumSet 使用位向量实现,比 HashSet 更高效。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import java.util.EnumSet;

public class EnumSetDemo {
    public static void main(String[] args) {
        // EnumSet 是抽象类,不能 new,用 of() 创建
        EnumSet<Color> set = EnumSet.noneOf(Color.class);  // 空集合
        set.add(Color.RED);
        set.add(Color.BLUE);

        // 其他创建方式
        EnumSet<Color> set2 = EnumSet.allOf(Color.class);  // 全部
        EnumSet<Color> set3 = EnumSet.of(Color.RED, Color.GREEN);  // 指定
        EnumSet<Color> set4 = EnumSet.range(Color.RED, Color.GLUE);  // 范围
    }
}

enum Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET, BLACK, WHITE, GLUE
}

坑66:PriorityQueue 的堆特性

PriorityQueue 是基于堆的优先级队列,不保证 FIFO,按自然顺序或自定义顺序出队。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.PriorityQueue;
import java.util.Queue;

public class PriorityQueueDemo {
    public static void main(String[] args) {
        // 默认是最小堆(自然顺序)
        Queue<Integer> pq = new PriorityQueue<>();
        pq.add(5);
        pq.add(2);
        pq.add(8);
        pq.add(1);

        System.out.println("出队顺序: ");
        while (!pq.isEmpty()) {
            System.out.print(pq.poll() + " ");  // 1, 2, 5, 8(不是添加顺序)
        }
        System.out.println();

        // 最大堆(逆序)
        Queue<Integer> maxPq = new PriorityQueue<>((a, b) -> b - a);
        maxPq.add(5);
        maxPq.add(2);
        maxPq.add(8);
        System.out.println("最大堆出队: " + maxPq.poll());  // 8
    }
}

坑67:LinkedHashSet 保持插入顺序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import java.util.LinkedHashSet;
import java.util.Set;

public class LinkedHashSetDemo {
    public static void main(String[] args) {
        Set<String> set = new LinkedHashSet<>();
        set.add("Zhao");
        set.add("Sun");
        set.add("Li");
        set.add("Qian");

        System.out.println("保持插入顺序: " + set);
        // 输出: [Zhao, Sun, Li, Qian]

        // 用 LinkedHashSet 实现 LRU 缓存
    }
}

坑68:Collections 工具类的坑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import java.util.Collections;
import java.util.List;

public class CollectionsStaticDemo {
    public static void main(String[] args) {
        // Collections.max/min 要求列表非空
        // Collections.max(List.of())  // NoSuchElementException

        // synchronizedXXX 返回的集合方法都要手动同步
        // List<String> syncList = Collections.synchronizedList(new ArrayList<>());
        // 遍历时需要手动同步: synchronized(syncList) { for(String s : syncList) {...} }

        // unmodifiableXXX 返回的集合不可修改
        // List<String> unmod = Collections.unmodifiableList(new ArrayList<>());
        // unmod.add("new");  // UnsupportedOperationException
    }
}

坑69:List.toArray() 的类型问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.ArrayList;
import java.util.List;

public class ListToArrayDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");

        // toArray() 返回 Object[]
        Object[] arr1 = list.toArray();
        System.out.println("arr1 类型: " + arr1.getClass());  // class [Ljava.lang.Object;

        // toArray(T[] a) 返回指定类型的数组
        String[] arr2 = list.toArray(new String[0]);
        System.out.println("arr2 类型: " + arr2.getClass());  // class [Ljava.lang.String;

        // 指定数组长度小于集合大小时,会创建新数组
        String[] arr3 = list.toArray(new String[100]);
        System.out.println("arr3 长度: " + arr3.length);  // 100
    }
}

坑70:集合和数组的相互转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.Arrays;
import java.util.List;

public class CollectionArrayConvert {
    public static void main(String[] args) {
        // 数组转 List
        String[] array = {"A", "B", "C"};

        // Arrays.asList() - 固定大小
        List<String> list1 = Arrays.asList(array);
        // list1.add("D");  // UnsupportedOperationException

        // new ArrayList<>() - 可变
        List<String> list2 = new java.util.ArrayList<>(Arrays.asList(array));
        list2.add("D");  // OK

        // List.of() - Java 9+,不可变
        List<String> list3 = List.of(array);

        // List 转数组
        List<String> list = Arrays.asList("X", "Y", "Z");
        String[] arr = list.toArray(new String[0]);
        String[] arr2 = list.toArray(String[]::new);  // 方法引用
    }
}

41.4 并发层面的坑(15个)

坑71:volatile 不保证原子性

volatile 保证可见性和有序性,但不保证原子性(如 i++)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class VolatileDemo implements Runnable {
    // volatile 保证可见性:一个线程修改,其他线程立即看到
    private volatile boolean running = true;
    private int count = 0;

    @Override
    public void run() {
        while (running) {
            count++;  // volatile 不能保证这个操作的原子性
        }
        System.out.println("线程结束,count = " + count);
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileDemo demo = new VolatileDemo();
        Thread t = new Thread(demo);
        t.start();

        Thread.sleep(1000);
        demo.running = false;  // 停止线程

        // count 可能小于 1000000,因为 i++ 不是原子操作
        System.out.println("main: running = false, count = " + demo.count);
    }
}

正确做法:使用 AtomicIntegersynchronized


坑72:synchronized 加错对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class WrongLockObject {
    // 错误:在不同对象上加锁,等于没锁
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void method1() {
        synchronized (lock1) {
            // 业务逻辑1
        }
    }

    public void method2() {
        synchronized (lock2) {
            // 业务逻辑2
        }
    }

    // 正确做法:用同一个锁
    private final Object lock = new Object();

    public void correctMethod1() {
        synchronized (lock) {
            // 业务逻辑1
        }
    }

    public void correctMethod2() {
        synchronized (lock) {
            // 业务逻辑2
        }
    }
}

坑73:Thread.sleep 不释放锁

sleep() 会让线程进入 TIMED_WAITING 状态,但不会释放已持有的锁。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class SleepNoReleaseLock {
    private final Object lock = new Object();

    public void method() {
        synchronized (lock) {
            System.out.println("获得锁");
            try {
                Thread.sleep(5000);  // 睡5秒,锁还在手里
                // 其他等待这个锁的线程只能干等
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

替代:使用 wait()/notify() 会释放锁。


坑74:wait() 必须在 synchronized 中调用

wait()notify()notifyAll() 必须先获得对象的监视器锁,否则抛 IllegalMonitorStateException。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class WaitNotifyDemo {
    private final Object lock = new Object();

    public void waitDemo() throws InterruptedException {
        // 错误写法
        // lock.wait();  // IllegalMonitorStateException!

        // 正确写法
        synchronized (lock) {
            while (someConditionNotMet()) {
                lock.wait();  // 等待并释放锁
            }
            // 继续执行
        }
    }

    public void notifyDemo() {
        synchronized (lock) {
            // 做一些准备
            lock.notify();  // 唤醒一个等待的线程
            // 或者 lock.notifyAll(); 唤醒所有
        }
    }

    private boolean someConditionNotMet() {
        return true;
    }
}

坑75:线程池的七大参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.concurrent.*;

public class ThreadPoolParams {
    public static void main(String[] args) {
        // 七大参数详解
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2,                          // corePoolSize:核心线程数
            5,                          // maximumPoolSize:最大线程数
            60L,                        // keepAliveTime:多余线程存活时间
            TimeUnit.SECONDS,           // 时间单位
            new LinkedBlockingQueue<>(3),  // 任务队列
            Executors.defaultThreadFactory(),  // 线程工厂
            new ThreadPoolExecutor.AbortPolicy()  // 拒绝策略
        );

        // 拒绝策略:
        // AbortPolicy - 抛 RejectedExecutionException
        // CallerRunsPolicy - 由调用者线程执行
        // DiscardPolicy - 丢弃任务
        // DiscardOldestPolicy - 丢弃队列中最老的任务

        executor.execute(() -> System.out.println("任务执行"));
        executor.shutdown();
    }
}

坑76:CountDownLatch 和 CyclicBarrier 的区别

CountDownLatch 是倒计时门闩,只能用一次;CyclicBarrier 是循环栅栏,可重复使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.concurrent.*;

public class LatchVsBarrier {
    public static void main(String[] args) throws InterruptedException {
        // CountDownLatch:等所有玩家加载完成才开始游戏
        CountDownLatch latch = new CountDownLatch(3);
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println("玩家加载完成");
                latch.countDown();  // 计数减1
            }).start();
        }
        latch.await();  // 等待计数为0
        System.out.println("游戏开始!");

        // CyclicBarrier:等所有玩家都到达某个点后一起继续
        CyclicBarrier barrier = new CyclicBarrier(3, () ->
            System.out.println("所有人都到了,出发!"));
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println("玩家到达出发点");
                try {
                    barrier.await();  // 等待其他人
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

坑77:Future.get() 的阻塞

Future.get() 会阻塞等待结果,永远不用或用错会导致线程卡住。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.concurrent.*;

public class FutureGetDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<Integer> future = executor.submit(() -> {
            Thread.sleep(2000);
            return 42;
        });

        System.out.println("做其他事情...");

        // future.get() 会阻塞等待结果
        Integer result = future.get();  // 阻塞2秒
        System.out.println("结果是: " + result);

        // 正确做法:用超时
        try {
            result = future.get(1, TimeUnit.SECONDS);  // 只等1秒
        } catch (TimeoutException e) {
            System.out.println("超时了!");
        }

        executor.shutdown();
    }
}

坑78:ThreadLocal 的内存泄漏

ThreadLocalMap 的 Entry 引用 ThreadLocal,如果 ThreadLocal 被回收,但 Entry 还存在,可能导致内存泄漏。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ThreadLocalDemo {
    // ThreadLocal 变量
    private static final ThreadLocal<String> tl = new ThreadLocal<>();

    public static void main(String[] args) {
        tl.set("主线程值");

        Thread t = new Thread(() -> {
            tl.set("子线程值");
            System.out.println("子线程: " + tl.get());
            // 如果不 remove,子线程用完回收时可能有问题
        });

        t.start();
        t.join();

        System.out.println("主线程: " + tl.get());

        // 用完要 remove(尤其是在线程池中)
        tl.remove();
    }
}

坑79:死锁

两个或多个线程互相等待对方持有的锁。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class DeadLockDemo {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void method1() {
        synchronized (lock1) {
            System.out.println("method1 获得 lock1");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {}

            synchronized (lock2) {  // 等待 lock2
                System.out.println("method1 获得 lock2");
            }
        }
    }

    public void method2() {
        synchronized (lock2) {
            System.out.println("method2 获得 lock2");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {}

            synchronized (lock1) {  // 等待 lock1 - 可能死锁!
                System.out.println("method2 获得 lock1");
            }
        }
    }

    public static void main(String[] args) {
        DeadLockDemo demo = new DeadLockDemo();

        new Thread(demo::method1).start();
        new Thread(demo::method2).start();
    }
}

避免死锁:按固定顺序获取锁,或使用 tryLock() 并设置超时。


坑80:notify() 和 notifyAll() 的误用

唤醒一个线程时用 notify(),唤醒所有线程时用 notifyAll()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class NotifyVsNotifyAll {
    private boolean condition = false;

    // notify() 只唤醒一个线程,可能唤醒错误的线程
    // notifyAll() 唤醒所有等待的线程,更安全

    public synchronized void waitForCondition() throws InterruptedException {
        while (!condition) {  // 用 while 不用 if,防止伪唤醒
            wait();
        }
        System.out.println("条件满足,继续执行");
    }

    public synchronized void signalCondition() {
        condition = true;
        notifyAll();  // 推荐使用 notifyAll()
    }
}

坑81:原子类不是万能的

AtomicIntegerAtomicReference 等只能保证单个操作的原子性,复合操作仍需加锁。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicTrap {
    private AtomicInteger counter = new AtomicInteger(0);

    // 这个操作不是原子的!
    public void incrementNotAtomic() {
        // 虽然 get() 和 set() 各自是原子的,但中间可能被其他线程插队
        int current = counter.get();
        counter.set(current + 1);
    }

    // 正确做法:用 incrementAndGet()
    public void incrementAtomic() {
        counter.incrementAndGet();
    }

    // CAS 操作
    public void incrementWithCAS() {
        while (true) {
            int current = counter.get();
            if (counter.compareAndSet(current, current + 1)) {
                break;  // 成功就退出
            }
            // 否则重试
        }
    }
}

坑82:Executors 创建线程池的坑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.concurrent.*;

public class ExecutorsTrap {
    public static void main(String[] args) {
        // 危险:FixedThreadPool 和 SingleThreadPool 队列无限大
        ExecutorService exec1 = Executors.newFixedThreadPool(1);  // 队列 Integer.MAX_VALUE
        // ExecutorService exec2 = Executors.newSingleThreadExecutor();

        // 危险:CachedThreadPool 线程数无上限
        // ExecutorService exec3 = Executors.newCachedThreadPool();

        // 推荐:手动创建线程池,明确参数
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            4, 4, 60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100)  // 有界队列
        );

        // 或者用 abortPolicy 防止任务丢失
        executor.execute(() -> {
            // 任务
        });

        executor.shutdown();
    }
}

坑83:Thread.interrupt() 不是立即停止线程

interrupt() 只是设置中断标志,线程需要主动检查并处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    System.out.println("工作中...");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // sleep 期间被打断会抛异常并清除中断标志
                    System.out.println("被打断了!");
                    // 处理完后应该退出,或者重新设置中断标志
                    Thread.currentThread().interrupt();  // 重新设置
                    break;
                }
            }
            System.out.println("线程退出");
        });

        t.start();
        Thread.sleep(3000);
        t.interrupt();  // 设置中断标志
        t.join();
    }
}

坑84:守护线程的陷阱

Java 进程会在所有用户线程结束后终止,守护线程会被直接杀掉。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DaemonThreadDemo {
    public static void main(String[] args) {
        Thread daemon = new Thread(() -> {
            while (true) {
                System.out.println("守护线程运行中...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    break;
                }
            }
        });

        daemon.setDaemon(true);  // 必须在 start() 之前设置
        daemon.start();

        System.out.println("主线程开始其他工作...");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {}

        System.out.println("主线程结束,进程即将退出");
        // JVM 不会等待守护线程,会直接退出
    }
}

坑85:CompletableFuture 的坑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.concurrent.*;

public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        // 默认使用 ForkJoinPool.commonPool(),不一定是你想要的
        CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
            return "Hello";
        });

        // 如果任务抛出异常,get() 会抛出 ExecutionException
        CompletableFuture<String> errorCf = CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("故意的");
        });

        try {
            errorCf.get();
        } catch (ExecutionException e) {
            System.out.println("异常: " + e.getCause());
        }

        // 正确做法:用 exceptionally 或 handle 处理异常
        CompletableFuture<String> safeCf = CompletableFuture
            .supplyAsync(() -> {
                throw new RuntimeException("错误");
            })
            .exceptionally(ex -> "默认值")
            .thenApply(s -> s.toUpperCase());

        System.out.println(safeCf.get());  // 默认值转大写
    }
}

41.5 异常层面的坑(10个)

坑86:捕获具体异常而非 Exception

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CatchSpecificException {
    public static void main(String[] args) {
        // 反面教材:捕获 Exception 太宽泛
        try {
            riskyMethod();
        } catch (Exception e) {
            // 这样会捕获所有异常,包括 RuntimeException、Error 等
            // 可能隐藏真正的 bug
        }

        // 正确做法:按具体类型捕获
        try {
            riskyMethod();
        } catch (IOException e) {
            // 处理 IO 异常
        } catch (SQLException e) {
            // 处理 SQL 异常
        } finally {
            // 释放资源
        }
    }

    private static void riskyMethod() throws IOException, SQLException {}
}

坑87:不要吞噬异常

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class SwallowException {
    // 反面教材
    public void badMethod() {
        try {
            // 可能抛异常
        } catch (Exception e) {
            // 什么都没做,异常消失了!
            // e.printStackTrace();  // 最多打印一下日志
        }
    }

    // 正确做法
    public void goodMethod() {
        try {
            // 可能抛异常
        } catch (SpecificException e) {
            // 记录日志
            Logger.getLogger().log(Level.SEVERE, "操作失败", e);
            // 或者转换异常重新抛出
            throw new BusinessException("业务操作失败", e);
            // 或者恢复并给出默认值
        }
    }
}

class Logger {
    static java.util.logging.Logger getLogger() {
        return java.util.logging.Logger.getLogger("test");
    }
}

class SpecificException extends Exception {}
class BusinessException extends RuntimeException {
    BusinessException(String msg, Throwable cause) {
        super(msg, cause);
    }
}

坑88:finally 里抛异常会覆盖原异常

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class FinallyException {
    public static void main(String[] args) {
        try {
            throw new RuntimeException("原始异常");
        } catch (RuntimeException e) {
            System.out.println("捕获: " + e.getMessage());
        } finally {
            // 如果这里抛异常,会覆盖原来的异常
            // throw new RuntimeException("finally 异常");
        }
    }

    // 正确做法
    public void correctApproach() {
        RuntimeException original = null;
        try {
            throw new RuntimeException("原始异常");
        } catch (RuntimeException e) {
            original = e;
        } finally {
            try {
                // 可能抛异常的代码
            } catch (Exception e) {
                if (original != null) {
                    original.addSuppressed(e);  // 把 finally 的异常加到原异常
                    throw original;
                }
                throw e;
            }
        }
    }
}

坑89:不要在 finally 里 return

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class FinallyReturnDemo {
    public static void main(String[] args) {
        System.out.println("返回值: " + test());
    }

    public static int test() {
        try {
            return 1;
        } finally {
            // finally 里的 return 会覆盖 try 的 return
            return 2;
        }
    }
    // 输出: 返回值: 2
}

坑90:Checked Exception 和 Unchecked Exception

RuntimeException 及子类是未受检异常,不需要强制捕获;其他 Throwable 是受检异常,必须捕获或声明 throws。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import java.io.*;

public class CheckedVsUnchecked {
    // 受检异常:必须处理
    public void readFile() throws FileNotFoundException {
        FileReader reader = new FileReader("file.txt");
    }

    // 未受检异常:可以选择捕获
    public void accessArray(int[] arr, int index) {
        // 数组越界是 RuntimeException,不需要声明 throws
        // 如果不捕获,会一直上抛直到 JVM
        System.out.println(arr[index]);
    }
}

坑91:自定义异常要有意义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 反面教材
class MyException extends Exception {}

// 正面教材
class InsufficientFundsException extends RuntimeException {
    private final double deficit;

    public InsufficientFundsException(double deficit) {
        super(String.format("余额不足,差 %.2f 元", deficit));
        this.deficit = deficit;
    }

    public double getDeficit() {
        return deficit;
    }
}

class Account {
    private double balance;

    public void withdraw(double amount) {
        if (amount > balance) {
            throw new InsufficientFundsException(amount - balance);
        }
        balance -= amount;
    }
}

坑92:异常链和根本原因

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ExceptionChaining {
    public static void main(String[] args) {
        try {
            level3();
        } catch (RuntimeException e) {
            System.out.println("顶层异常: " + e.getMessage());
            Throwable cause = e.getCause();
            while (cause != null) {
                System.out.println("  原因: " + cause.getMessage());
                cause = cause.getCause();
            }
        }
    }

    static void level3() {
        try {
            level2();
        } catch (Exception e) {
            throw new RuntimeException("业务层错误", e);  // 保留原因
        }
    }

    static void level2() throws Exception {
        level1();
    }

    static void level1() throws Exception {
        throw new Exception("根本原因:数据库连接失败");
    }
}

坑93:try-with-resources 自动关闭资源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.*;

public class TryWithResourcesDemo {
    public static void main(String[] args) {
        // Java 7+ 推荐用法
        try (BufferedReader reader = new BufferedReader(
                new FileReader("file.txt"));
             BufferedWriter writer = new BufferedWriter(
                new FileWriter("output.txt"))) {
            // 自动关闭,即使发生异常也会关闭
            String line;
            while ((line = reader.readLine()) != null) {
                writer.write(line);
                writer.newLine();
            }
        } catch (IOException e) {
            // 只处理业务异常
            e.printStackTrace();
        }

        // 自定义资源类需要实现 AutoCloseable
    }
}

// 自定义可关闭资源
class DatabaseConnection implements AutoCloseable {
    @Override
    public void close() {
        System.out.println("关闭数据库连接");
    }
}

坑94:断言不是业务逻辑

断言在生产环境可能被禁用,不能依赖它做业务判断。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class AssertDemo {
    public static void main(String[] args) {
        // 默认禁用,需要 -ea 或 -enableassertions 开启
        assert args.length > 0 : "必须提供参数";

        // 正确用法:检查不应该发生的情况(内部 invariants)
        int balance = -100;  // 不应该发生
        assert balance >= 0 : "余额不能为负";

        // 错误用法:不能用于公开 API 的参数校验
        // public void withdraw(double amount) {
        //     assert amount > 0;  // 不靠谱!可能没开启断言
        // }
    }
}

坑95:异常匹配顺序

catch 块按顺序匹配,先捕获子类再捕获父类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class ExceptionCatchOrder {
    public static void main(String[] args) {
        try {
            throw new FileNotFoundException("文件不存在");
        } catch (Exception e) {
            // 如果 Exception 在前面,后面的具体 catch 永远不会执行
        }

        // 正确顺序:从小到大(子类在前,父类在后)
        try {
            throw new FileNotFoundException("文件不存在");
        } catch (FileNotFoundException e) {
            // 先捕获具体的
        } catch (IOException e) {
            // 再捕获宽泛的
        } catch (Exception e) {
            // 最后兜底
        }
    }
}

41.6 泛型层面的坑(5个)

坑96:泛型类型擦除

泛型信息在编译后被擦除,运行时不认识泛型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import java.util.ArrayList;

public class TypeErasureDemo {
    public static void main(String[] args) {
        ArrayList<String> stringList = new ArrayList<>();
        ArrayList<Integer> intList = new ArrayList<>();

        // 运行时类型都是 ArrayList
        System.out.println(stringList.getClass() == intList.getClass());  // true

        // 不能这样判断
        // if (stringList instanceof ArrayList<String>)  // 编译错误!

        // 如果需要泛型的运行时类型信息
        System.out.println(stringList instanceof ArrayList<?>);  // 可以
    }
}

坑97:泛型不能用于基本类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class GenericPrimitiveDemo {
    // 错误:泛型不能是基本类型
    // List<int> intList = new ArrayList<>();

    // 正确:使用包装类
    java.util.List<Integer> intList = new java.util.ArrayList<>();
    intList.add(42);  // 自动装箱

    // 获取时自动拆箱
    int val = intList.get(0);  // 自动拆箱
}

坑98:泛型方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class GenericMethodDemo {
    // 泛型方法:类型参数放在返回类型之前
    public static <T> T returnFirst(T first, T second) {
        return first;
    }

    // 多个类型参数
    public static <K, V> java.util.Map<K, V> createMap(K key, V value) {
        java.util.Map<K, V> map = new java.util.HashMap<>();
        map.put(key, value);
        return map;
    }

    public static void main(String[] args) {
        // 调用时自动推断类型
        String first = returnFirst("Hello", "World");
        Integer num = returnFirst(42, 100);

        // 也可以显式指定
        String s = GenericMethodDemo.<String>returnFirst("A", "B");
    }
}

坑99:泛型通配符

? extends T 表示上界(生产者),? super T 表示下界(消费者)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.*;

public class WildcardDemo {
    // PECS 原则:Producer-Extends, Consumer-Super

    // 生产者:只读取数据,用 extends
    public static double sumOfList(List<? extends Number> list) {
        double sum = 0;
        for (Number n : list) {
            sum += n.doubleValue();
        }
        return sum;
    }

    // 消费者:只写入数据,用 super
    public static void addNumbers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        // list.get(0) 返回的是 Object,不是 Integer
    }

    public static void main(String[] args) {
        List<Integer> ints = Arrays.asList(1, 2, 3);
        List<Double> doubles = Arrays.asList(1.1, 2.2);

        System.out.println(sumOfList(ints));     // OK
        System.out.println(sumOfList(doubles)); // OK

        List<Number> numbers = new ArrayList<>();
        addNumbers(numbers);
        System.out.println(numbers);
    }
}

坑100:泛型数组

Java 不允许创建具体类型的泛型数组,但可以创建通配符数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class GenericArrayDemo {
    // 错误:不能 new T[]
    // public static <T> T[] createArray() {
    //     return new T[10];  // 编译错误!
    // }

    // 正确做法1:使用 Class<T>
    public static <T> T[] createArray(Class<T> clazz, int size) {
        @SuppressWarnings("unchecked")
        T[] array = (T[]) java.lang.reflect.Array.newInstance(clazz, size);
        return array;
    }

    // 正确做法2:使用 List<T>
    public static <T> List<T> createList() {
        return new ArrayList<>();
    }

    // 通配符数组(有警告)
    public static void wildcardArray() {
        // @SuppressWarnings("unchecked")
        // List<String>[] array = new List<String>[10];  // 编译错误!

        List<?>[] array = new List<?>[10];  // OK
        array[0] = Arrays.asList("A");
    }

    public static void main(String[] args) {
        String[] arr = createArray(String.class, 5);
        arr[0] = "Hello";
        System.out.println(Arrays.toString(arr));
    }
}

本章小结

本章汇集了 Java 初学者最常踩的 100 个坑,按语法、OOP、集合、并发、异常、泛型六个类别整理:

类别坑的数量核心要点
语法层面30个分号、字符串比较、类型转换、运算符优先级
OOP层面20个构造链、访问修饰符、static 方法、equals/hashCode
集合层面20个初始化容量、fail-fast、null 值、subList 视图
并发层面15个volatile 原子性、死锁、线程池参数、ThreadLocal 泄漏
异常层面10个异常吞噬、finally return、try-with-resources
泛型层面5个类型擦除、通配符 PECS、泛型数组

学习建议

  1. 动手实践:每个坑都自己运行一遍,印象深刻
  2. 理解原理:知道"为什么错"比知道"怎么改"更重要
  3. 养成习惯:遵循最佳实践,从源头避免踩坑
  4. 持续积累:遇到新坑就记录下来,形成自己的"避坑手册"

“纸上得来终觉浅,绝知此事要躬行。”

踩坑不可怕,可怕的是踩了同一个坑两次。希望本章能帮你少走弯路,写出更健壮的 Java 代码!


下一章我们将进入 Java 高级特性的学习,敬请期待!

最后修改 March 30, 2026: 新增 Java 教程 (4da1bd7)