本文目录导读:

- 经典的
Integer缓存“==”陷阱 - 浮点数精度丢失(金融计算大忌)
- 字符串 与
equals混用 ArrayList删除元素时的“漏删”与“并发修改”- 自动拆箱引发的
NullPointerException SimpleDateFormat并发线程安全问题- 使用
substring和split导致的内存泄漏(JDK 6 时代) switch对null的处理ConcurrentHashMap的size()方法误导- 反射与 的比较(类型判断)
- 避坑三原则
Java 是一门“坑”非常多的语言,从值传递、字符串常量池、自动拆装箱到并发和集合类,每一个特性背后都藏着不少隐患。
下面我整理了 10 个高频且经典的避坑案例,每个都附带了现象、原因和解决方案:
经典的 Integer 缓存“==”陷阱
场景:比较两个数值是否相等,使用 却得到了错误结果。
public class IntegerTrap {
public static void main(String[] args) {
Integer a = 100;
Integer b = 100;
Integer c = 128;
Integer d = 128;
System.out.println(a == b); // true
System.out.println(c == d); // false (经典坑)
}
}
原因:Integer 在 -128 ~ 127 之间有缓存(IntegerCache),在这个范围内直接返回缓存对象;超出范围会 new 新对象, 比较的是内存地址。
避坑方案:永远使用 equals() 比较包装类型,除非你明确知道在缓存范围内。
浮点数精度丢失(金融计算大忌)
场景:计算 0.1 + 0.2,结果不是 0.3。
double a = 0.1; double b = 0.2; System.out.println(a + b); // 输出: 0.30000000000000004
原因:二进制无法精确表示十进制小数(IEEE 754 标准),浮点数本身是近似值。
避坑方案:涉及金额、利率、精确计算时,严禁使用 double/float,必须使用 BigDecimal,且优先使用字符串构造函数。
BigDecimal c = new BigDecimal("0.1");
BigDecimal d = new BigDecimal("0.2");
System.out.println(c.add(d)); // 正确输出: 0.3
字符串 与 equals 混用
场景:比较字符串内容时使用了 。
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s1 == s2); // false (一个在常量池,一个在堆)
System.out.println(s1.equals(s2)); // true (正确比较)
避坑方案:字符串内容比较一律使用 .equals(),不要用 (除非你是在校验内存引用是否唯一)。
ArrayList 删除元素时的“漏删”与“并发修改”
场景:循环删除满足条件的元素。
// 错误写法
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (int i = 0; i < list.size(); i++) {
String item = list.get(i);
if ("B".equals(item)) {
list.remove(i); // 删除后,后面的元素会前移,索引错位
}
}
原因:删除元素导致索引重排,会跳过下一个元素。
避坑方案:使用迭代器的 remove() 或 JDK 8+ 的 removeIf()。
list.removeIf("B"::equals); // 一行搞定,安全高效
自动拆箱引发的 NullPointerException
场景:包装类和基础类型混合运算,包装类型为 null。
public class UnboxingTrap {
static Integer total = null;
public static void main(String[] args) {
int result = total + 100; // 编译通过,但运行时报 NPE
}
}
原因:total 自动拆箱成 int 时,对 null 调用 intValue() 抛异常。
避坑方案:在使用包装类型参与运算前,必须判空,或者使用 Optional 进行流式处理。
SimpleDateFormat 并发线程安全问题
场景:多线程使用同一个 SimpleDateFormat 解析日期。
// 错误写法
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 多线程调用 sdf.parse(str) 会抛 NumberFormatException 或解析错误
原因:SimpleDateFormat 不是线程安全的,内部有 Calendar 共享状态。
避坑方案:
- 使用
DateTimeFormatter(JDK 8+,线程安全)。 - 或配合
ThreadLocal<SimpleDateFormat>使用。
使用 substring 和 split 导致的内存泄漏(JDK 6 时代)
场景(历史坑):截取字符串后,常驻内存。
- 在 JDK 6 中,
substring会保持原字符串的字符数组引用,如果你截取了一个很大的字符串的一小段,原大字符串无法被 GC 回收。 - 避坑方案:在生产环境升级到 JDK 7+(已修复);或者显式使用
new String(str.substring(...))切断引用。
switch 对 null 的处理
场景:对 null 进行 switch 判断。
String str = null;
switch (str) { // 这里直接 NPE!
case "a":
break;
}
原因:switch 在匹配前会调用 str.hashCode(),null 会抛 NPE。
避坑方案:switch 前提前判空;或者使用 if-else + Objects.equals()。
ConcurrentHashMap 的 size() 方法误导
场景:使用 ConcurrentHashMap 的 size() 获取准确数量。
原因:ConcurrentHashMap 是弱一致性的并发容器,如果写入并发很高,size() 返回的是近似值,不能用于精确的业务逻辑(比如判断是否只有一个人操作)。
避坑方案:需要严格精确时加锁或使用全局计数器,不要依赖并发容器的 size() 作为硬性判断条件。
反射与 的比较(类型判断)
场景:通过反射获取 Class,比较类型。
Class clazz = Integer.class; System.out.println(clazz == int.class); // false System.out.println(clazz == Integer.class); // true
原因:包装类型的 Integer.class(引用类型)与 int.class(原始类型)是不同的 Class 对象。
避坑方案:判断逻辑必须区分基础类型和包装类型,不能混用。
避坑三原则
作为一个 Java 开发者,要避免踩坑,请记住这三条铁律:
- 比较用方法:凡是比较,无论是字符串还是包装类,一律用
equals; 只用于比较原始数值和引用。 - 精度用 BigDecimal:凡是涉及钱、精确计算,别用浮点。
- 并发用专类:凡是多线程环境,别用
SimpleDateFormat,别手动加 synchronized 修复合集器,用java.util.concurrent包。
这些是作为 Java 开发最常踩的级别最高、修复成本最大的坑,掌握这些能有效减少日常线上 bug,如果你是新手,建议把案例 1、2、4 背下来,面试和笔试经常考。