【从零入门23种设计模式21】行为型之空对象模式
一、空对象模式核心定义空对象模式是行为型设计模式的一种核心目的是用一个 “空对象”Null Object来替代代码中的null值这个空对象实现了与真实对象相同的接口但不做任何实际业务操作仅返回 “空” 的结果或默认行为。简单来说用一个有具体实现的 “空对象” 代替null让代码无需频繁判断null同时保证程序逻辑的完整性。核心解决的问题避免空指针异常NPE消除代码中大量的if (obj null)判断从根源上减少 NPE简化代码逻辑无需在每个调用点处理null空对象会返回合理的默认值统一行为接口空对象与真实对象实现相同接口调用方无需区分符合里氏替换原则提升代码可读性用语义化的空对象如NullUser、NullOrder替代无意义的null代码意图更清晰默认行为可控空对象可自定义默认行为如返回空列表、0 值、提示信息而非直接抛出异常。生活类比场景 1用户查询真实对象存在的用户张三ID:1001余额1000 元空对象不存在的用户NullUserID:-1余额0 元getName () 返回 匿名用户核心查询用户时无论用户是否存在都返回一个 User 对象真实 / 空调用方无需判断null直接调用getName()/getBalance()。场景 2购物车真实对象有商品的购物车包含 3 件商品总价 200 元空对象空购物车NullCart商品列表为空总价 0 元getItemCount () 返回 0核心获取购物车时即使未创建购物车也返回空购物车对象调用方直接调用getTotalPrice()不会报错。场景 3权限控制真实对象有管理员权限的用户canDelete () 返回 true空对象无权限的匿名用户NullPermissioncanDelete () 返回 falsecanEdit () 返回 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匿名用户, balance0.0}; } }4. 步骤 4实现对象工厂用户工厂import java.util.HashMap; import java.util.Map; /** * 对象工厂用户工厂封装null判断创建真实/空对象 * 核心将所有null判断集中在工厂调用方无需处理 */ public class UserFactory { // 模拟用户数据库 private static final MapLong, 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{id1001, name张三, balance1000.0} 用户ID1001 用户名张三 用户余额1000.0 是否为空对象false 测试2查询不存在的用户 用户信息NullUser{id-1, name匿名用户, balance0.0} 用户ID-1 用户名匿名用户 用户余额0.0 是否为空对象true 测试3空对象方法调用 空用户ID-1 空用户名称匿名用户 空用户余额0.0 是否为空对象true三、Spring 实战版订单查询 缓存在业务开发中空对象模式最核心的实战场景是订单查询 缓存—— 查询订单时无论订单是否存在 / 缓存是否命中都返回Order对象真实 / 空避免在业务层频繁判断null同时统一缓存的空值处理防止缓存穿透。1. 依赖准备Spring Bootdependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter/artifactId version3.2.3/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-cache/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies2. 核心模型定义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{orderIdNULL_ORDER, userIdNULL_USER, amount0.0, statusNOT_EXIST}; } }3. 订单仓库模拟数据库import org.springframework.stereotype.Repository; import java.util.HashMap; import java.util.Map; /** * 订单仓库模拟数据库查询 */ Repository public class OrderRepository { // 模拟订单数据库 private final MapString, 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查询存在的订单 【订单服务】查询订单数据库| 订单IDO20240315001 【订单服务】查询结果 | 订单IDO20240315001是否为空对象false 订单信息RealOrder(orderIdO20240315001, userIdU1001, amount999.9, status已支付) 订单IDO20240315001 用户IDU1001 订单金额999.9 订单状态已支付 是否为空对象false 订单总金额含运费1009.9 测试2缓存命中 订单信息RealOrder(orderIdO20240315001, userIdU1001, amount999.9, status已支付) 订单IDO20240315001 用户IDU1001 订单金额999.9 订单状态已支付 是否为空对象false 测试3查询不存在的订单 【订单服务】查询订单数据库| 订单IDO20240315999 【订单服务】查询结果 | 订单IDO20240315999是否为空对象true 订单信息NullOrder{orderIdNULL_ORDER, userIdNULL_USER, amount0.0, statusNOT_EXIST} 订单IDNULL_ORDER 用户IDNULL_USER 订单金额0.0 订单状态NOT_EXIST 是否为空对象true 订单总金额含运费0.0四、空对象模式的核心特点与适用场景优点消除空指针异常从根源上避免NullPointerException代码更健壮简化代码逻辑移除大量if (obj null)判断代码更简洁、可读性更高统一接口行为空对象与真实对象实现相同接口调用方无需区分符合开闭原则默认行为可控空对象可自定义默认返回值如空列表、0、提示信息而非直接抛出异常防止缓存穿透将空对象存入缓存避免频繁查询不存在的数据如查询不存在的订单 / 用户语义化更清晰用NullUser/NullOrder替代null代码意图更明确。缺点类数量增加每个需要处理空值的接口都需新增一个空对象类如User→NullUserOrder→NullOrder可能隐藏问题空对象的默认行为可能掩盖真实的 “数据不存在” 问题需通过isNull()显式判断单例维护空对象通常设计为单例需注意线程安全无状态的空对象天然线程安全简单场景冗余仅需少量null判断的简单场景使用空对象模式会增加代码复杂度。适用场景数据查询用户 / 订单 / 商品查询返回空对象而非null缓存处理缓存穿透防护将空对象存入缓存集合操作返回空列表Collections.emptyList()而非null权限控制匿名用户 / 无权限用户返回空权限对象第三方接口调用接口返回null时用空对象封装默认行为可选依赖组件可选时用空对象替代null如日志组件、监控组件避免 NPE 的核心场景频繁调用对象方法、链式调用如user.getAddress().getCity()。五、JDK/ Spring 中的原生应用必须知道空对象模式是框架中处理空值的核心模式以下是常见的原生实现1. JDK 核心空对象Collections.emptyList()/emptyMap()/emptySet()返回空集合而非null避免 NPEOptional类JDK 8 引入封装可能为null的对象空对象模式的变种推荐使用String.isEmpty()/String.isBlank()判断字符串是否为空替代str nulljava.sql.ResultSet查询无结果时返回空 ResultSet而非null。2. Spring 核心空对象org.springframework.util.CollectionUtils提供emptyIfNull()方法将null集合转为空集合org.springframework.lang.Nullable注解标记可能为null的参数 / 返回值但需配合空对象使用Spring Data JPA查询无结果时返回空列表而非nullNullable/NonNull空值注解配合空对象模式使用。3. 第三方库中的空对象GuavaImmutableList.of()/ImmutableMap.of()返回空集合OptionalGuava 版Apache CommonsStringUtils.isEmpty()/CollectionUtils.isEmpty()EmptyIteratorMyBatis查询无结果时返回空列表而非null。六、空对象模式 vs OptionalJDK 8JDK 8 引入的Optional是处理空值的另一种方式与空对象模式互补维度空对象模式Optional核心思想用具体对象替代null实现相同接口封装可能为null的对象显式处理空值适用场景长期复用的对象如 User、Order临时的空值处理如方法返回值代码侵入性高需定义空对象类低仅封装对象语义化高NullUser语义明确中OptionalUser需解析默认行为可自定义返回默认值、空列表需手动指定orElse()/orElseGet()链式调用支持nullUser.getName()支持optional.map(User::getName).orElse(匿名)最佳实践核心业务对象User、Order临时返回值、链式调用总结空对象模式的核心是用实现相同接口的空对象替代null消除空指针异常和频繁的null判断核心角色包括抽象对象、真实对象、空对象、对象工厂空对象通常设计为单例所有方法返回合理的默认值无业务副作用空对象模式适用于数据查询、缓存处理、权限控制等场景尤其适合需要长期复用的核心业务对象JDK 8 的Optional是空对象模式的轻量级补充适合临时空值处理二者可结合使用如Optional.ofNullable(user).orElse(NullUser.INSTANCE)。