一、空对象模式核心定义
空对象模式是行为型设计模式的一种,核心目的是:
用一个 "空对象"(Null Object)来替代代码中的
null值,这个空对象实现了与真实对象相同的接口,但不做任何实际业务操作,仅返回 "空" 的结果或默认行为。
简单来说:用一个有具体实现的 "空对象" 代替null,让代码无需频繁判断null,同时保证程序逻辑的完整性。
核心解决的问题
- 避免空指针异常(NPE) :消除代码中大量的if (obj == null)判断,从根源上减少 NPE;
- 简化代码逻辑 :无需在每个调用点处理null,空对象会返回合理的默认值;
- 统一行为接口 :空对象与真实对象实现相同接口,调用方无需区分,符合里氏替换原则;
- 提升代码可读性 :用语义化的空对象(如NullUser、NullOrder)替代无意义的null,代码意图更清晰;
- 默认行为可控 :空对象可自定义默认行为(如返回空列表、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
四、空对象模式的核心特点与适用场景
优点
- 消除空指针异常 :从根源上避免
NullPointerException,代码更健壮;- 简化代码逻辑 :移除大量
if (obj == null)判断,代码更简洁、可读性更高;- 统一接口行为:空对象与真实对象实现相同接口,调用方无需区分,符合开闭原则;
- 默认行为可控:空对象可自定义默认返回值(如空列表、0、提示信息),而非直接抛出异常;
- 防止缓存穿透:将空对象存入缓存,避免频繁查询不存在的数据(如查询不存在的订单 / 用户);
- 语义化更清晰 :用
NullUser/NullOrder替代null,代码意图更明确。缺点
- 类数量增加 :每个需要处理空值的接口都需新增一个空对象类(如
User→NullUser,Order→NullOrder);- 可能隐藏问题 :空对象的默认行为可能掩盖真实的 "数据不存在" 问题(需通过
isNull()显式判断);- 单例维护:空对象通常设计为单例,需注意线程安全(无状态的空对象天然线程安全);
- 简单场景冗余 :仅需少量
null判断的简单场景,使用空对象模式会增加代码复杂度。适用场景
- 数据查询 :用户 / 订单 / 商品查询(返回空对象而非
null);- 缓存处理:缓存穿透防护(将空对象存入缓存);
- 集合操作 :返回空列表(
Collections.emptyList())而非null;- 权限控制:匿名用户 / 无权限用户(返回空权限对象);
- 第三方接口调用 :接口返回
null时,用空对象封装默认行为;- 可选依赖 :组件可选时,用空对象替代
null(如日志组件、监控组件);- 避免 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) | 临时返回值、链式调用 |
总结
- 空对象模式的核心是用实现相同接口的空对象替代
null,消除空指针异常和频繁的null判断,核心角色包括抽象对象、真实对象、空对象、对象工厂; - 空对象通常设计为单例,所有方法返回合理的默认值,无业务副作用;
- 空对象模式适用于数据查询、缓存处理、权限控制等场景,尤其适合需要长期复用的核心业务对象;
- JDK 8 + 的
Optional是空对象模式的轻量级补充,适合临时空值处理,二者可结合使用(如Optional.ofNullable(user).orElse(NullUser.INSTANCE))。
