概述:Optional 最早是 Google 公司 Guava 中的概念,代表的是可选值。Optional 类从 Java8 版本开始加入豪华套餐,主要为了解决程序中的 NPE 问题,从而使得更少的显式判空,防止代码污染,另一方面,也使得领域模型中所隐藏的知识,得以显式体现在代码中。Optional 类位于 java.util 包下,对链式编程风格有一定的支持。实际上,Optional 更像是一个容器,其中存放的成员变量是一个 T 类型的 value,可值可 Null,使用的是 Wrapper 模式,对 value 操作进行了包装与设计。本文将从 Optional 所解决的问题开始,逐层解剖,由浅入深,文中会出现 Optioanl 方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项 Java8 中的新特性,进行理解和深入。
1、解决的问题
臭名昭著的空指针异常,是每个程序员都会遇到的一种常见异常,任何访问对象的方法与属性的调用,都可能会出现 NullPointException,如果要确保不触发异常,我们通常需要进行对象的判空操作。
举个栗子,有一个人(Shopper)进超市购物,可能会用购物车(Trolley)也可能会用其它方式,购物车里可能会有一袋栗子(Chestnut),也可能没有。三者定义的代码如下:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public class Shopper {
private Trolley trolley;
public Trolley getTrolley(){
return trolley;
}
}
public class Trolley {
private Chestnut chestnut;
public Chestnut getChestnut(){
return chestnut;
}
}
public class Chestnut {
private String name;
public String getName(){
return name;
}
}
复制代码
这时想要获得购物车中栗子的名称,像下面这么写,就可能会见到我们的“老朋友”(NPE)
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public String result(Shopper shopper){
return shopper.getTrolley().getChestnut().getName();
}
复制代码
为了能避免出现空指针异常,通常的写法会逐层判空(多层嵌套法),如下
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public String result(Shopper shopper) {
if (shopper != null) {
Trolley trolley = shopper.getTrolley();
if (trolley != null) {
Chestnut chestnut = trolley.getChestnut();
if (chestnut != null) {
return chestnut.getName();
}
}
}
return "获取失败辽";
}
复制代码
多层嵌套的方法在对象级联关系比较深的时候会看的眼花缭乱的,尤其是那一层一层的括号😒;另外出错的原因也因为缺乏对应信息而被模糊(例如 trolley 为空时也只返回了最后的获取失败。当然也可以在每一层增加 return,相应的代码有会变得很冗长),所以此时我们也可以用遇空则返回的卫语句进行改写。
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public String result(Shopper shopper) {
if (shopper == null) {
return "购物者不存在";
}
Trolley trolley = shopper.getTrolley();
if (trolley == null) {
return "购物车不存在";
}
Chestnut chestnut = trolley.getChestnut();
if (chestnut == null) {
return "栗子不存在";
}
return chestnut.getName();
}
复制代码
为了取一个名字进行了三次显示判空操作,这样的代码当然没有问题,但是优秀的工程师们总是希望能获得更优雅简洁的代码。Optional 就提供了一些方法,实现了这样的期望。
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public String result(Shopper shopper){
return Optional.ofNullable(shopper)
.map(Shopper::getTrolley)
.map(Trolley::getChestnut)
.map(Chestnut::getName)
.orElse("获取失败辽");
}
复制代码
2、常用方法
1)获得 Optional 对象
Optional 类中有两个构造方法:带参和不带参的。带参的将传入的参数赋值 value,从而构建 Optional 对象;不带参的用 null 初始化 value 构建对象。
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
private Optional() {}
private Optional(T value) {}
复制代码
但是两者都是私有方法,而实际上 Optional 的对象都是通过静态工厂模式的方式构建,主要有以下三个函数
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public static <T> Optional<T> of(T value) {}
public static <T> Optional<T> ofNullable(T value) {}
public static <T> Optional<T> empty() {}
复制代码
创建一个一定不为空的 Optional 对象,因为如果传入的参数为空会抛出 NPE
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Chestnut chestnut = new Chestnut();
Optional<Chestnut> opChest = Optional.of(chestnut);
复制代码
创建一个空的 Optional 对象
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.empty();
复制代码
创建一个可空的 Optional 对象
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Chestnut chestnut = null;
Optional<Chestnut> opChest = Optional.ofNullable(chestnut);
复制代码
2)正常使用
正常使用的方法可以被大致分为三种类型,判断类、操作类和取值类
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
//判断类
public boolean isPresent() {}
//操作类
public void ifPresent(Consumer<? super T> consumer) {}
//取值类
public T get() {}
public T orElse(T other) {}
public T orElseGet(Supplier<? extends T> other) {}
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {}
复制代码
isPresent()方法像一个安全阀,控制着容器中的 value 值是空还是有值,用法与原本的 null != obj 的用法相似。当 obj 有值返回 true,为空返回 false(即 value 值存在为真)。但一般实现判断空或不为空的逻辑,使用 Optional 其他的方法处理会更为常见。如下代码将会打印出没有栗子的悲惨事实。
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.empty();
if (!opChest.isPresent()){
System.out.println("容器里没有栗子");
}
复制代码
ifPresent()方法是一个操作类的方法,他的参数是一段目标类型为 Consumer 的函数,当 value 不为空时,自动执行 consumer 中的 accept()方法(传入时实现),为空则不执行任何操作。比如下面这段代码,我们传入了一段输出 value 的 lamda 表达式,打印出了“迁西板栗”。
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.ofNullable(new Chestnut("迁西板栗"));
opChest.ifPresent(c -> System.out.println(c.getName()));
复制代码
get()方法源码如下,可以看出,get 的作用是直接返回容器中的 value。但如此粗暴的方法,使用前如果不判空,在 value 为空时,便会毫不留情地抛出一个异常。
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
复制代码
三个 orElse 方法与 get 相似,也都属于取值的操作。与 get 不同之处在于 orElse 方法不用额外的判空语句,撰写逻辑时比较愉快。三个 orElse 的相同之处是当 value 不为空时都会返回 value。当为空时,则另有各自的操作:orElse()方法会返回传入的 other 实例(也可以为 Supplier 类型的函数);orElseGet()方法会自动执行 Supplier 类型实例的 get()方法;orElseThrow()方法会抛出一个自定的异常。更具体的差别会在后面的方法对比中描述。
如下面这段代码,展示了在没有栗子的时候,如何吐出“信阳板栗”、“镇安板栗”,以及抛出“抹油栗子”的警告。
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.empty();
System.out.println(opChest.orElse(new Chestnut("信阳板栗")));
System.out.println(opChest.orElseGet(() -> new Chestnut("镇安板栗")));
try {
opChest.orElseThrow(() -> new RuntimeException("抹油栗子呀"));
}catch (RuntimeException e){
System.out.println(e.getMessage());
}
复制代码
3)进阶使用
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public Optional<T> filter(Predicate<? super T> predicate) {}
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {}
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {}
复制代码
filter()方法接受谓词为 Predicate 类型的函数作为参数,如果 value 值不为空则自动执行 predicate 的 test()方法(传入时实现),来判断是否满足条件,满足则会返回自身 Optional,不满足会返回空 Optional;如果 value 值为空,则会返回自身 Optional(其实跟空 Optional 也差不多)。如下代码,第二句中筛选条件“邵店板栗”与 opChest 中的板栗名不符,没有通过过滤。而第三句的筛选条件与 opChest 一致,所以最后打印出来的是“宽城板栗”。
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.ofNullable(new Chestnut("宽城板栗"));
opChest.filter(c -> "邵店板栗".equals(c.getName())).ifPresent(System.out::println);
opChest.filter(c -> "宽城板栗".equals(c.getName())).ifPresent(System.out::println);
复制代码
map()和 flatmap()方法传入的都是一个 Function 类型的函数,map 在这里翻译为“映射”,当 value 值不为空时进行一些处理,返回的值是经过 mapper 的 apply()方法处理后的 Optional 类型的值,两个方法的结果一致,处理过程中会有差别。如下代码,从 opChest 中获取了板栗名后,重新 new 了一个板栗取名“邢台板栗”,并打印出来,两者输出一致,处理形式上有差异,这个在后面的方法对比中会再次说到。
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.ofNullable(new Chestnut("邢台板栗"));
System.out.println(opChest.map(c -> new Chestnut(c.getName())));
System.out.println(opChest.flatMap(c -> Optional.ofNullable(new Chestnut(c.getName()))));
复制代码
4)1.9 新增
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) {}
public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier) {}
public Stream<T> stream() {}
复制代码
JDK1.9 中增加了三个方法:ifPresentOrElse() 、or()和 stream()方法。
1.8 时,ifPresent()仅提供了 if(obj != null)的方法,并未提供 if(obj != null)else{}的操作,所以在 1.9 中增加了一个 ifPresentElse()方法,提供了这方面的支持。该方法接收两个参数 Consumer 和 Runnable 类型的函数,当 value 不为空,调用 action 的 accept()方法,这点与 ifPresent()一致,当 value 为空时,会调用 emptyAction 的 run()方法,执行 else 语义的逻辑。如下面代码,会打印出“木有栗子”的提示。
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> opChest = Optional.empty();
opChest.ifPresentElse(c -> System.out.println(c.getName()),c -> System.out.println("木有栗子呀"));
复制代码
or()方法是作为 orElse()和 orElseGet()方法的改进而出现的,使用方法一致,但后两个方法在执行完成后返回的并非包装值。如果需要执行一些逻辑并返回 Optional 时,可以使用 or()方法。该方法传入 Supplier 接口的实例,当 value 有值时直接返回自身 Optional,当为空时,自动执行 suuplier 的 get()方法,并包装成 Optional 返回,其源码中包装的语句如下:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<T> r = (Optional<T>) supplier.get();
return Objects.requireNonNull(r);
复制代码
stream()方法则不用多说,是一个提供给流式编程使用的方法,功能上是一个适配器,将 Optional 转换成 Stream:没有值返回一个空的 stream,或者包含一个 Optional 的 stream。其源码如下:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
if (!isPresent()) {
return Stream.empty();
} else {
return Stream.of(value);
}
复制代码
3、方法对比和总结
Optional 封装的方法较多,选择一个合适的方法的前提是要了解各自适用的场景和异同
1)创建方法的对比
由于构造器为私有方法,创建对象只能通过静态工厂的方式创建。of()、ofNullable()和 empty()方法是三个静态方法。先上源码:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
//工厂方法
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
//构造方法
private Optional() {
this.value = null;
}
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
//静态常量
private static final Optional<?> EMPTY = new Optional<>()
复制代码
of()方法通过调用带参构造,new 出一个 Optional 对象,正常形参带值是不会有问题的,但是当形参为空时,设置 value 前的 Objects.requireNonNull()非空校验,就会抛出一个异常,代码如下:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
复制代码
requireNonNull()方法是 java.util 包下 Objects 类的一个方法,作用是检查传入的参数是否为空,为空会抛出一个 NPE,在 Optional 类中用到的地方还有很多。所以只有确信构造 Optional 所传入的参数不为空时才可使用 of()方法。
与 of()相对的还有一个 ofNullable()方法,该方法允许接受 null 值构造 Optional,当形参为 null 时,调用 empty()方法,而 empty()方法返回的是一个编译期就确定的常量 EMPTY,EMPTY 取值是无参构造器创建对象,最终得到的是一个 value 为空的 Optional 对象。
2)使用方法的对比
2.2)中说到,正常使用的方法中有属于取值类的方法,orElse()、orElseGet()和 orElseThrow(),这三个方法在非空时均返回 value,但是为空时的处理各不相同。先上源码:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
复制代码
orElse()和 orElseGet()方法最直观的差异是形参的不同,看下面一段代码:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
//测试语句
Optional<Chestnut> opChest = Optional.ofNullable(new Chestnut("桐柏板栗"));
//Optional<Chestnut> opChest = Optional.empty();
opChest.orElse(print("orELse"));
opChest.orElseGet(()->print("orElseGet"));
//调用方法
private static Chestnut print(String method){
System.out.println("燕山板栗最好吃----"+method);
return new Chestnut("燕山板栗");
}
复制代码
第一次,new 出一个“桐柏板栗”的 Optional,分别调用 orElse()和 orElseGet()方法,结果出现了两行的“燕山板栗最好吃”的输出,因为两个方法在 value 不为 null 时都会执行形参中的方法;
第二次,通过 empty()方法获得一个空栗子的 Optional,再调用 orElse()和 orElseGet()方法,结果居然还出现了一行“燕山板栗最好吃”的输出。
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
第一次输出:
燕山板栗最好吃----orELse
燕山板栗最好吃----orElseGet
第二次输出:
燕山板栗最好吃----orELse
复制代码
其原因是 orElseGet()的参数是 Supplier 目标类型的函数,简单来说,Suppiler 接口类似 Spring 的懒加载,声明之后并不会占用内存,只有执行了 get()方法之后,才会调用构造方法创建出对象,而 orElse()是快加载,即使没有调用,也会实际的运行。
这个特性在一些简单的方法上差距不大,但是当方法是一些执行密集型的调用时,比如远程调用,计算类或者查询类方法时,会损耗一定的性能。
orElseThrow()方法与 orElseGet()方法的参数都是函数类型的,这意味着这两种方法都是懒加载,但针对于必须要使用异常控制流程的场景,orElseThrow()会更加合适,因为可以控制异常类型,使得相比 NPE 会有更丰富的语义。
3)其他方法的对比
a、map 与 filterMap
先上源码:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}
复制代码
map()与 filterMap()的相同点是,都接受一个 Function 类型的函数,并且返回值都是 Optional 类型的数据。但是从源码中我们也能看出:
首先,map()在返回时,使用了 ofNullable()函数对返回值包了一层,这个函数在 2.1)已经说过,是一个 Optional 的工厂函数,作用是将一个数据包装成 Optional;而 filterMap()返回时只是做了非空校验,在应用 mapper.apply 时就已经是一个 Optional 类型的对象。
其次,从签名中也可以看出,map()的 Function 的输出值是"? extends U",这意味着在 mapper.apply()处理完成后,只要吐出一个 U 类型或者 U 类型的子类即可;而 filterMap()的 Functional 的输出值是“Optional<U>”,则在 mapper.apply()处理完成之后,返回的必须是一个 Optional 类型的数据。
b、ifPresent 与 ifPresentOrElse
ifPresentOrElse()方法是作为 ifPresent()的改进方法出现的。先看源码:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public void ifPresent(Consumer<? super T> action) {
if (value != null) {
action.accept(value);
}
}
public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) {
if (value != null) {
action.accept(value);
} else {
emptyAction.run();
}
}
复制代码
从源码中可以看出,ifPresentOrElse()参数增加了一个 Runnable 类型的函数 emptyAction,在 value != null 时,都激活了 action.accept()方法。只是当 value == null 时,ifPresentOrElse()方法还会调用 emptyAction.run()方法。所以总的来说,jdk1.9 加入 ifPresentOrElse()方法,是作为 ifPreset 在 if-else 领域的补充出现的。
c、or 与 orElse
同样作为改进的 or()方法也是为了解决 orElse 系列方法的“小缺点”出现的,先看源码:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier) {
Objects.requireNonNull(supplier);
if (isPresent()) {
return this;
} else {
@SuppressWarnings("unchecked")
Optional<T> r = (Optional<T>) supplier.get();
return Objects.requireNonNull(r);
}
}
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> supplier) {
return value != null ? value : supplier.get();
}
复制代码
or()方法在签名形式上更接近 orElseGet(),即形参都是 Supplier 类型的函数,但是与其不同的是,or()方法在形参中,指定了 Supplier 返回的类型必须为 Optional 类型,且 value 的类型必须为 T 或者 T 的子类。orElse 系列的方法,更像是一种消费的方法,从一个 Optional 的实例中“取出“value 的值进入下一步操作,而 or()方法则像是建造者模式,对 value 有一定的操作之后,重新吐出的还是 Optional 类型的数据,所以使用时可以串联在一起,后一个 or 处理前一个 or 吐出的 Optional。
4)“危险”方法的对比
这里指的“危险”指的是会抛出异常,毕竟引进 Optional 类的目的就是去除对 NPE 的判断,如果此时再抛出一个 NPE 或者其他的异常,没有处理好就会为程序引入不小的麻烦。所以对 Optional 中可能抛出异常的方法做一个总结。
首先,最直观的会抛出异常的方法就是 of()方法,因为 of 方法会调用带参构造创建实例,而带参构造中有对 value 非空的检查,如果空会抛出 NPE 异常;
其次,get()方法也是一个“危险”的方法,因为当不判空直接使用 get 取值时,会触发 get 中 NoSuchElementException 异常;
再次,orElseThrow()方法也会抛出异常,但是这种异常属于人为指定的异常,是为了使得异常情况的语义更加丰富,而人为设置的,是一种可控的异常;
最后,在一些方法中,设置了参数非空检查(Objects.requireNonNull()),这种检查会抛出 NPE 异常,除去已经提到的带参构造器,还有 filter、map、flatMap、or 这四个方法,如果传入的接口实例是 Null 值就会随时引爆 NPE。
4、误用形式与 Best practice
1)误用形式
a、初始化为 null
第一种误用形式是给 Optional 类型的变量初始化的时候.Optional 类型变量是默认不为空的,所以在取方法执行的时候才可以肆无忌惮"点"出去,如果在初始化的时候出现:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> chest = null;
复制代码
并且不及时为 chest 赋值,则还是容易出现 NPE,正确的初始化方式应该是:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Optional<Chestnut> chest = Optional.empty();
复制代码
b、简单判空
第二种比较常见的误用形式应该是使用 isPresent()做简单判空。原本的代码如下:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public String getName(Chestnut chestnut){
if(chestnut == null){
return "栗子不存在";
}else return chestnut.name();
}
复制代码
代码中,通过检查 chestnut == null 来处理为空时的情况,简单使用 isPresent()方法判空的代码如下:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public String getName(Chestnut chestnut){
Optional<Chestnut> opChest = Optional.ofNullable(chestnut);
if(!opChest.isPresent()){
return "栗子不存在";
}else return opChest.getname();
}
复制代码
酱婶儿并没有太大差别,所以在使用 Optional 时,首先应避免使用 Optional.isPresent()
来检查实例是否存在,因为这种方式和 null!= obj
没有区别也没什么意义。
c、简单 get
第三种比较常见的误用形式是使用 Optional.get()方式来获取 Optional 中 value 的值,get()方法中对 value==null 的情况有抛出异常,所以应该在做完非空校验之后再从 get 取值,或者十分确定 value 一定不为空,否则会出现 NoSuchElementException 的异常。相对的,如果不是很确信,则使用 orElse(),orElseGet(),orElseThrow()获得你的结果会更加合适。
d、作为属性字段和方法参数
第四种误用形式在初学 Optional 的时候容易碰到,当指定某个类中的属性,或者方法的参数为 Optional 的时候,idea 会给出如下提示:
Reports any uses of java.util.Optional<T>, java.util.OptionalDouble, java.util.OptionalInt, java.util.OptionalLong or com.google.common.base.Optional as the type for a field or parameter. Optional was designed to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result". Using a field with type java.util.Optional is also problematic if the class needs to be Serializable, which java.util.Optional is not.
大意是不建议如此使用 Optional。第一,不建议使用 Optional 作为字段或参数,其设计是为库方法返回类型提供一种有限的机制,而这种机制可以清晰的表示“没有结果”的语义;第二,Optional 没有实现 Serilazable,是不可被序列化的。
这种误用方法比较明显,复现和避免也比较简单。但笔者还想就这两个建议的原因做进一步的探究,所以查阅了一些资料,大体的原因如下:
第一个原因,为什么不适合做属性字段和方法参数?直白的说,就是麻烦。为了引入 Optional,却需要加入多段样板代码,比如判空操作。使得在不合适的位置使用 Optional 不仅没有给我们带来便利,反而约束了写代码的逻辑。
写以下域模型代码
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public class Chestnut {
private String firstName;
private Optional<String> midName = Optional.empty();
private String lastName;
public void setMidName(Optional<String> midName) {
this.midName = midName;
}
public String getFullName() {
String fullName = firstName;
if(midName != null) {
if(midName.isPresent()){
fullName = fullName.concat("." + midName.get());
}
return fullName.concat("." + lastName);
}
}
}
复制代码
可见在 setter 方法中没有对形参做相应的校验,那么则需要在使用的 getFullName()方法中,增加对属性 midName 的判空操作,因为完全可能通过 setter 方法使得属性为 null。如果把判空移到 setter 方法中,也并没有减少判空,使得平白挤进了一段样板代码。另外在传入方法时,也需要对原本的 value 包装一层后再传入,使得代码显得累赘了,如下:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
chest.setMidName(Optional.empty());
chest.setMidName(Optional.of("阿栗"));
复制代码
在属性不为 Optional 的时候,如果给属性赋值,需要使用“消费”操作,比如 orElse(),取出值再赋给属性,相比直接传入 String 类型的值作为字段和形参可以减少这些步骤,后者反而更加合适。
第二个原因,为什么没有实现序列化?相关可以参见 Java Lamda 的专家组讨论。
JDK 在序列化上比较特殊,需要同时兼顾向前和向后兼容,比如在 JDK7 中序列化的对象应该能够在 JDK8 中反序列化,反之亦然。并且,序列化依赖于对象的 identity 保持唯一性。当前 Optional 是引用类型的,但其被标记为 value-based class(基于值的类),并且有计划在今后的某一个 JDK 版本中实现为 value-based class,可见上图。如果被设计为可序列化,就将出现两个矛盾点:1)如果 Optional 可序列化,就不能将 Optional 实现为 value-based class,而必须是引用类型,2)否则将 value-based class 加入同一性的敏感操作(包含引用的相等性如:==,同一性的 hashcode 或者同步等),但是这个与当前已发布的 JDK 版本都是冲突的。所以综上,考虑到未来 JDK 的规划和实现的冲突,一开始就将 Optional 设置为不可序列化的,应该是最合适的方案了。
Value-Based Classes(基于值的类),以下是来自 Java doc的解释:
Value-based Classes
Some classes, such as java.util.Optional
and java.time.LocalDateTime
, are value-based. Instances of a value-based class:
1、are final and immutable (though may contain references to mutable objects);
2、have implementations of equals
, hashCode
, and toString
which are computed solely from the instance's state and not from its identity or the state of any other object or variable;
3、make no use of identity-sensitive operations such as reference equality (==
) between instances, identity hash code of instances, or synchronization on an instances's intrinsic lock;
4、are considered equal solely based on equals()
, not based on reference equality (==
);
5、do not have accessible constructors, but are instead instantiated through factory methods which make no committment as to the identity of returned instances;
6、are freely substitutable when equal, meaning that interchanging any two instances x
and y
that are equal according to equals()
in any computation or method invocation should produce no visible change in behavior.
A program may produce unpredictable results if it attempts to distinguish two references to equal values of a value-based class, whether directly via reference equality or indirectly via an appeal to synchronization, identity hashing, serialization, or any other identity-sensitive mechanism. Use of such identity-sensitive operations on instances of value-based classes may have unpredictable effects and should be avoided.
2)Best practice
实践中常常组合使用以上各种方法,且很多方法常与 Lambda 表达式结合,获取想要的结果,这里列出两个常见的使用方式,值类型转换和集合元素过滤。
a、示例一
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Chestnut chestnut = new Chestnut("锥栗板栗");
if(chestnut != null){
String chestName = chestnut.getName();
if(chestName != null){
return chestName.concat("好好吃!");
}
}else{
return null;
}
复制代码
可以简化成:
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
Chestnut chestnut = new Chestnut("锥栗板栗");
return Optional.ofNullable(chestnut)
.map(Chestnut::getName)
.map(name->name.concat("好好吃!"))
.orElse(null);
复制代码
b、示例二
PlainJavascriptJavaHTML/XMLMarkdownMakefileGoJSONSQLObjective-cYAMLBashPHPPython
public static void main(String[] args) {
// 创建一个栗子集合
List<Chestnut> chestList = new ArrayList<>();
// 创建几个栗子
Chestnut chest1 = new Chestnut("abc");
Chestnut chest2 = new Chestnut("efg");
Chestnut chest3 = null;
// 将栗子加入集合
chestList.add(chest1);
chestList.add(chest2);
chestList.add(chest3);
// 创建用于存储栗子名的集合
List<String> nameList = new ArrayList();
// 循环栗子列表获取栗子信息,值获取不为空且栗子名以‘a’开头
// 如果不符合条件就设置默认值,最后将符合条件的栗子名加入栗子名集合
for (Chestnut chest : chestList) {
nameList.add(Optional.ofNullable(chest)
.map(Chestnut::getName)
.filter(value -> value.startsWith("a"))
.orElse("未填写"));
}
// 输出栗子名集合中的值
System.out.println("通过 Optional 过滤的集合输出:");
nameList.stream().forEach(System.out::println);
}
复制代码
5、总结
本文首先,从所解决的问题开始,介绍了当前 NPE 处理所遇到的问题;然后,分类地介绍了 Optional 类中的方法并给出相应的示例;接着,从源码层面对几个常用的方法进行了对比;最后,列举出了几个常见的误用形式和 Best practice,结束了全文。
Optional 类具有:可以显式体现值可能为空的语义和隐藏可能存在空指针的不确定性的优点,但是同时也具有,适用范围不是很广(建议使用于返回值和 NPE 逻辑处理)以及使用时需要更多考量的缺点。
但是总体看来,Optional 类是伴随 Java8 函数式编程出现的一项新特性。为处理逻辑的实现提供了更多的选择,未来期待更多的实践和 best practice 出现,为 Optional 带来更多出场的机会。
6、参考
[1] https://mp.weixin.qq.com/s/q_WmD3oMvgPhakiPLAq-CQ 来源:wechat
[2] https://www.runoob.com/java/java8-optional-class.html 来源:菜鸟教程
[3] https://blog.csdn.net/qq_40741855/article/details/103251436 来源:CSDN
[4] https://yanbin.blog/java8-optional-several-common-incorrect-usages/#more-8824 来源:blog
[5] https://www.javaspecialists.eu/archive/Issue238-java.util.Optional---Short-Tutorial-by-Example.html 来源:java specialists
[6] Java核心技术 卷II - Java8 的流库 - Optional 类型
[7] https://www.zhihu.com/question/444199629/answer/1729637041 来源:知乎
如有不当之处,望指正~
作者:历子谦
评论