【从零入门23种设计模式21】行为型之空对象模式

一、空对象模式核心定义

空对象模式是行为型设计模式的一种,核心目的是:

用一个 "空对象"(Null Object)来替代代码中的null值,这个空对象实现了与真实对象相同的接口,但不做任何实际业务操作,仅返回 "空" 的结果或默认行为。

简单来说:用一个有具体实现的 "空对象" 代替null,让代码无需频繁判断null,同时保证程序逻辑的完整性

核心解决的问题
  1. 避免空指针异常(NPE) :消除代码中大量的if (obj == null)判断,从根源上减少 NPE;
  2. 简化代码逻辑 :无需在每个调用点处理null,空对象会返回合理的默认值;
  3. 统一行为接口 :空对象与真实对象实现相同接口,调用方无需区分,符合里氏替换原则;
  4. 提升代码可读性 :用语义化的空对象(如NullUser、NullOrder)替代无意义的null,代码意图更清晰;
  5. 默认行为可控 :空对象可自定义默认行为(如返回空列表、0 值、提示信息),而非直接抛出异常。
生活类比
  • 场景 1 :用户查询
    • 真实对象:存在的用户(张三,ID:1001,余额:1000 元);
    • 空对象:不存在的用户(NullUser,ID:-1,余额:0 元,getName () 返回 "匿名用户");
    • 核心:查询用户时,无论用户是否存在,都返回一个 User 对象(真实 / 空),调用方无需判断null,直接调用getName()/getBalance()。
  • 场景 2 :购物车
    • 真实对象:有商品的购物车(包含 3 件商品,总价 200 元);
    • 空对象:空购物车(NullCart,商品列表为空,总价 0 元,getItemCount () 返回 0);
    • 核心:获取购物车时,即使未创建购物车,也返回空购物车对象,调用方直接调用getTotalPrice()不会报错。
  • 场景 3 :权限控制
    • 真实对象:有管理员权限的用户(canDelete () 返回 true);
    • 空对象:无权限的匿名用户(NullPermission,canDelete () 返回 false,canEdit () 返回 false);
    • 核心:权限校验时,无需判断用户是否登录,直接调用权限方法,空对象返回默认的 "无权限"。
标准角色
角色 职责 类比(用户查询场景) 代码定位
抽象对象(AbstractObject) 定义真实对象和空对象的通用接口(如 User、Cart、Permission) 用户接口(getId ()、getName ()) 接口 / 抽象类
真实对象(ConcreteObject) 实现抽象对象接口,执行实际的业务逻辑(如返回真实用户信息) 真实用户类(RealUser) 业务核心实现类
空对象(NullObject) 实现抽象对象接口,执行空的 / 默认的业务逻辑(如返回默认值、空列表) 空用户类(NullUser) 空实现类(无副作用)
对象工厂(ObjectFactory) 根据条件创建真实对象或空对象(核心:封装null判断逻辑) 用户工厂(UserFactory) 工厂类 / 服务类
核心 UML 类图

二、基础版实现(用户查询)

以 "用户查询" 为例,实现空对象模式的核心逻辑 ------ 查询用户时,无论用户是否存在,都返回一个User对象(真实 / 空),彻底消除null判断和 NPE。

1. 步骤 1:定义抽象对象(用户接口)
复制代码
/**
 * 抽象对象:用户接口(定义真实对象和空对象的通用行为)
 */
public interface User {
    /**
     * 获取用户ID
     */
    Long getId();

    /**
     * 获取用户名
     */
    String getName();

    /**
     * 获取用户余额
     */
    double getBalance();

    /**
     * 判断是否为空对象(可选,便于特殊处理)
     */
    boolean isNull();
}
2. 步骤 2:实现真实对象(真实用户)
复制代码
/**
 * 真实对象:真实用户(执行实际业务逻辑)
 */
public class RealUser implements User {
    private final Long id;
    private final String name;
    private final double balance;

    public RealUser(Long id, String name, double balance) {
        this.id = id;
        this.name = name;
        this.balance = balance;
    }

