前言
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家:https://www.captainbed.cn/z
文章目录
1. 错误场景复现
场景1:未校验空值的直接调用
public void processOrder(User user) {
String address = user.getAddress().toUpperCase(); // 若user或getAddress()返回null则抛出NPE
System.out.println("配送地址:" + address);
}
初级开发者常假设方法调用链中的每个对象都已初始化,忽略外部传参或中间环节可能返回null
的情况。
场景2:自动拆箱引发的NPE
public void calculatePrice(Integer discount) {
int realDiscount = discount; // 若discount为null,自动拆箱时抛出NPE
// 业务逻辑...
}
包装类(如Integer
)的自动拆箱操作隐藏了空值风险,尤其在接收外部参数时。
场景3:返回null的“陷阱方法”
public List<Order> queryUserOrders(Long userId) {
// 若用户无订单则直接返回null
return orderDao.findByUserId(userId);
}
// 调用方代码
List<Order> orders = queryUserOrders(123L);
for (Order order : orders) { // 当orders为null时抛出NPE
// 处理订单
}
返回null
的方法会强制调用方处理空值,容易遗漏检查。
2. 原理解析
空指针异常的本质
- 当程序试图在
null
引用上执行方法调用(如obj.method()
)或字段访问(如obj.field
)时触发 - 根源:对象生命周期管理失控,未明确约束可为空的变量与不可为空的变量
Java的空值哲学缺陷
- 默认允许任何对象为
null
- 缺乏编译时强制检查机制(对比Kotlin的
Nullable
类型标注)
3. 正确解决方案
方案1:拥抱Optional(Java 8+)
// 改造返回null的方法
public Optional<List<Order>> queryUserOrders(Long userId) {
List<Order> orders = orderDao.findByUserId(userId);
return Optional.ofNullable(orders); // 包装可能为null的结果
}
// 调用方安全使用
queryUserOrders(123L)
.orElse(Collections.emptyList()) // 若为null则返回空列表
.forEach(order -> process(order));
Optional使用规范
- 不要用
Optional
作为方法参数(违反设计初衷) - 避免
Optional.get()
直接取值(优先使用orElse()
/orElseThrow()
)
方案2:防御性编程
// 场景1修复:添加空值检查
public void processOrder(User user) {
if (user == null || user.getAddress() == null) {
throw new IllegalArgumentException("用户或地址信息不完整");
}
String address = user.getAddress().toUpperCase();
// 或使用Apache Commons工具类
String address = StringUtils.defaultString(user.getAddress(), "").toUpperCase();
}
// 场景2修复:显式检查包装类
public void calculatePrice(Integer discount) {
int realDiscount = discount != null ? discount : 0;
}
方案3:永不返回null
// 返回空集合替代null
public List<Order> queryUserOrders(Long userId) {
List<Order> orders = orderDao.findByUserId(userId);
return orders != null ? orders : Collections.emptyList(); // 返回不可变空集合
}
// 返回空对象(Null Object模式)
public class EmptyUser extends User {
public EmptyUser() {
super(-1L, "未知用户");
}
}
public User findUserById(Long id) {
User user = userDao.findById(id);
return user != null ? user : new EmptyUser();
}
4. 工具与最佳实践
静态代码分析工具
- SpotBugs:检测可能抛出NPE的代码路径
- SonarQube规则:
Null pointers should not be dereferenced
- IntelliJ IDEA插件:
@NotNull
/@Nullable
注解支持
注解驱动空安全
// 使用JSR-305注解明确约束
public void updateProfile(@NotNull User user, @Nullable String memo) {
// 编译器或IDE会提示未做null检查
System.out.println(user.getName());
}
5. Code Review检查清单
检查项 | 正确做法 |
---|---|
方法是否可能返回null? | 优先返回空集合/空对象,或用Optional包装 |
调用对象方法前是否检查null? | 链式调用需逐级校验(如user.getAddress().getCity() ) |
是否滥用Optional.isPresent() ? | 用map() /flatMap() 替代命令式检查 |
是否处理了外部输入的空值? | 对参数做前置校验,使用Objects.requireNonNull() |
6. 真实案例
某金融系统在计算用户资产时,未处理用户未开户(account
字段为null
)的情况:
BigDecimal balance = user.getAccount().getBalance(); // 抛出NPE
后果:线上服务崩溃2小时,影响5万用户查询操作。
修复:
- 修改
getAccount()
方法返回Optional<Account>
- 调用方使用
user.getAccount().orElse(Account.EMPTY).getBalance()
总结
- 优先使用Optional:明确表达“可能无值”的语义
- 防御性编程:对输入参数和中间结果保持警惕
- 永不返回null:用空集合/空对象消除传染性null
- 工具辅助:静态分析 + 注解约束 = 编译时防护
下期预告:《集合框架的暗坑:ArrayList遍历时删除元素的灾难》——揭秘ConcurrentModificationException的成因与高并发场景下的终极解决方案。
联系作者
职场经验分享,Java面试,简历修改,求职辅导尽在科技泡泡
思维导图面试视频合集