根據 SonarQube 統計,NullPointerException (NPE) 佔所有運行時異常的 30% 以上。Tony Hoare 稱發明 null 為「十億美元錯誤」。建構完整防禦體系,需從語言特性、工具鏈、編碼規範、架構設計 四個維度入手。
// ❌ 危險:unknownObject 可能為 null
if (unknownObject.equals("expected")) { ... }
// ✅ 安全:常量不為 null
if ("expected".equals(unknownObject)) { ... }
// ✅ Java 7+ Objects.equals (雙向安全)
if (Objects.equals(unknownObject, "expected")) { ... }
BigDecimal price = getPrice();
// ❌ 危險:price 為 null 拋 NPE
String s1 = price.toString();
// ✅ 安全:返回 "null" 字符串
String s2 = String.valueOf(price);
// ✅ 更好:自定義默認值
String s3 = Optional.ofNullable(price).map(Object::toString).orElse("0.00");
// StringUtils - 所有方法 null-safe
StringUtils.isEmpty(null); // true
StringUtils.isBlank(null); // true
StringUtils.isNumeric(null); // false
StringUtils.trimToNull(" "); // null
StringUtils.trimToEmpty(null); // ""
StringUtils.defaultIfBlank(str, "默認值");
// CollectionUtils
CollectionUtils.isEmpty(list); // null 或 size=0 返回 true
CollectionUtils.isNotEmpty(list); // 非空且有元素
// ObjectUtils
ObjectUtils.defaultIfNull(obj, defaultVal);
ObjectUtils.allNotNull(obj1, obj2, ...);
// ❌ 反模式:調用方需判空
public List getOrders(User user) {
if (user.isVip()) return orderDao.findByUser(user);
return null;
}
// ✅ 最佳實踐:返回不可變空集合
public List getOrders(User user) {
if (!user.isVip()) return Collections.emptyList(); // 單例,零開銷
return orderDao.findByUser(user);
}
// ✅ 或返回流 (Java 8+)
public Stream getOrderStream(User user) {
return user.isVip() ? orderDao.findByUser(user).stream() : Stream.empty();
}
// 依賴: com.google.code.findbugs:jsr305:3.0.2
// 或: org.jetbrains:annotations:23.0.0 (IntelliJ 內建)
public class UserService {
// 參數非空契約
public void updateProfile(@NonNull User user, @Nullable String bio) {
// IDE/編譯器/FindBugs 會在調用處檢查
}
// 返回值非空契約
@NonNull
public User findById(@NonNull Long id) {
return userDao.findById(id).orElseThrow(() -> new NotFoundException("用戶不存在"));
}
}
工具鏈支持:IntelliJ IDEA 實時檢測、FindBugs/SpotBugs 靜態分析、Error Prone 編譯期檢查、NullAway (Uber) 編譯期空檢查。
// ❌ 反模式:可變、不完整對象
public class User {
private Long id;
private String name;
// 無參構造器 + setter → 可創建 name 為 null 的對象
}
// ✅ 契約式:構造器強制要求必要字段
public class User {
private final Long id;
private final String name; // 必填
private final String phone; // 選填
public User(Long id, String name) {
this(id, name, null);
}
public User(Long id, String name, String phone) {
this.id = Objects.requireNonNull(id, "ID 不能為空");
this.name = Objects.requireNonNull(name, "姓名不能為空");
this.phone = phone; // 可為 null
}
// 無 setter,不可變
}
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(50) NOT NULL, -- 強制非空
phone VARCHAR(20) DEFAULT '', -- 空字符串代替 null
email VARCHAR(100) NOT NULL UNIQUE, -- 非空 + 唯一
status TINYINT NOT NULL DEFAULT 1, -- 默認值
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
效果:數據庫成為最後一道防線,Java 代碼可大幅減少非空判斷,信任數據完整性。
// 接口
interface DiscountStrategy {
BigDecimal apply(BigDecimal price);
}
// 實際策略
class VipDiscount implements DiscountStrategy {
public BigDecimal apply(BigDecimal price) { return price.multiply(new BigDecimal("0.8")); }
}
// 空對象:無折扣
class NoDiscount implements DiscountStrategy {
public static final NoDiscount INSTANCE = new NoDiscount();
private NoDiscount() {}
public BigDecimal apply(BigDecimal price) { return price; }
}
// 使用:無需判斷 strategy 是否為 null
public class OrderService {
public BigDecimal calculateFinalPrice(Order order) {
DiscountStrategy strategy = discountFactory.getStrategy(order.getUserLevel());
// strategy 永不為 null,即使是 NoDiscount.INSTANCE
return strategy.apply(order.getOriginalPrice());
}
}
| 場景 | 推薦方案 | 避免方案 |
|---|---|---|
| 方法參數 | @NonNull + Objects.requireNonNull | 手動 if-null-throw |
| 返回值 | @NonNull + Optional (僅作為返回類型) | 返回 null |
| 集合/數組 | Collections.emptyList() / new int[0] | 返回 null |
| 字符串比較 | "constant".equals(var) / Objects.equals | var.equals("constant") |
| 對象轉字符串 | String.valueOf(obj) | obj.toString() |
| 鏈式調用 | Optional.ofNullable(obj).map(...).orElse(default) | 層層 if-null |
| DTO/實體 | Record / Lombok @Value / Builder 模式 | 無參構造 + setter |
org.apache.maven.plugins
maven-compiler-plugin
org.jetbrains
annotations
23.0.0
-Xlint:null
com.github.spotbugs
spotbugs-maven-plugin
4.8.0.0
Max
Low
spotbugs-include.xml
空指針防禦是系統工程,而非單點技巧。核心原則:
將這些實踐內化為團隊編碼規範,並通過工具鏈強制執行,才能從根本上消除 NPE 這一「十億美元錯誤」。