    @Override
    public Long getId() {
        return id;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public double getBalance() {
        return balance;
    }

    @Override
    public boolean isNull() {
        return false; // 非空对象
    }

    @Override
    public String toString() {
        return "RealUser{id=" + id + ", name='" + name + "', balance=" + balance + "}";
    }
}
3. 步骤 3:实现空对象(空用户)
复制代码
/**
 * 空对象:空用户(执行默认/空的业务逻辑)
 * 核心:所有方法返回合理的默认值,无副作用
 */
public class NullUser implements User {
    // 单例模式:空对象无需多次创建
    public static final NullUser INSTANCE = new NullUser();

    // 私有构造,避免外部实例化
    private NullUser() {}

    @Override
    public Long getId() {
        return -1L; // 默认ID:-1
    }

    @Override
    public String getName() {
        return "匿名用户"; // 默认名称:匿名用户
    }

    @Override
    public double getBalance() {
        return 0.0; // 默认余额:0
    }

    @Override
    public boolean isNull() {
        return true; // 是空对象
    }

    @Override
    public String toString() {
        return "NullUser{id=-1, name='匿名用户', balance=0.0}";
    }
}
4. 步骤 4:实现对象工厂(用户工厂)
复制代码
import java.util.HashMap;
import java.util.Map;

/**
 * 对象工厂:用户工厂(封装null判断,创建真实/空对象)
 * 核心:将所有null判断集中在工厂,调用方无需处理
 */
public class UserFactory {
    // 模拟用户数据库
    private static final Map<Long, User> USER_DB = new HashMap<>();

    // 初始化模拟数据
    static {
        USER_DB.put(1001L, new RealUser(1001L, "张三", 1000.0));
        USER_DB.put(1002L, new RealUser(1002L, "李四", 2000.0));
        USER_DB.put(1003L, new RealUser(1003L, "王五", 3000.0));
    }

    /**
     * 根据ID查询用户(核心:返回RealUser或NullUser,永不返回null)
     */
    public static User getUserById(Long userId) {
        // 若用户存在,返回真实对象;否则返回空对象
        return USER_DB.getOrDefault(userId, NullUser.INSTANCE);
    }
}
5. 客户端(测试用户查询)
复制代码
/**
 * 客户端:测试空对象模式(无null判断,无NPE)
 */
public class NullObjectClient {
    public static void main(String[] args) {
        // 测试1:查询存在的用户
        System.out.println("======= 测试1:查询存在的用户 =======");
        User user1 = UserFactory.getUserById(1001L);
        printUserInfo(user1);

        // 测试2:查询不存在的用户(返回空对象,无NPE)
        System.out.println("\n======= 测试2:查询不存在的用户 =======");
        User user2 = UserFactory.getUserById(9999L);
        printUserInfo(user2);

        // 测试3:直接调用空对象的方法(无需判断null)
        System.out.println("\n======= 测试3:空对象方法调用 =======");
        System.out.println("空用户ID:" + user2.getId());
        System.out.println("空用户名称:" + user2.getName());
        System.out.println("空用户余额:" + user2.getBalance());
        System.out.println("是否为空对象:" + user2.isNull());
    }

    /**
     * 打印用户信息(无需判断user是否为null)
     */
    private static void printUserInfo(User user) {
        System.out.println("用户信息:" + user);
        System.out.println("用户ID:" + user.getId());
        System.out.println("用户名:" + user.getName());
        System.out.println("用户余额:" + user.getBalance());
        System.out.println("是否为空对象:" + user.isNull());
    }
}
输出结果
复制代码
======= 测试1:查询存在的用户 =======
用户信息:RealUser{id=1001, name='张三', balance=1000.0}
用户ID:1001
用户名:张三
用户余额:1000.0
是否为空对象:false

======= 测试2:查询不存在的用户 =======
用户信息:NullUser{id=-1, name='匿名用户', balance=0.0}
用户ID:-1
用户名:匿名用户
用户余额:0.0
是否为空对象:true

======= 测试3:空对象方法调用 =======
空用户ID:-1
空用户名称:匿名用户
空用户余额:0.0
是否为空对象:true

三、Spring 实战版(订单查询 + 缓存)

在业务开发中,空对象模式最核心的实战场景是订单查询 + 缓存 ------ 查询订单时,无论订单是否存在 / 缓存是否命中,都返回Order对象(真实 / 空),避免在业务层频繁判断null,同时统一缓存的空值处理(防止缓存穿透)。

1. 依赖准备(Spring Boot)
复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <version>3.2.3</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
2. 核心模型定义
复制代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 抽象对象:订单接口
 */
public interface Order {
    /**
     * 获取订单ID
     */
    String getOrderId();

