别再写 `obj!= null`:资深开发者如何通过设计消除空值检查

初级开发者习惯防御式编程,在每个角落添加 if (obj != null);而资深开发者思考的是:为什么这里会出现 null? 问题不在于 null 本身,而在于将空值检查散布在代码各处的设计缺陷。

一、问题的本质:防御式编程的陷阱

看这段典型的“防御式”代码:

if (order != null && 
    order.getUser() != null && 
    order.getUser().getAddress() != null && 
    order.getUser().getAddress().getCity() != null) {
    String city = order.getUser().getAddress().getCity();
    process(city);
}

这种写法看似“安全”,实则暴露了三个深层问题:

问题说明后果
责任分散每个调用方都要检查 null重复代码 + 维护成本高
意图模糊null 代表“未初始化”、“不存在”还是“错误”?语义不清晰,易引发误判
设计缺陷对象图中存在大量可空引用违反“告诉,不要询问”原则

💡 关键洞察:资深开发者不把 null 视为“合法状态”,而是设计失败的信号。他们不思考“如何检查 null”,而是思考“如何让 null 不可能出现”。

二、四种替代 null 的设计策略

策略 1:Optional / Maybe 类型(显式表达可空性)

// ❌ 初级写法:返回 null 表示“未找到”
public User findUserById(Long id) {
    return userRepository.findById(id).orElse(null);
}

// ✅ 资深写法:用 Optional 显式表达“可能不存在”
public Optional<User> findUserById(Long id) {
    return userRepository.findById(id);
}

// 调用方必须显式处理“不存在”场景
findUserById(123L)
    .map(User::getEmail)
    .ifPresent(this::sendWelcomeEmail);

优势

  • 编译期强制处理“不存在”场景
  • 消除 NullPointerException 风险
  • 语义清晰:Optional.empty() 明确表示“值不存在”

📌 注意:Optional 不应作为字段类型或方法参数,仅用于返回值表示可空性。[[21]]

策略 2:Null Object 模式(用“空对象”替代 null)

当“不存在”是一种合法业务状态时,创建实现相同接口的空对象:

// 定义接口
public interface Logger {
    void log(String message);
    boolean isEnabled();
}

// 真实实现
public class FileLogger implements Logger {
    private final File file;
    
    public FileLogger(File file) {
        this.file = file;
    }
    
    @Override
    public void log(String message) {
        // 写入文件
    }
    
    @Override
    public boolean isEnabled() {
        return true;
    }
}

// Null Object:空实现
public class NullLogger implements Logger {
    @Override
    public void log(String message) {
        // 什么都不做
    }
    
    @Override
    public boolean isEnabled() {
        return false;
    }
    
    // 单例模式
    public static final NullLogger INSTANCE = new NullLogger();
}

使用场景

// 配置中可能没有指定日志文件
Logger logger = config.getLogFile() != null 
    ? new FileLogger(config.getLogFile()) 
    : NullLogger.INSTANCE;

// 调用方无需检查 null
logger.log("Application started"); // NullLogger 会静默忽略

优势

  • 消除所有 if (logger != null) 检查
  • 调用方代码更简洁
  • 符合“开闭原则”:新增空对象无需修改调用方

策略 3:契约式设计(前置条件 + 不变式)

通过设计约束确保关键对象永不为 null:

public class Order {
    private final User user; // final 确保初始化后不可变
    
    // 构造函数契约:user 不能为空
    public Order(User user, List<Item> items) {
        if (user == null) {
            throw new IllegalArgumentException("User cannot be null");
        }
        this.user = user;
        this.items = Objects.requireNonNull(items);
    }
    
    // Getter 永不返回 null
    public User getUser() {
        return user; // 无需检查 null
    }
}

配套实践

  • 使用 @NonNull 注解(Lombok/Checker Framework)
  • 静态分析工具(如 ErrorProne)在编译期捕获潜在 null
  • 构造函数/工厂方法强制验证

策略 4:领域建模(用类型系统表达状态)

将“存在/不存在”建模为显式状态,而非用 null 表示:

// ❌ 用 null 表示“未激活”
public class User {
    private Date activatedAt; // null = 未激活
    
    public boolean isActive() {
        return activatedAt != null;
    }
}

