背景
NPE 问题,100% 的 Java 程序员都碰到,并且曾经是心中的痛。
1965 年英国 TonyHoare 引入了 Null 引用,后续的设计语言包括 Java 都保持了这种设计。
一个例子
业务模型
Person 有车一族,有 Car 字段,
Car 车,每个车都有购买保险,有 Insurance 字段;
Insurance 保险,每个保险都有名字 有 name 字段;
需求:获取某个 Person 对象的购买保险的名称;
常规编程
public String getCarInsuranceName(Person person) {return person.getCar().getInsurance().getName();
}
检查式编程
public String getCarInsuranceName_check(Person person) {if (Objects.nonNull(person)) {final Car car = person.getCar();
if (Objects.nonNull(car)) {final Insurance insurance = car.getInsurance();
if (Objects.nonNull(insurance)) {return insurance.getName();
}
}
}
return "unkown";
}
防御式编程
public String getCarInsuranceName_protect(Person person) {if (Objects.isNull(person)) {return "unkown";}
final Car car = person.getCar();
if (Objects.isNull(car)) {return "unkown";}
final Insurance insurance = car.getInsurance();
if (Objects.isNull(insurance)) {return "unkown";}
return insurance.getName();}
对比一下缺点:
编程方法 | 缺点 |
---|---|
常规编程 | NPE 问题 |
检查式编程 | 1. 可读性不好,多层 if 嵌套;2. 扩展性不好,需要熟悉全流程,否则不知道应该在哪个 if 中扩展,极易出错; |
防御式编程 | 1. 维护困难,4 个不同的退出点,极易出错,容易遗漏检查项目 |
NPE 的痛点
- java 程序中出现最多的 Exception; 没有之一;
- 使得代码量膨胀混乱,对象的空判断充斥在代码中,但是却没有实际的业务意义;
- 类型系统的一个后门,实际上不属于任何类型,也可以说是任何类型;
- 本身无意义,标识对缺失值的建模,也破坏了 java 中弱化指针的理念。
java8 中对缺失值的建模对象是 Optional, 可以基于它解决 NPE 的痛点,设计更好的 API
Optional
领域模型的建模进化
- Person , 含有一个 Optional<Car> car 字段,一个人可能有车,也可能没有车;
- Car, 包含有一个 Optional<Insurance> insurance 字段,一台车可能买了保险,也可能没有买保险;
- Insurance , 保险公司必定有名字所有,他有一个字段 name;
构造方法
构造方法 | 说明 | 备注 |
---|---|---|
Optional.empty() | 一定是空的对象 | 跟 null 有区别,是一个单例对象 |
Optional.of(T t) | 一定是不空的对象 | 如果给了 null 值会立刻抛出 NPE |
Optioanl.ofNullable(T t) | 允许为空的对象放在里面 | 使用值之前需要做检查 |
map 方法 - 对象中提取和转换值
可以把 Optional 看成一种单元素的 Stream, Map, 即把其中的元素按照一定规则转换为其它类型或者进行其它运算后的值,如果没有元素,则啥也不做。
下面的代码是等同的。
public class Test {
public static final String UNKNOWN = "unknown";
/**
* 传统方法
* @param insurance
* @return
*/
public static String getInsuranceName(Insurance insurance){if (Objects.isNull(insurance)){return UNKNOWN;}
return insurance.getName();}
/**
* map 的方式提取
* @param insurance
* @return
*/
public static String getInsuranceNameOp(Insurance insurance){return Optional.ofNullable(insurance).map(Insurance::getName).orElse(UNKNOWN);
}
}
flatMap 方法 – 转换为 Optional 对象输出;
类似于 Stream 的 flatMap 方法,把元素切割或者组合成另外一个流输出。
public static String getInsuranceName(Person person) {return Optional.ofNullable(person)
.flatMap(Person::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getName).orElse(UNKNOWN);
}
默认值设置方法(5 种适合不同的场景)
默认值方法 | 说明 | 场景 |
---|---|---|
or(Supplier<Optional>) | 为空则延迟构造一个 Optional 对象 | 可以采用延迟的方式,对接某些代码来产生默认值 |
orElse(T t) | 为空则采用默认值 | 直接,简单 |
orElseGet(Supplier<T> sp) | 为空则通过函数返回 | 延迟返回,可以对接某些代码逻辑 |
orElseThrow() | 为空则跑出异常 | 默认是 NoSuchElementException |
orElseThrow(Supplier<Throwable> sp) | 为空则跑出自定义异常 | 异常类型可以自定义 |
使用值(获取或者消费)
主要分两种场景,直接获取值,采用 get()方法;
里面有值,则消费,ifPresent(Consumer<T> c)
消费或者获取方法 | 说明 | 场景 |
---|---|---|
get() | 获取 Optional 中的值,如果没有值,会抛出异常 | 确认里面有值才会调用该防范 |
ifPresent(Consumer<T> c) | 有值则执行自定义代码段,消费该值 | 流式编程,有值继续处理逻辑 |
ifPresentOrElse(Consumer<T> c , Runnable r) | 如果有值,则消费,没有值,进行另外的处理 | 有值或者没有值都进行处理 java9 才有 |
多个 Optional 进行运算
通过使用 flatMap,map 可以做到, 方法里执行的已经做好了对 empty 的情况进行处理。
实例如下:
public static String getCheapestPrizeIsuranceNameOp(Person person, Car car) {return Optional.ofNullable(person)
.flatMap(p -> Optional.ofNullable(car).map(c -> getCheapest(p, c)))
.orElse(UNKNOWN);
}
public static String getCheapestPrizeIsuranceName(Person person, Car car) {if (Objects.nonNull(person) && Objects.nonNull(car)) {return getCheapest(person, car);
}
return UNKNOWN;
}
/**
* 模拟得到最便宜的保险
*
* @param person 人
* @param car 车
* @return 最便宜的车险名称
*/
private static String getCheapest(Person person, Car car) {return "pinan";}
filter 方法(过滤)
因为 Optional 中只有一个值,所以这里的 filter 实际上是判断单个值是不是。
对比代码:
public static Insurance getPinanInsurance(Person person){Optional<Insurance> insuranceOptional = Optional.ofNullable(person).map(Person::getCar).map(Car::getInsurance);
if (insuranceOptional.isPresent() && Objects.equals("pinan", insuranceOptional.get().getName())){return insuranceOptional.get();
}
return null;
}
public static Insurance getPinanInsurance_filter(Person person){return Optional.ofNullable(person)
.map(Person::getCar)
.map(Car::getInsurance)
.filter(item->Objects.equals(item.getName(),"pinan" ))
.orElse(null);
}
empty 方法(构造一个空的 Optional 对象)
Optional 改造历史代码
封装可能潜在为 null 的对象
public Object getFromMap(String key){Map<String,Object> map = new HashMap<>(4);
map.put("a", "aaa");
map.put("b", "bbb");
map.put("c", "ccc");
Object value = map.get(key);
if (Objects.isNull(value)){throw new NoSuchElementException("不存在 key");
}
return value;
}
public Object getFromMapOp(String key){Map<String,Object> map = new HashMap<>(4);
map.put("a", "aaa");
map.put("b", "bbb");
map.put("c", "ccc");
Object value = map.get(key);
return Optional.ofNullable(value).orElseThrow(()->new NoSuchElementException("不存在 key"));
}
发生异常的建模可以替换为 Optional 对象
这种是建模思想的转变,不一定适用每个人;
/**
* 如果字符串不是数字,会抛出异常
* @param a 字符串
* @return 数字
*/
public Integer string2Int(String a){return Integer.parseInt(a);
}
/**
* Optional.empty 对应异常的情况,后续比较好处理;* @param a 字符串
* @return 可能转换失败的整数,延迟到使用方去处理
*/
public Optional<Integer> string2Int_op(String a){
try{return Optional.of(Integer.parseInt(a));
}catch (Exception ex){return Optional.empty();
}
}
尽量不使用封装的 Optional
封装的 OptionalInt, OptionalLong , 因为 Optional 里面只有一个元素,使用封装类没有性能优势,而且缺失了重要的 flatMap, map,filter 方法;
总的来说,Optional 的使用,简化了代码,使得代码可读性和可维护性更好。
最后来个例子:
public Integer getFromProperties(Properties properties, String key) {String value = properties.getProperty(key);
if (Objects.nonNull(value)) {
try {Integer integer = Integer.parseInt(value);
if (integer > 0) {return integer;}
} catch (Exception ex) {
// 无需处理异常
return 0;
}
}
return 0;
}
public Integer getFromProperties_op(Properties properties, String key) {return Optional.ofNullable(properties.getProperty(key))
.map(item -> {
try {return Integer.parseInt(item);
} catch (Exception ex) {return 0;}
})
.orElse(0);
}
Optional 源码阅读
一个容器对象,可能有也可能没有非空值,如果值存在,isPresent() 返回 true, 如果没有值,则对象被当成空,isPresent() 返回 false;
更多的方法依赖于容器中是否含有值,比如 orElse(返回一个默认值当没有值)
ifPresent(Consumer c) 是当值存在的时候,执行一个动作;这是一个基于值的类,使用标识敏感的操作,包含 比较引用的 ==,hashcode , synchronization 针对一个 Optional 对象,可能有无法预料的结果,然后应该避免这类操作。编写 API 的注意点:Optional 最初被用来设计为方法的返回值,当明确需要代表没有值的情况。返回 null, 可能出错;而返回 Optional 对象不是一个 null 对象,它总是指向一个 Optional 对象实例。/**
* A container object which may or may not contain a non-{@code null} value.
* If a value is present, {@code isPresent()} returns {@code true}. If no
* value is present, the object is considered <i>empty</i> and
* {@code isPresent()} returns {@code false}.
*
* <p>Additional methods that depend on the presence or absence of a contained
* value are provided, such as {@link #orElse(Object) orElse()}
* (returns a default value if no value is present) and
* {@link #ifPresent(Consumer) ifPresent()} (performs an
* action if a value is present).
*
* <p>This is a <a href="../lang/doc-files/ValueBased.html">value-based</a>
* class; use of identity-sensitive operations (including reference equality
* ({@code ==}), identity hash code, or synchronization) on instances of
* {@code Optional} may have unpredictable results and should be avoided.
*
* @apiNote
* {@code Optional} is primarily intended for use as a method return type where
* there is a clear need to represent "no result," and where using {@code null}
* is likely to cause errors. A variable whose type is {@code Optional} should
* never itself be {@code null}; it should always point to an {@code Optional}
* instance.
*
* @param <T> the type of value
* @since 1.8
*/
其它的代码比较简单,模型就是里面含有一个 T 类型的值,empty() 是一个特殊的 Optional 对象,里面的值是 null;
public final class Optional<T> {
/**
* Common instance for {@code empty()}.
*/
private static final Optional<?> EMPTY = new Optional<>();
/**
* If non-null, the value; if null, indicates no value is present
*/
private final T value;
/**
* Constructs an empty instance.
*
* @implNote Generally only one empty instance, {@link Optional#EMPTY},
* should exist per VM.
*/
private Optional() {this.value = null;}
小结
- Optional 表示一个可能缺失的对象,API 可以依据这个进行建模,但是要注意序列化的问题;可以避免空指针的问题,并且提升代码的可读性和可维护性。
- Optional 的构造方法有 3 个,of,ofNullable,empty;
- map,flatmap , filter 可以快速的转换和过滤值;
- 值缺失的处理方法有 3 个,orElse, orElseGet, orElseThrow;
原创不易,转载请注明出处,欢迎沟通交流。