    /**
     * 获取用户ID
     */
    String getUserId();

    /**
     * 获取订单金额
     */
    double getAmount();

    /**
     * 获取订单状态
     */
    String getStatus();

    /**
     * 判断是否为空对象
     */
    boolean isNull();
}

/**
 * 真实对象:真实订单
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RealOrder implements Order {
    private String orderId;
    private String userId;
    private double amount;
    private String status;

    @Override
    public boolean isNull() {
        return false;
    }
}

/**
 * 空对象:空订单(单例)
 */
public class NullOrder implements Order {
    public static final NullOrder INSTANCE = new NullOrder();

    private NullOrder() {}

    @Override
    public String getOrderId() {
        return "NULL_ORDER";
    }

    @Override
    public String getUserId() {
        return "NULL_USER";
    }

    @Override
    public double getAmount() {
        return 0.0;
    }

    @Override
    public String getStatus() {
        return "NOT_EXIST";
    }

    @Override
    public boolean isNull() {
        return true;
    }

    @Override
    public String toString() {
        return "NullOrder{orderId='NULL_ORDER', userId='NULL_USER', amount=0.0, status='NOT_EXIST'}";
    }
}
3. 订单仓库(模拟数据库)
复制代码
import org.springframework.stereotype.Repository;

import java.util.HashMap;
import java.util.Map;

/**
 * 订单仓库(模拟数据库查询)
 */
@Repository
public class OrderRepository {
    // 模拟订单数据库
    private final Map<String, Order> orderDB = new HashMap<>();

    // 初始化模拟数据
    public OrderRepository() {
        orderDB.put("O20240315001", new RealOrder("O20240315001", "U1001", 999.9, "已支付"));
        orderDB.put("O20240315002", new RealOrder("O20240315002", "U1002", 1999.9, "已发货"));
        orderDB.put("O20240315003", new RealOrder("O20240315003", "U1003", 2999.9, "已收货"));
    }

    /**
     * 从数据库查询订单(返回RealOrder或NullOrder)
     */
    public Order findByOrderId(String orderId) {
        return orderDB.getOrDefault(orderId, NullOrder.INSTANCE);
    }
}
4. 订单服务(整合缓存 + 空对象)
复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * 订单服务(整合缓存,封装空对象逻辑)
 */
@Slf4j
@Service
public class OrderService {
    @Resource
    private OrderRepository orderRepository;

    /**
     * 查询订单(缓存+空对象,永不返回null)
     * @Cacheable:缓存结果(包括空对象,防止缓存穿透)
     */
    @Cacheable(value = "orderCache", key = "#orderId")
    public Order getOrderById(String orderId) {
        log.info("【订单服务】查询订单(数据库)| 订单ID:{}", orderId);
        Order order = orderRepository.findByOrderId(orderId);
        log.info("【订单服务】查询结果 | 订单ID:{},是否为空对象:{}", orderId, order.isNull());
        return order;
    }

