← 返回 Java核心图解 目录

🕳️ Java基础陷阱图解

equals/hashCode · String常量池 · 异常体系 · 泛型擦除 · 自动装箱 · 面试高频坑

⚖️ S1: equals() 与 hashCode() 契约

equals/hashCode = 身份证系统
equals() = 比对两个人是不是同一个人(看身份证号)
hashCode() = 身份证前6位(地区码)→ 先按地区分到不同窗口,再在窗口内细查

契约:同一个地区码(hashCode)的人不一定相同(equals),但相同(equals)的人一定在同一地区码(hashCode)!
如果你只重写equals不重写hashCode → 两个"相同"的人被分到了不同窗口 → HashMap找不到!
HashMap 查找过程:为什么只重写equals不够? HashMap 内部数组(桶) 桶0 key:A(1) 桶1 key:B(2) 桶2 key:C(3) ··· 桶N-2 ❌ key:B'(2) 桶N-1 key:D ❌ 只重写equals,不重写hashCode B = new Person("张三") → hashCode=桶1 B'= new Person("张三") → hashCode=桶N-2 equals为true,但hashCode不同 → 找不到! ✅ equals和hashCode都重写 B = new Person("张三") → hashCode=桶1 B'= new Person("张三") → hashCode=桶1 同一个桶 → equals比较 → 找到了!
HashMap查找:先hashCode定位桶,再equals比较key → 两者必须一致!
EqualsHashCodeTrap.java
📁 src/main/java
📁 trap
EqualsHashCodeTrap.java
StringPoolTrap.java
GenericTrap.java
// 🕳️ 陷阱1: 只重写equals不重写hashCode class BadPerson { private String name; public BadPerson(String name) { this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof BadPerson)) return false; return name.equals(((BadPerson)o).name); } // ❌ 没重写hashCode!用的是Object默认的(内存地址) } Map<BadPerson, String> map = new HashMap<>(); map.put(new BadPerson("张三"), "A"); map.get(new BadPerson("张三")); // ❌ null!找不到! // ✅ 正确做法:equals和hashCode一起重写 class GoodPerson { private String name; @Override public boolean equals(Object o) { ... } @Override public int hashCode() { return name.hashCode(); } } // 💡 最佳实践:用Lombok @EqualsAndHashCode 或 IDE自动生成

equals/hashCode 五大契约

#契约含义违反后果
1自反性x.equals(x) == true放入集合后找不到
2对称性x.equals(y) ↔ y.equals(x)两个集合包含关系不一致
3传递性x=y, y=z → x=z链式比较出错
4一致性多次调用结果不变集合行为不确定
5hashCode一致性equals为true → hashCode必须相同HashMap/HashSet失效
面试速记:equals相等→hashCode必须相等;hashCode相等→equals不一定相等(哈希碰撞)。只重写equals不重写hashCode是最常见Bug

📝 S2: String 常量池陷阱

String常量池 = 公共布告栏
字面量 "hello" → 直接写在布告栏上,所有人看到的都是同一张
new String() → 抄一份到自己的本子上(堆内存),和布告栏上的不是同一张
intern() → 先看布告栏有没有,没有就贴上去;有就返回布告栏上的那张
String 常量池 vs 堆内存 📋 字符串常量池(方法区/元空间) "hello" "world" "java" 同一个字面量 → 同一个对象引用 📦 堆内存(new出来的) new String("hello") #1 new String("hello") #2 每次new → 新建一个对象! 🔍 经典面试题结果: String a = "hello"; String b = "hello"; → a==b ? ✅ true(常量池同一个) String c = new String("hello"); → a==c ? ❌ false(堆 vs 常量池)
String常量池:字面量共享 → 同一引用;new String() → 每次新建堆对象
StringPoolTrap.java
📁 src/main/java
📁 trap
EqualsHashCodeTrap.java
StringPoolTrap.java
GenericTrap.java
// 🕳️ 陷阱1: == vs equals String s1 = "hello"; // 常量池 String s2 = "hello"; // 常量池(复用s1) String s3 = new String("hello"); // 堆内存新对象 s1 == s2; // ✅ true(常量池同一引用) s1 == s3; // ❌ false(堆 vs 常量池) s1.equals(s3); // ✅ true(内容相同) // 🕳️ 陷阱2: 编译期优化 vs 运行期拼接 String a = "hel" + "lo"; // ✅ 编译期优化 → "hello" → 常量池 String b = "hel"; String c = b + "lo"; // ❌ 运行期拼接 → 堆内存新对象 a == "hello"; // ✅ true c == "hello"; // ❌ false // 🕳️ 陷阱3: intern()方法 String s4 = new String("hello"); String s5 = s4.intern(); // 查常量池 → 有就返回池中的 s1 == s5; // ✅ true → s5指向常量池中的s1 // ⚠️ JDK 7+ 字符串常量池从方法区移到了堆中 // ⚠️ new String("hello") 实际创建了2个对象: // ① 常量池中的"hello"(如果不存在) // ② 堆中的String对象

