—— 从NullPointerException到集合操作,手把手教你绕过雷区
前言
Java作为一门“看似简单,实则暗藏玄机”的编程语言,每年吸引数百万开发者入坑。然而,许多新手在初期开发中反复掉入相同的陷阱,轻则调试到怀疑人生,重则引发线上事故。本文基于真实项目案例,系统梳理了Java开发者最常踩的10个深坑,并提供可落地的解决方案。无论你是刚入门的新手,还是希望查漏补缺的中级开发者,这篇文章都将成为你的“避坑宝典”。
一、空指针异常(NullPointerException)
1.1 问题现象
User user = getUserById(1001);
System.out.println(user.getName()); // user可能为null,触发NPE
1.2 根本原因
-
未初始化对象:直接使用未赋值的对象引用。
-
方法返回null:未对第三方接口或数据库查询结果进行判空。
-
链式调用:
user.getAddress().getCity()
中任一环节为null。
1.3 解决方案
方案一:防御性编程
if (user != null && user.getAddress() != null) {
System.out.println(user.getAddress().getCity());
}
方案二:Optional优雅处理
Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCity)
.ifPresent(System.out::println);
方案三:注解约束
public @NotNull User getUserById(@NonNull Integer id) {
// 方法明确声明不可返回null,参数不可为null
}
工具支持:
-
IDEA插件:
NullAway
实时检测潜在NPE -
静态分析:SpotBugs、Checkstyle集成到CI流程
二、字符串比较误区
2.1 == vs equals的经典陷阱
String s1 = "Java";
String s2 = new String("Java");
System.out.println(s1 == s2); // false(内存地址不同)
System.out.println(s1.equals(s2)); // true(内容相同)
2.2 intern方法的风险
String s3 = new String("Python").intern();
String s4 = "Python";
System.out.println(s3 == s4); // true(强制入池,但滥用会导致性能问题)
2.3 最佳实践
-
始终使用equals比较内容
-
避免频繁调用intern()
-
敏感场景使用枚举替代字符串
三、循环删除List元素
3.1 并发修改异常(ConcurrentModificationException)
错误示例:
List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3,4));
for (Integer num : list) {
if (num % 2 == 0) {
list.remove(num); // 抛出异常
}
}
3.2 正确删除方式
方式一:迭代器删除
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
if (it.next() % 2 == 0) {
it.remove(); // 安全删除
}
}
方式二:Java8 Stream过滤
list = list.stream()
.filter(num -> num % 2 != 0)
.collect(Collectors.toList());
方式三:CopyOnWriteArrayList(线程安全场景)
List<Integer> cowList = new CopyOnWriteArrayList<>(list);
for (Integer num : cowList) {
if (num % 2 == 0) {
cowList.remove(num);
}
}
四、资源未关闭导致内存泄漏
4.1 典型场景
FileInputStream fis = new FileInputStream("data.txt");
// 忘记调用fis.close(),资源句柄泄漏
4.2 自动关闭的正确姿势
try-with-resources语法(Java7+)
try (FileInputStream fis = new FileInputStream("data.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
// 自动关闭资源
}
自定义AutoCloseable实现
public class DatabaseConnection implements AutoCloseable {
@Override
public void close() {
// 释放数据库连接
}
}
五、日期时间处理的暗坑
5.1 SimpleDateFormat线程不安全
// 错误:多线程共享同一个SimpleDateFormat实例
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
解决方案:
-
ThreadLocal隔离
-
Java8 DateTimeFormatter(线程安全)
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate date = LocalDate.parse("2023-10-01", formatter);
5.2 时区忽略引发事故
Instant now = Instant.now(); // 默认UTC时间
ZonedDateTime shanghaiTime = now.atZone(ZoneId.of("Asia/Shanghai"));
六、equals与hashCode的契约
6.1 违反契约的后果
public class User {
private Integer id;
@Override
public boolean equals(Object o) {
// 仅根据id判断相等性
}
// 未重写hashCode,导致HashSet/HashMap行为异常
}
6.2 正确实现方式
@Override
public int hashCode() {
return Objects.hash(id); // 确保相同对象有相同hashCode
}
工具支持:
-
Lombok:
@EqualsAndHashCode
自动生成 -
IDE生成:IntelliJ右键Generate一键生成
七、浅拷贝与深拷贝混淆
7.1 浅拷贝陷阱
User user1 = new User("Alice");
User user2 = user1.clone();
user1.setName("Bob");
System.out.println(user2.getName()); // 输出"Bob"(浅拷贝共享引用)
7.2 实现深拷贝
方案一:序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(user1);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
User user2 = (User) new ObjectInputStream(bis).readObject();
方案二:工具库
User user2 = SerializationUtils.clone(user1); // Apache Commons Lang3
八、并发修改共享变量
8.1 非原子操作导致数据错乱
private int count = 0;
public void increment() {
count++; // 非原子操作
}
并发执行后count可能小于预期值
8.2 线程安全方案
方案一:Atomic原子类
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
方案二:synchronized同步
public synchronized void increment() {
count++;
}
方案三:LongAdder高性能计数
private LongAdder count = new LongAdder();
public void increment() {
count.increment();
}
九、异常处理不当
9.1 吞掉异常
try {
riskyOperation();
} catch (Exception e) {
// 空catch块,异常被“吞掉”
}
9.2 过度泛化的catch
try {
// 业务代码
} catch (Throwable t) { // 捕获所有异常,包括Error
// 可能导致JVM无法处理严重错误
}
9.3 最佳实践
-
按需捕获特定异常
-
记录完整堆栈信息
-
使用全局异常处理器兜底
十、静态方法滥用
10.1 测试难以Mock
public class OrderService {
public static boolean validate(Order order) {
// 静态方法难以被Mock框架替换
}
}
10.2 替代方案
策略模式改造
public interface Validator {
boolean validate(Order order);
}
public class OrderValidator implements Validator {
@Override
public boolean validate(Order order) { ... }
}
结语
避开这些“经典”深坑,你的Java开发之路将少走80%的弯路!记住:
-
防御性编程:永远不要信任外部输入
-
工具先行:IDE插件、静态分析工具是你的最佳战友
-
持续学习:关注Java新特性(如Records、Pattern Matching)
下一步行动:
-
将本文案例代码导入本地环境复现
-
在团队内部分享你的避坑经验
-
订阅Java技术博客(如Baeldung、InfoQ)