    /**
     * 计算订单总金额(无需判断order是否为null)
     */
    public double calculateTotalAmount(Order order) {
        // 空订单返回0,真实订单返回金额+运费(10元)
        return order.isNull() ? 0.0 : order.getAmount() + 10.0;
    }
}
5. Spring 配置(启用缓存)
复制代码
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

/**
 * Spring配置:启用缓存
 */
@Configuration
@EnableCaching
public class CacheConfig {
}
6. 客户端(Spring Boot 测试)
复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

/**
 * 客户端:测试Spring版空对象模式
 */
@SpringBootApplication
public class SpringNullObjectDemoApplication {
    public static void main(String[] args) {
        // 1. 启动Spring容器
        ConfigurableApplicationContext context = SpringApplication.run(SpringNullObjectDemoApplication.class, args);
        OrderService orderService = context.getBean(OrderService.class);

        // 2. 测试1:查询存在的订单(缓存未命中→数据库→缓存)
        System.out.println("======= 测试1:查询存在的订单 =======");
        Order order1 = orderService.getOrderById("O20240315001");
        printOrderInfo(order1);
        System.out.println("订单总金额(含运费):" + orderService.calculateTotalAmount(order1));

        // 3. 测试2:再次查询同一订单(缓存命中,不查数据库)
        System.out.println("\n======= 测试2:缓存命中 =======");
        Order order1Cache = orderService.getOrderById("O20240315001");
        printOrderInfo(order1Cache);

        // 4. 测试3:查询不存在的订单(返回空对象,缓存空对象)
        System.out.println("\n======= 测试3:查询不存在的订单 =======");
        Order order2 = orderService.getOrderById("O20240315999");
        printOrderInfo(order2);
        System.out.println("订单总金额(含运费):" + orderService.calculateTotalAmount(order2));

        context.close();
    }