🚨 S3: Java异常体系 — checked vs unchecked

异常体系 = 医院分诊
Throwable = 所有病人
  ├ Error = 急诊(ICU级别,你处理不了:OOM、StackOverflow)
  └ Exception = 门诊
    ├ Checked = 必须挂号(编译器强制你处理:IOException、SQLException)
    └ Unchecked(RuntimeException) = 可以不挂号(空指针、越界、算术异常)
Java 异常体系结构 Throwable Error(不处理) Exception OutOfMemoryError StackOverflowError Checked(必须处理) Unchecked(可忽略) IOException SQLException NPE IndexOutOfBounds Checked = 编译器强制try-catch/throws | Unchecked = RuntimeException子类,编译器不强制 | @Transactional默认只回滚Unchecked
Java异常体系:Error(不处理) / Checked(必须处理) / Unchecked(可忽略)

异常陷阱:@Transactional只回滚RuntimeException

@Transactional 默认只回滚 RuntimeException和Error,不回滚Checked Exception!
如果方法抛出 IOException(Checked),事务不会回滚
修复@Transactional(rollbackFor = Exception.class) → 所有异常都回滚

🔮 S4: 泛型类型擦除陷阱

泛型 = 透明收纳盒上的标签
编译期:盒子上贴了"只放书籍"的标签 → 编译器帮你检查,放衣服进去报错
运行期:标签被撕掉了(类型擦除)→ 运行时所有盒子都变成Object,不再区分

所以List<String>List<Integer> 在运行时是同一个类(List)!
不能 new T()、不能 instanceof List<String>、不能创建泛型数组
泛型类型擦除过程 📝 编译期(有泛型) List<String> list = new ArrayList<>(); list.add(123); ❌ 编译报错! String s = list.get(0); ✅ 自动转型 ⚡ 运行期(擦除后) List list = new ArrayList(); list.add(123); ✅ 能放进去! String s = (String)list.get(0); 💥 ClassCastException! ⚠️ 类型擦除的6大限制: ① 不能new T() ② 不能instanceof T ③ 不能new T[] ④ 不能 = new Generic<T>() ⑤ 基本类型不能用泛型 ⑥ 不能创建泛型异常类
泛型擦除:编译期有类型检查,运行期全部变成Object
GenericTrap.java
📁 src/main/java
📁 trap
EqualsHashCodeTrap.java
StringPoolTrap.java
GenericTrap.java
// 🕳️ 陷阱1: 泛型不参与运行时类型检查 List<String> strList = new ArrayList<>(); List<Integer> intList = new ArrayList<>(); System.out.println(strList.getClass() == intList.getClass()); // ✅ true!运行时都是ArrayList,泛型被擦除了 // 🕳️ 陷阱2: 通过反射可以绕过泛型检查 List<String> list = new ArrayList<>(); list.add("hello"); Method add = List.class.getMethod("add", Object.class); add.invoke(list, 123); // ✅ 反射能加Integer进去! // → 因为擦除后add(Object),不检查泛型 // 🕳️ 陷阱3: 泛型数组的坑 List<String>[] array = new ArrayList<String>[10]; // ❌ 编译报错! // 原因:如果允许,可以往里放ArrayList<Integer>,类型安全被破坏 // ✅ 正确做法:用List代替数组 List<List<String>> outer = new ArrayList<>();

📦 S5: 自动装箱拆箱陷阱

装箱/拆箱 = 纸箱打包
int = 散装零件(直接在栈上,快)
Integer = 纸箱包装的零件(堆上,多了箱子开销)
自动装箱 = 自动把零件装进箱子(int→Integer)
自动拆箱 = 自动把零件从箱子取出(Integer→int)

陷阱:两个"看起来一样"的纸箱,用==比较 → 比的是箱子地址不是内容!

Integer 缓存池陷阱

