Appearance
常见坑
开发中的常见陷阱与避坑指南。每个坑点都包含:问题现象、原因分析、错误示例和正确做法。
一、基础类型陷阱
1. Integer 缓存问题
现象:用 == 比较两个 Integer 时,有时为 true 有时为 false。
原因:Integer 在 -128 到 127 范围内使用缓存(IntegerCache),范围内 == 比较是 true,超出范围则是 false。
java
// ❌ 错误
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true -- 在缓存范围内
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false -- 超出缓存范围,是两个不同对象
// ✅ 正确:始终用 equals 比较包装类型
System.out.println(a.equals(b)); // true
System.out.println(c.equals(d)); // true
// 或者拆箱后比较
System.out.println(c.intValue() == d.intValue()); // true2. BigDecimal 使用 double 构造器
现象:new BigDecimal(0.1) 的结果不是精确的 0.1。
原因:double 本身无法精确表示 0.1(二进制浮点数精度问题),BigDecimal(double) 构造函数传入的就是不精确的值。
java
// ❌ 错误
BigDecimal price = new BigDecimal(0.1);
System.out.println(price);
// 输出: 0.1000000000000000055511151231257827021181583404541015625
// ❌ 同样错误
BigDecimal price2 = BigDecimal.valueOf(0.1 + 0.2);
System.out.println(price2);
// 输出: 0.30000000000000004
// ✅ 正确:使用字符串构造函数
BigDecimal price = new BigDecimal("0.1");
System.out.println(price); // 0.1
// ✅ 正确:使用 valueOf(内部调用 toString)
BigDecimal price2 = BigDecimal.valueOf(0.1); // JDK 把参数转为 String
System.out.println(price2); // 0.1
// ✅ 计算正确
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
BigDecimal result = a.add(b);
System.out.println(result); // 0.33. 自动装箱引发的 NPE
现象:看似正常的代码抛出 NullPointerException。
原因:自动拆箱时,如果包装类型为 null,intValue() 调用会抛出 NPE。
java
// ❌ 错误
public int getDiscount(Integer level) {
// 如果 level 为 null,自动拆箱抛出 NPE
return level > 3 ? 20 : 10;
}
// ❌ 错误
Map<String, Integer> map = new HashMap<>();
int count = map.get("key"); // get 返回 null,拆箱 NPE
// ✅ 正确:判空处理
public int getDiscount(Integer level) {
if (level == null) {
return 0; // 或抛出有意义的异常
}
return level > 3 ? 20 : 10;
}
// ✅ 正确:使用 Optional
Map<String, Integer> map = new HashMap<>();
int count = Optional.ofNullable(map.get("key")).orElse(0);4. switch 中 null 值
java
// ❌ 错误 -- NPE!
String type = null;
switch (type) { // switch 会调用 hashCode(),触发 NPE
case "A":
break;
default:
break;
}
// ✅ 正确:先判空
if (type == null) {
// 处理 null
} else {
switch (type) {
case "A":
break;
}
}二、集合陷阱
5. ArrayList 遍历时删除(ConcurrentModificationException)
现象:在 for-each 循环中直接 list.remove() 抛出 ConcurrentModificationException。
原因:for-each 本质是迭代器遍历,list.remove() 修改了 modCount,迭代器 next() 时检测到不一致。
java
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
// ❌ 错误 -- ConcurrentModificationException
for (String item : list) {
if ("b".equals(item)) {
list.remove(item);
}
}
// ✅ 正确方式一:使用迭代器
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
if ("b".equals(iterator.next())) {
iterator.remove();
}
}
// ✅ 正确方式二:使用 removeIf(JDK 8+)
list.removeIf(item -> "b".equals(item));
// ✅ 正确方式三:倒序遍历(仅适用于 ArrayList)
for (int i = list.size() - 1; i >= 0; i--) {
if ("b".equals(list.get(i))) {
list.remove(i);
}
}6. Arrays.asList 的不可变性
现象:Arrays.asList() 返回的列表不能增删元素。
原因:Arrays.asList() 返回的是 Arrays 内部类 ArrayList,不是 java.util.ArrayList,它继承自 AbstractList,没有实现 add/remove 方法。
java
// ❌ 错误 -- UnsupportedOperationException
List<String> list = Arrays.asList("a", "b", "c");
list.add("d"); // 异常!
list.remove(0); // 异常!
// 更隐蔽的问题:修改原数组会影响列表
String[] arr = {"a", "b", "c"};
List<String> list = Arrays.asList(arr);
arr[0] = "x";
System.out.println(list); // [x, b, c] -- 列表也被修改了!
// ✅ 正确:包装为真正的 ArrayList
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
list.add("d"); // 正常7. List.subList 的陷阱
现象:修改 subList 或原列表,另一个也被修改。
原因:subList() 返回的是原列表的视图,不是独立拷贝。
java
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d", "e"));
// ❌ 错误:修改 subList 影响原列表
List<String> sub = list.subList(0, 3);
sub.clear();
System.out.println(list); // [d, e] -- 前三个元素也被删了
// ❌ 错误:修改原列表后 subList 不可用
List<String> sub2 = list.subList(0, 2);
list.add("f"); // 原列表结构修改
sub2.get(0); // ConcurrentModificationException
// ✅ 正确:创建独立副本
List<String> sub = new ArrayList<>(list.subList(0, 3));8. HashMap 在 JDK 7 中的死循环
现象:多线程并发 resize 时,HashMap 可能出现死循环(CPU 100%)。
原因:JDK 7 HashMap 扩容时采用头插法,并发环境下链表可能形成环。
java
// ❌ 多线程下使用 HashMap 可能导致死循环(JDK 7)
// 或数据丢失(JDK 8)
Map<String, String> map = new HashMap<>();
// ✅ 正确:使用 ConcurrentHashMap
Map<String, String> map = new ConcurrentHashMap<>();
// 或者使用 Collections.synchronizedMap
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());三、异常处理陷阱
9. finally 块中的 return
现象:finally 中有 return 语句,try 中的 return 被覆盖。
原因:finally 块在 return 语句执行前运行,finally 中的 return 会覆盖 try 中的返回值。
java
// ❌ 错误:finally 中的 return 覆盖了 try 中的返回值
public int getValue() {
try {
return 1;
} finally {
return 2; // 实际返回 2!
}
}
// ❌ 错误:finally 中 return 吞掉了异常
public int getValue() {
try {
throw new RuntimeException("error");
} finally {
return 0; // 异常被吞掉,返回 0
}
}
// ✅ 正确:finally 中只做清理,不 return
public int getValue() {
try {
return computeValue();
} finally {
cleanup();
}
}10. 异常被吞掉
java
// ❌ 错误:空 catch 或只打印
try {
doSomething();
} catch (Exception e) {
// 什么都不做 -- 异常被吞掉
}
try {
doSomething();
} catch (Exception e) {
e.printStackTrace(); // 只打印不处理,生产环境找不到
}
// ✅ 正确:记录日志并传播
try {
doSomething();
} catch (Exception e) {
log.error("操作失败,userId={}", userId, e);
throw new ServiceException("操作失败", e);
}四、线程安全陷阱
11. SimpleDateFormat 线程不安全
现象:多线程共享 SimpleDateFormat 导致解析错误或异常。
原因:SimpleDateFormat 内部维护了 Calendar 对象作为共享状态,并发修改导致数据错乱。
java
// ❌ 错误:多线程共享 SimpleDateFormat
public static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd");
// 多线程调用 SDF.parse() 或 SDF.format() 可能出错
// ✅ 正确方式一:使用 ThreadLocal
private static final ThreadLocal<DateFormat> DATE_FORMAT =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static String format(Date date) {
return DATE_FORMAT.get().format(date);
}
// ✅ 正确方式二:使用 DateTimeFormatter(JDK 8+,线程安全)
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
public static String format(LocalDate date) {
return date.format(FORMATTER);
}
// ✅ 正确方式三:每次新建实例(不推荐高频场景)
public static String format(Date date) {
return new SimpleDateFormat("yyyy-MM-dd").format(date);
}12. 双重检查锁定的指令重排
现象:DCL 单例模式中,instance 可能被分配了内存但未初始化就被使用。
原因:new Singleton() 不是原子操作(分配内存 -> 执行构造 -> 赋值引用),指令重排可能导致引用先赋值后初始化。
java
// ❌ 错误:缺少 volatile
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 可能指令重排
}
}
}
return instance;
}
}
// ✅ 正确:加上 volatile
public class Singleton {
private static volatile Singleton instance; // volatile 禁止指令重排
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}五、Stream 与函数式编程陷阱
13. Stream 不可重复使用
现象:Stream 第二次使用抛出 IllegalStateException。
原因:Stream 是流式、一次性的,操作后就被关闭了。
java
// ❌ 错误
Stream<String> stream = list.stream();
long count = stream.count(); // 第一次消费
stream.forEach(System.out::println); // 异常:stream has already been operated upon or closed
// ✅ 正确:重新创建 Stream
long count = list.stream().count();
list.stream().forEach(System.out::println);
// ✅ 正确:使用 Supplier 包装
Supplier<Stream<String>> streamSupplier = list::stream;
long count = streamSupplier.get().count();
streamSupplier.get().forEach(System.out::println);14. Stream 中的变量必须是 effectively final
java
// ❌ 错误:Lambda 中修改外部变量
int sum = 0;
list.forEach(item -> sum += item); // 编译错误
// ✅ 正确:使用 reduce 或收集器
int sum = list.stream().mapToInt(Integer::intValue).sum();
// ✅ 正确:使用原子类(不推荐,违反函数式编程原则)
AtomicInteger sum = new AtomicInteger(0);
list.forEach(item -> sum.addAndGet(item));15. parallelStream 的线程池问题
现象:所有 parallelStream 共享同一个 ForkJoinPool(默认线程数为 CPU 核心数 - 1),可能导致阻塞。
java
// ❌ 隐患:parallelStream 中调用阻塞操作,可能耗尽线程池
list.parallelStream().forEach(item -> {
// 这是一个阻塞的网络调用
httpClient.get("http://api.example.com/" + item);
});
// ✅ 正确:自定义线程池
ForkJoinPool customPool = new ForkJoinPool(10);
customPool.submit(() ->
list.parallelStream().forEach(item -> {
httpClient.get("http://api.example.com/" + item);
})
).get();
// ✅ 或在非阻塞场景下使用 parallelStream(CPU 密集型)
list.parallelStream()
.map(this::computeIntensive) // 纯计算操作
.collect(Collectors.toList());六、资源与连接陷阱
16. 资源没有关闭(连接泄漏)
java
// ❌ 错误:资源未关闭,可能耗尽连接池
public void readFile(String path) {
FileInputStream fis = new FileInputStream(path);
// 使用 fis
// 忘记关闭
}
// ❌ 错误:虽然加了 finally 但也很繁琐且容易出错
FileInputStream fis = null;
try {
fis = new FileInputStream(path);
// 使用 fis
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// ✅ 正确:try-with-resources
try (FileInputStream fis = new FileInputStream(path);
BufferedInputStream bis = new BufferedInputStream(fis)) {
// 使用 bis
} catch (IOException e) {
log.error("读取文件失败: {}", path, e);
}
// JDBC、HTTP 连接等也同理17. 数据库连接未归还
java
// ❌ 错误:连接未关闭
public List<User> queryUsers() {
Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users");
ResultSet rs = stmt.executeQuery();
// 处理结果
return users;
// 连接、语句、结果集全都没关闭!
}
// ✅ 正确:try-with-resources
public List<User> queryUsers() throws SQLException {
String sql = "SELECT * FROM users";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
List<User> users = new ArrayList<>();
while (rs.next()) {
users.add(mapRow(rs));
}
return users;
}
}七、其他常见陷阱
18. BigDecimal 的 equals vs compareTo
java
// ❌ 错误:BigDecimal 的 equals 比较精度
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
System.out.println(a.equals(b)); // false -- 精度不同!
// ✅ 正确:用 compareTo
System.out.println(a.compareTo(b) == 0); // true -- 数值相等19. 字符串拼接性能问题
java
// ❌ 错误:循环中 String 拼接
String result = "";
for (int i = 0; i < 10000; i++) {
result += "item" + i; // 每次循环创建新 String 对象
}
// ✅ 正确:StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append("item").append(i);
}
String result = sb.toString();20. 浮点数比较
java
// ❌ 错误:直接用 == 比较浮点数
double a = 0.1 + 0.2;
System.out.println(a == 0.3); // false!
// ✅ 正确:用 BigDecimal 或误差范围
double epsilon = 0.0000001;
System.out.println(Math.abs(a - 0.3) < epsilon); // true
// ✅ 正确:BigDecimal
BigDecimal result = new BigDecimal("0.1").add(new BigDecimal("0.2"));
System.out.println(result.compareTo(new BigDecimal("0.3")) == 0); // true避坑速查表
| 序号 | 陷阱 | 关键点 | 解决方案 |
|---|---|---|---|
| 1 | Integer 缓存 | -128~127 内 == 为 true | 用 equals() |
| 2 | BigDecimal double 构造 | 精度丢失 | 用 String 构造器 |
| 3 | 自动拆箱 NPE | null 拆箱抛异常 | 判空或 Optional |
| 4 | switch null | 调用 hashCode() NPE | 先判空 |
| 5 | 遍历时删除 | ConcurrentModificationException | 迭代器或 removeIf |
| 6 | Arrays.asList | 不可增删,与原数组关联 | new ArrayList<>() 包装 |
| 7 | subList | 视图而非拷贝 | new ArrayList<>(list.subList) |
| 8 | HashMap 并发 | JDK 7 死循环,JDK 8 数据丢失 | ConcurrentHashMap |
| 9 | finally return | 覆盖 try 返回值、吞异常 | finally 只做清理 |
| 10 | 吞异常 | 空 catch 或只 printStackTrace | 记录日志并传播 |
| 11 | SimpleDateFormat | 线程不安全 | ThreadLocal 或 DateTimeFormatter |
| 12 | DCL 指令重排 | instance 未初始化 | volatile 修饰 |
| 13 | Stream 重用 | 一次性消费 | 重新创建 Stream |
| 14 | Lambda 变量 | 必须 effectively final | 用 reduce/collect |
| 15 | parallelStream 线程池 | 共享 ForkJoinPool | 自定义线程池 |
| 16 | 资源泄漏 | IO/连接未关闭 | try-with-resources |
| 17 | 连接泄漏 | 数据库连接未归还 | try-with-resources |
| 18 | BigDecimal equals | 比较精度 | 用 compareTo |
| 19 | 循环字符串拼接 | 大量临时对象 | StringBuilder |
| 20 | 浮点数比较 | 精度误差 | BigDecimal 或误差范围 |
面试常见问题
Q1: 为什么 SimpleDateFormat 不是线程安全的?
A: SimpleDateFormat 继承自 DateFormat,内部维护了 Calendar 对象作为成员变量。在 format/parse 过程中,都会修改这个共享的 Calendar 状态。多线程并发调用时,线程 A 的设置可能被线程 B 覆盖,导致解析结果错乱或抛出异常。JDK 8 的 DateTimeFormatter 是不可变对象,天生线程安全。
Q2: JDK 7 HashMap 为什么会形成死循环?
A: JDK 7 HashMap 扩容时采用头插法(将旧链表元素逐个插入新桶的头部),在并发环境下,两个线程同时扩容时,可能导致链表中的节点互相引用形成环。当调用 get() 查找不存在的 key 时,会陷入死循环造成 CPU 100%。JDK 8 改为尾插法,避免了死循环,但并发下仍可能出现数据丢失。
Q3: BigDecimal 的 equals 和 compareTo 有什么区别?
A: equals 同时比较数值和精度(scale),如 1.0 和 1.00 的 precision 不同,equals 返回 false。compareTo 只比较数值大小,忽略精度差异,1.0 和 1.00 比较返回 0。业务场景中的金额比较应使用 compareTo。
Q4: 为什么循环中不应该用 + 拼接字符串?
A: String 是不可变对象,每次 + 拼接都会创建新的 String 对象和 StringBuilder 对象。循环 10000 次意味着创建 10000 个临时对象,导致大量 GC 和性能下降。应使用 StringBuilder(非线程安全)或 StringBuffer(线程安全)在循环外声明,循环内 append。
Q5: Stream 为什么不能重复消费?
A: Stream 设计为流式操作,类似于迭代器,只能遍历一次。Stream 的终端操作(如 count、collect、forEach)会遍历数据源并关闭流,再次使用会抛出 IllegalStateException。这是 Stream 的惰性求值和短路机制决定的,如果需要多次使用,应重新创建 Stream。