    /**
     * 打印订单信息(无需判断null)
     */
    private static void printOrderInfo(Order order) {
        System.out.println("订单信息:" + order);
        System.out.println("订单ID:" + order.getOrderId());
        System.out.println("用户ID:" + order.getUserId());
        System.out.println("订单金额:" + order.getAmount());
        System.out.println("订单状态:" + order.getStatus());
        System.out.println("是否为空对象:" + order.isNull());
    }
}
输出结果
复制代码
======= 测试1:查询存在的订单 =======
【订单服务】查询订单(数据库)| 订单ID:O20240315001
【订单服务】查询结果 | 订单ID:O20240315001,是否为空对象:false
订单信息:RealOrder(orderId=O20240315001, userId=U1001, amount=999.9, status=已支付)
订单ID:O20240315001
用户ID:U1001
订单金额:999.9
订单状态:已支付
是否为空对象:false
订单总金额(含运费):1009.9

======= 测试2:缓存命中 =======
订单信息:RealOrder(orderId=O20240315001, userId=U1001, amount=999.9, status=已支付)
订单ID:O20240315001
用户ID:U1001
订单金额:999.9
订单状态:已支付
是否为空对象:false

======= 测试3:查询不存在的订单 =======
【订单服务】查询订单(数据库)| 订单ID:O20240315999
【订单服务】查询结果 | 订单ID:O20240315999,是否为空对象:true
订单信息:NullOrder{orderId='NULL_ORDER', userId='NULL_USER', amount=0.0, status='NOT_EXIST'}
订单ID:NULL_ORDER
用户ID:NULL_USER
订单金额:0.0
订单状态:NOT_EXIST
是否为空对象:true
订单总金额(含运费):0.0

四、空对象模式的核心特点与适用场景

优点
  1. 消除空指针异常 :从根源上避免NullPointerException,代码更健壮;
  2. 简化代码逻辑 :移除大量if (obj == null)判断,代码更简洁、可读性更高;
  3. 统一接口行为:空对象与真实对象实现相同接口,调用方无需区分,符合开闭原则;
  4. 默认行为可控:空对象可自定义默认返回值(如空列表、0、提示信息),而非直接抛出异常;
  5. 防止缓存穿透:将空对象存入缓存,避免频繁查询不存在的数据(如查询不存在的订单 / 用户);
  6. 语义化更清晰 :用NullUser/NullOrder替代null,代码意图更明确。
缺点
  1. 类数量增加 :每个需要处理空值的接口都需新增一个空对象类(如UserNullUserOrderNullOrder);
  2. 可能隐藏问题 :空对象的默认行为可能掩盖真实的 "数据不存在" 问题(需通过isNull()显式判断);
  3. 单例维护:空对象通常设计为单例,需注意线程安全(无状态的空对象天然线程安全);
  4. 简单场景冗余 :仅需少量null判断的简单场景,使用空对象模式会增加代码复杂度。
适用场景
  1. 数据查询 :用户 / 订单 / 商品查询(返回空对象而非null);
  2. 缓存处理:缓存穿透防护(将空对象存入缓存);
  3. 集合操作 :返回空列表(Collections.emptyList())而非null
  4. 权限控制:匿名用户 / 无权限用户(返回空权限对象);
  5. 第三方接口调用 :接口返回null时,用空对象封装默认行为;
  6. 可选依赖 :组件可选时,用空对象替代null(如日志组件、监控组件);
  7. 避免 NPE 的核心场景 :频繁调用对象方法、链式调用(如user.getAddress().getCity())。

五、JDK/ Spring 中的原生应用(必须知道)

空对象模式是框架中处理空值的核心模式,以下是常见的原生实现:

1. JDK 核心空对象
  • Collections.emptyList()/emptyMap()/emptySet() :返回空集合(而非null),避免 NPE;
  • Optional类 :JDK 8 + 引入,封装可能为null的对象(空对象模式的变种,推荐使用);
  • String.isEmpty()/String.isBlank() :判断字符串是否为空,替代str == null;
  • java.sql.ResultSet :查询无结果时返回空 ResultSet(而非null)。
2. Spring 核心空对象
  • org.springframework.util.CollectionUtils :提供emptyIfNull()方法,将null集合转为空集合;
  • org.springframework.lang.Nullable :注解标记可能为null的参数 / 返回值,但需配合空对象使用;
  • Spring Data JPA :查询无结果时返回空列表(而非null);
  • @Nullable/@NonNull :空值注解,配合空对象模式使用。
3. 第三方库中的空对象
  • Guava :ImmutableList.of()/ImmutableMap.of()返回空集合,Optional(Guava 版);
  • Apache Commons :StringUtils.isEmpty()/CollectionUtils.isEmpty(),EmptyIterator;
  • MyBatis :查询无结果时返回空列表(而非null)。

六、空对象模式 vs Optional(JDK 8+)

JDK 8 引入的Optional是处理空值的另一种方式,与空对象模式互补:

维度 空对象模式 Optional
核心思想 用具体对象替代null,实现相同接口 封装可能为null的对象,显式处理空值
适用场景 长期复用的对象(如 User、Order) 临时的空值处理(如方法返回值)
代码侵入性 高(需定义空对象类) 低(仅封装对象)
语义化 高(NullUser语义明确) 中(Optional<User>需解析)
默认行为 可自定义(返回默认值、空列表) 需手动指定(orElse()/orElseGet()
链式调用 支持(nullUser.getName() 支持(optional.map(User::getName).orElse("匿名")
最佳实践 核心业务对象(User、Order) 临时返回值、链式调用

总结

  1. 空对象模式的核心是用实现相同接口的空对象替代null,消除空指针异常和频繁的null判断,核心角色包括抽象对象、真实对象、空对象、对象工厂;
  2. 空对象通常设计为单例,所有方法返回合理的默认值,无业务副作用;
  3. 空对象模式适用于数据查询、缓存处理、权限控制等场景,尤其适合需要长期复用的核心业务对象;
  4. JDK 8 + 的Optional是空对象模式的轻量级补充,适合临时空值处理,二者可结合使用(如Optional.ofNullable(user).orElse(NullUser.INSTANCE))。
相关推荐
斯幽柏雷科技1 小时前
[Unity]Inspector各种写法(持续更新中)
java·unity·游戏引擎
超级大只老咪1 小时前
输入(java)
算法
灰阳阳1 小时前
Redis的缓存机制
数据库·redis·缓存
wenlonglanying1 小时前
【Redis】设置Redis访问密码
数据库·redis·缓存
jing-ya1 小时前
day51 图论part3
数据结构·算法·深度优先·图论
盐水冰2 小时前
【烘焙坊项目】后端搭建(6)- 店铺状态设置
java·redis
@insist1232 小时前
软件设计师-数据库技术基础:系统组成、三级模式两级映像与数据模型核心考点解析
数据库·软考·软件设计师
健康平安的活着2 小时前
java中乐观锁+事务在批量导入,批量审批案例的使用
java·开发语言
夏语灬2 小时前
SpringBoot集成MQTT客户端
java·spring boot·后端