Integer在-128~127之间有缓存!用==比较:
• Integer a = 127; Integer b = 127; → a==b true(缓存同一对象)
• Integer c = 128; Integer d = 128; → c==d false(超出缓存,新建对象)
• Integer e = new Integer(127); → e==a false(new强制创建新对象,不走缓存)
永远用equals()比较Integer!不要用==,除非你确定在-128~127范围内且不是new出来的。
BoxingTrap.java
📁 src/main/java
📁 trap
GenericTrap.java
BoxingTrap.java
MoreTraps.java
// 🕳️ 陷阱1: Integer缓存池 Integer a = 127, b = 127; System.out.println(a == b); // ✅ true(缓存范围内) Integer c = 128, d = 128; System.out.println(c == d); // ❌ false(超出缓存) // Integer.valueOf(128) 每次创建新对象! // 🕳️ 陷阱2: 自动拆箱的NPE Integer num = null; int value = num; // 💥 NullPointerException! // 自动拆箱:num.intValue() → null.intValue() → NPE // 🕳️ 陷阱3: 三元运算符类型转换 Boolean flag = null; String result = flag ? "yes" : "no"; // 💥 NPE!flag自动拆箱为boolean → null.booleanValue() // ✅ 安全写法 String safe = Boolean.TRUE.equals(flag) ? "yes" : "no"; // 💡 Integer缓存范围:-128 ~ 127(可配置:-XX:AutoBoxCacheMax=200) // 💡 其他缓存:Boolean.TRUE/FALSE, Byte全部, Character(0~127), Short(-128~127), Long(-128~127)

🧩 S6: 更多常见陷阱

🕳️ 陷阱合集

float f = 3.4; 编译报错?
3.4 默认是double类型!double→float是窄化转换,需要强转或加f后缀:
float f = 3.4; → 编译错误:incompatible types
float f = 3.4f;
float f = (float)3.4;

记住:Java中所有小数字面量默认是double,整数默认是int。
switch能判断null吗?
不能!switch如果是枚举或包装类型,先自动拆箱,如果值是null → NPE

switch(status) { ... } → status为null时炸
✅ 先判空:if (status == null) return; switch(status) { ... }
ArrayList的subList修改会影响原List吗?
会!subList返回的是原List的视图,不是副本!

List<String> sub = list.subList(0, 3);
① 修改sub → list也变了!
② 修改list的结构(增删)→ 再操作sub → ConcurrentModificationException

安全做法List<String> copy = new ArrayList<>(list.subList(0, 3));
为什么不能用float/double做金额计算?
精度丢失!float和double是二进制浮点数,不能精确表示0.1:
0.1 + 0.2 = 0.30000000000000004 ≠ 0.3

解决方案
BigDecimal(推荐✅):new BigDecimal("0.1").add(new BigDecimal("0.2")) → 0.3 精确
② 整数分:long amount = 100; → 1元=100分,全程用整数运算

⚠️ new BigDecimal(0.1) 也不精确!必须用字符串构造new BigDecimal("0.1")
Arrays.asList()返回的List能add吗?
不能!Arrays.asList()返回的是Arrays内部的固定大小List,不是java.util.ArrayList!
调用add/remove → UnsupportedOperationException

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
用真正的ArrayList包装一下,就可以增删了。
try-finally中的return怎么执行?
finally始终执行,且覆盖try中的return值!

int test() { try { return 1; } finally { return 2; } } → 返回2!

int test() { int i = 1; try { return i; } finally { i = 2; } } → 返回1!(return时已经缓存了值)

最佳实践:不要在finally中return!只做资源释放。
short s = 1; s = s + 1; 编译报错?
是的!s+1的结果是int(自动提升),int→short需要强转:
s = s + 1; → 编译错误:incompatible types
s = (short)(s + 1);
s += 1; → 编译器自动帮你强转(复合赋值运算符的隐藏特性)

记住:所有比int小的类型(byte/short/char)运算时都会自动提升为int!
i++是线程安全的吗?
不是!i++不是原子操作,分三步:读→+1→写回。多线程同时i++会丢数据。

即使i是volatile也不行——volatile只保证可见性,不保证原子性。

解决方案
① AtomicInteger:atomicI.incrementAndGet();
② synchronized:synchronized(lock) { i++; }
③ LongAdder(高并发计数):adder.increment();

📋 Java基础陷阱速查表

陷阱错误写法正确做法
equals/hashCode只重写equals必须一起重写
String ==用==比较字符串用equals()
Integer ==用==比较Integer用equals()
自动拆箱NPEInteger num=null; int i=num;先判空再拆箱
Checked异常@Transactional不配rollbackForrollbackFor=Exception.class
泛型擦除运行时检查泛型类型用Class<T>传参
float字面量float f = 3.4;float f = 3.4f;
金额计算double计算金额BigDecimal或整数分
Arrays.asList直接add/removenew ArrayList<>(Arrays.asList(...))
subList修改原List后操作subList创建副本new ArrayList(subList)

— 全栈资料库 · Java基础陷阱图解 —

核心三原则速记

Java基础陷阱三原则:① equals/hashCode 必须同时重写,HashSet/HashMap 才能正常工作 → ② String 不可变 + 常量池,intern() 慎用 → ③ 受检异常用于可恢复、非受检异常用于编程错误