受 GOF(Gang of Four,四人组)《设计模式》一书的影响,让人误以为设计模式只有 23 种,其实不然,除了《设计模式》介绍的 23 种设计模式外,还有很多经典的设计模式,例如我们本文将要介绍的空对象模式。
定义
在空对象模式(Null Object Pattern)中,一个空对象取代 NULL 对象实例的检查。
英文定义如下:
Provide an object as a surrogate for the lack of an object of a given type. The Null Object provides intelligent do nothing behavior, hiding the details from its collaborators.
意思是:为缺少的对象提供一个默认的无意义对象,用来避免 Null 对象的产生。
简单来说,就是用一个空对象,来取代程序中的 Null 值判断,从而让调用者可以直接使用对象,而无需关心对象是否为 Null。
例如,在没用空对象模式之前,要正确的获取以下值:
String isocode = user.getAddress().getCountry().getIsocode().toUpperCase();
它的实现代码如下:
if (user != null) {(img)
Address address = user.getAddress();
if (address != null) {
Country country = address.getCountry();
if (country != null) {
String isocode = country.getIsocode();
if (isocode != null) {
isocode = isocode.toUpperCase();
}
}
}
}
非空判断已经多到令我们崩溃了,如果属性中还有更多的对象,那 Null 值判断就更多了,为了解决这个问题,就要使用本文将要介绍的空对象模式了。
组成角色
空对象模式包含如下角色:
- 抽象对象(Abstract Object)角色:声明统一的对象行为(属性和方法);
- 具体对象(Concrete Object)角色:确实存在的具体对象,程序中的非 Null 对象;
- 空对象(Null Object)角色:非具体存在的对象,Null 对象;
- 对象工厂(Object Factory)角色:根据传递的标识得到相关类的工厂类,返回值可以是具体对象或 Null 对象。
角色之间的 UML 关系图如下:
观察者模式代码实现
抽象对象
/**
* 抽象对象
*/
abstract class AbstractObject {
String name;
abstract String getName();
abstract boolean isNull();
}
具体对象
/**
* 具体对象
*/
class ConcreteObject extends AbstractObject {
public ConcreteObject(final String name) {
this.name = name;
}
@Override
String getName() {
return this.name;
}
@Override
boolean isNull() {
return false;
}
}
空对象
/**
* 空对象
*/
class NullObject extends AbstractObject {
@Override
String getName() {
return "Not Available in Customer Database";
}
@Override
boolean isNull() {
return true;
}
}
对象工厂
/**
* 对象生成工厂
*/
class ObjectFactory {
public static AbstractObject creator(final String name) {
AbstractObject result = null;
switch (name) {
case "Java":
result = new ConcreteObject("Java");
break;
case "SQL":
result = new ConcreteObject("SQL");
break;
default:
result = new NullObject();
break;
}
return result;
}
}
程序执行结果如下:
Java
Not Available in Customer Database
SQL
从以上的代码可以看出,其中 getName () 为所有对象需要执行的公共方法,如果没使用空对象模式的情况下,每次在调用 getName () 之前,我们需要先判空再使用,而如果使用的是空对象模式的话,则可以直接使用(该方法)。
优缺点
空对象模式的优点:
- 省去代码中对 Null 值的判断和检查;
- 让代码显的更加优雅和可读性更高;
- 让系统更加稳定,避免程序抛出 NullPointerException 异常。
空对象模式的缺点: - 因为增加了更多的类信息,从而使系统更复杂。
应用场景
JDK 8 中的 Optional 对象使用的就是空对象模式,避免空指针的异常,同时又能写出优雅而简洁的 Java 代码。
Optional 类中有以下几个重要的方法:
- ofNullable () 方法:为指定的值创建一个 Optional, 如果指定的值为 null,则返回一个空的 Optional 对象;
- orElse () 方法:如果有值则将其返回,否则返回指定的其它值;
- map () 方法:如果创建的 Optional 中的值存在,对该值执行提供的 Function 函数调用;
- flagMap () 方法:如果创建的 Optional 中的值存在,就对该值执行提供的 Function 函数调用,返回一个 Optional 类型的值,否则就返回一个空的 Optional 对象。
小贴士:很多人可能对 “对该值执行提供的 Function 函数调用” 这句话不太理解,它的意思是说,例如下面代码:
Optional.ofNullable(concreteUser).flatMap(u -> u.getAddress())
其中 “(u -> u.getAddress ())” 这部分代码就是 “该值执行提供的 Function 函数”。
接下来我们就是用 Optional 对象,优雅的实现判空操作,优雅的实现文章开头 4 层令人崩溃的 Null 值判断,实现代码如下。
用户类
/**
* 用户类
**/
class User {
public User(Address address) {
this.address = address;
}
private Address address;
public Optional<Address> getAddress() {
return Optional.ofNullable(address);
}
public void setAddress(Address address) {
this.address = address;
}
}
地址类
/**
* 地址类
**/
class Address {
public Address(Country country) {
this.country = country;
}
private Country country;
public Optional<Country> getCountry() {
return Optional.ofNullable(country);
}
public void setCountry(Country country) {
this.country = country;
}
}
国际编码类
/**
* 国际编码类
**/
class Country {
public Country(String isocode) {
this.isocode = isocode;
}
private String isocode;
public String getIsocode() {
return isocode;
}
public void setIsocode(String isocode) {
this.isocode = isocode;
}
}
客户端调用
public class Client {
public static void main(final String[] args) {
// JDK 8 Optional 对象判空示例
// 具体对象
User concreteUser = new User(new Address(new Country("china")));
// 空对象
User nullUser = new User(null);
// 具体对象编码获取
String concreteIsocode = Optional.ofNullable(concreteUser)
.flatMap(u -> u.getAddress())
.flatMap(a -> a.getCountry())
.map(c -> c.getIsocode())
.orElse("暂无").toUpperCase();
// 空对象编码获取
String nullIsocode = Optional.ofNullable(nullUser)
.flatMap(u -> u.getAddress())
.flatMap(a -> a.getCountry())
.map(c -> c.getIsocode())
.orElse("暂无").toUpperCase();
System.out.println("Concrete User:" + concreteIsocode);
System.out.println("Null User:" + nullIsocode);
}
}
程序直接结果如下:
Concrete User:CHINA
Null User:暂无
使用实例
以生活中场景为例,例如,在一个小商店里,售货员可以根据商品的编码,得到商品的具体名称和价格等信息,实现代码如下。
抽象商品类
/**
* 抽象商品类
**/
abstract class AbstractGoods {
public String isbn; // 商品编码
public String name;
public Double price;
public abstract void show();
}
具体商品类
/**
* 具体商品
**/
class ConcreteGoods extends AbstractGoods {
public ConcreteGoods(String isbn, String name, Double price) {
this.isbn = isbn;
this.name = name;
this.price = price;
}
@Override
public void show() {
System.out.println("商品名:" + this.name + ",价格:" + this.price + "元");
}
}
空商品类
/**
* 空商品类
**/
class NullGoods extends AbstractGoods {
@Override
public void show() {
System.out.println("商品信息暂无!");
}
}
商品工厂
/**
* 商品查询工厂
**/
class GoodsFactory {
public static AbstractGoods find(final String isbn) {
AbstractGoods result = null;
switch (isbn) {
case "001":
result = new ConcreteGoods("001", "Java面试全解析", 69.0);
break;
case "002":
result = new ConcreteGoods("002", "MySQL面试金典", 19.0);
break;
default:
result = new NullGoods();
break;
}
return result;
}
}
客户端调用
public class Client {
public static void main(final String[] args) {
final AbstractGoods goods1 = GoodsFactory.find("001");
final AbstractGoods goods2 = GoodsFactory.find("003");
goods1.show();
goods2.show();
}
}
程序执行结果如下:
商品名:Java 面试全解析,价格:69.0 元
商品信息暂无!
总结
在 Java 语言中,解决 NullPointerException 异常的常见方法是使用空对象模式,空对象模式可以省去代码中对 Null 值的判断,从而使代码更加的简洁和优雅。在 JDK 8 之后,Java API 给我们提供了 Optional 类,使用它可以优雅且有效的,规避空对象产生 NullPointerException 的问题。