// ✅ 用状态机建模
public enum UserStatus {
    PENDING, ACTIVE, SUSPENDED, DELETED
}

public class User {
    private UserStatus status;
    
    public boolean isActive() {
        return status == UserStatus.ACTIVE;
    }
    
    // 状态转换显式可控
    public void activate() {
        if (status != UserStatus.PENDING) {
            throw new IllegalStateException("Cannot activate user in " + status + " state");
        }
        this.status = UserStatus.ACTIVE;
        this.activatedAt = LocalDateTime.now();
    }
}

优势

  • 状态转换受控,避免非法状态
  • 业务规则显式表达
  • 消除“魔法值”(如用 null 表示特殊状态)[[25]]

三、实战对比:订单处理场景

初级实现(防御式)

public void processOrder(Order order) {
    if (order == null) return;
    
    User user = order.getUser();
    if (user == null) return;
    
    Address address = user.getAddress();
    if (address == null) return;
    
    String city = address.getCity();
    if (city == null || city.isEmpty()) return;
    
    // 业务逻辑...
    shippingService.calculateFee(city);
}

资深实现(设计驱动)

// 1. 订单必须关联用户(构造函数契约)
public class Order {
    private final User user; // final + 非空
    
    public Order(User user, /*...*/) {
        this.user = Objects.requireNonNull(user);
    }
    
    public User getUser() {
        return user; // 永不为 null
    }
}

// 2. 用户必须有地址(领域约束)
public class User {
    private final Address address; // 业务规则:注册时必须填写地址
    
    public Address getAddress() {
        return address; // 永不为 null
    }
}

// 3. 地址字段非空(值对象约束)
@Value // Lombok 生成不可变类
public class Address {
    @NonNull String street;
    @NonNull String city;   // city 永不为 null
    @NonNull String zipCode;
}

// 4. 调用方代码极度简化
public void processOrder(Order order) {
    // 无需任何 null 检查!
    String city = order.getUser().getAddress().getCity();
    shippingService.calculateFee(city);
}

四、何时可以接受 null?

并非所有场景都要消灭 null。以下情况可谨慎使用:

场景建议理由
性能敏感的底层代码可接受 null避免对象分配开销
与外部系统交互用 Adapter 封装将 null 限制在边界层
临时占位仅限方法内部不作为返回值暴露给调用方
遗留系统集成用 Optional 包装逐步迁移,不扩大污染范围

⚠️ 黄金法则null 不应穿越模块边界。如果必须处理外部传入的 null,应在入口处立即转换为 Optional/Null Object,不让空值污染内部代码。[[30]]

五、迁移路线图

阶段 1:识别热点

# 使用 grep 查找高频 null 检查
grep -r "!= null" src/main/java | sort | uniq -c | sort -nr

阶段 2:边界防护

// 在系统边界(如 Controller)将 null 转换为 Optional
@GetMapping("/users/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
    return userService.findUserById(id) // Optional<User>
        .map(UserDto::from)
        .map(ResponseEntity::ok)
        .orElse(ResponseEntity.notFound().build());
}

阶段 3:重构核心领域

  • 将关键字段标记为 @NonNull
  • 用 Null Object 替代可空依赖
  • 用状态机替代 null 表示业务状态

阶段 4:建立规范

  • 代码审查 checklist:禁止在业务逻辑中出现 != null
  • 静态分析规则:禁止方法返回 null(除边界层)
  • 新人培训:强调“设计消除 null”而非“检查 null”

六、总结

维度初级开发者资深开发者
思维模式“如何检查 null?”“如何让 null 不可能出现?”
代码特征到处散布 if (x != null)零 null 检查(核心领域)
设计重点防御式编程契约式设计 + 类型安全
工具选择手动 null 检查Optional / Null Object / 状态机
责任归属调用方负责检查被调用方保证非空

💡 终极心法
不要写 != null,要写让 null 无法出现的设计。
空值检查是症状,不是解药。真正的解决方案是重新思考对象之间的关系、状态的表达方式,以及 API 的契约设计。

当你的代码中不再需要写 if (obj != null) 时,你就真正理解了“设计优于防御”的精髓。


0
0
0
0
评论
未登录
暂无评论