java8 Optional【原创】

关于java8 Optional

文档版本:v1.0版本

和C/C++不一样,java从一开始就尝试将指针彻底的包装起来,所有关于指针的操作都由底层的jvm完成,java程序员只需要知道引用对象和null就ok。

但是这个null确实也没少让程序员头疼,每天不遇到几个NullPointException(NPE)都感觉今天是不是不太正常。那如何彻底的从技术和思维上解决这个让人头疼的问题就很值的探讨。

null的问题

java开发的同学都很清楚,实际的开发中很多场景都会遇到NPE:

  • 隐晦的自动拆箱NPE
  • 数据库查询结果为null导致的NPE
  • 集合内部元素为null引起NPE
  • session中获取数据为null导致NPE
  • 级联调用NPE

说白了只要尝试调用实际为null的对象的属性或者方法都会导致NPE。

如下下面这段代码(示例1):

public class Demo1 {

    /**
     * 获取人对应的车的保险的名称
     * @param person
     * @return
     */
    public String getName(Person person) {
        return person.getCar().getInsurance().getName();
    }
}

其中数据model如下,后面也会复用(示例2)

public class Person {

    private Car car;

    public Car getCar() {
        return car;
    }
}

public class Car {

    private Insurance insurance;

    public Insurance getInsurance() {
        return insurance;
    }
}

public class Insurance {

    private String name;

    public String getName() {
        return name;
    }
}

这段代码从业务功能上讨论其实没啥问题。但是实际情况是调用此代码的人不确定会扔什么样的数据进来,只要 personcarinsurace 三个bean中有任何一个为null,都会导致NPE。实际开发中,这种NPE场景也是最为常见和多发的。

防御式检查

如果避免上述操作导致NPE的问题,常见的做法如下,也叫做防御式检查:

/**
 * 简单的防御式检查
 * @param person
 * @return
 */
public String getName2(Person person) {
    if (person != null) {
        Car car = person.getCar();
        if (car != null) {
            Insurance insurance = car.getInsurance();
            if (insurance != null) {
                return insurance.getName();
            }
        }
    }
    return "Unknown";
}

通过一层层的检查,直到完全确认insurance不为null时,才返回正确的name,否则返回“unknown”。但是这段代码在实际维护中其实会让人头疼,因为层层嵌套的金字塔式的代码看起来不美观,而且不容易理解。尝试改进如下:

/**
 * 获取name3
 * @param person
 * @return
 */
public String getName3(Person person) {
    if (person == null) {
        return "Unknown";
    }
    Car car = person.getCar();
    if (car == null) {
        return "Unknown";
    }
    Insurance insurance = car.getInsurance();
    if (insurance == null) {
        return "Unknown";
    }
    return insurance.getName();
}

这段代码尝试做了一点点改进,将多层嵌套的大括号代码进行拆解,使得所有的逻辑都在一个层面,使用截断式的判断,只要检查到对象为null,就返回错误信息。稍微比上面的好一点,但是也会存在问题。多行重复的操作,很实容易写错,而且后期维护的时候,任何一处细小的改动都需要所有地方同步进行更新,否则就会出现预期之外的结果返回,也很麻烦。最重要的是,也没有彻底解决null的问题,甚至为了处理null,引入的安全检查代码看起来比业务代码还多,稍微有点啰嗦。

了解Optional

铺垫了这么多,其实就想说明一个问题,null很头疼,咋办?

java8中引入的新的类:Optional,可以帮助我们更好的去处理null

Optional 示例(引用自java8 实战)

Optional的示例如图,简单来说就是当变量存在时,Optional类只是包装了一层,而当变量为null时,就建立了一个“空”的Optional对象,当然此时如果调用optional.get()方法,Optional还是会扔出NPE,为空时通常应该调用Optional.empty()

Optional.empty()

empty()其实是一个静态的工厂方法,返回一个“空”的Optional对象。看起来和调用空对象没什么区别,但是实际运行中却有着质的却别,null对象的调用会引起NPE,导致程序奔溃,而empty()就完全没问题。

Optional类结构如图:


Optional类 结构

简单说明一下基础方法,后面会对每一个方法和对应的思路详细进行说明和举例:

  • empty() 返回“空”Optional对象,静态方法
  • of(T value)constructor 一样,创建Optional对象
  • ofNullable(T value) 传入的参数为null时返回“空”Optional,否则返回包装好valueOptional对象
  • isPresent() 判断Optional包装的对象是否为空
  • get()获取Optional包装的对象,包装的对象为null时产生NPE
  • orElse() 为空时返回指定的参数,否则返回内部包装的对象
  • map() 执行指定的“转换”方法,返回null时,可以包装为“空”的Optional对象
  • filter() 基于Optional,对对象的值进行安全的检查和过滤
  • flatMap(T, Optional<U>) 先简单来说吧,有点流模式的包装转换器,后面这个是重点需要关注的对象

首先划重点:如下的使用Optional本质上其实和文中刚开始提到的防御式检查没有任何区别,而且也是完全不推荐这么写:

/**
 * 获取car
 * @param person
 * @return
 */
public Car getCar(Optional<Person> person) {
    
    if (person.isPresent()) {
        return person.get().getCar();
    }
    return null;
}

从业务层面去理解Optional应该是这样的:被Optional包装的对象在业务上允许为null,因此我们将其包装为Optional对象。因为在实际调用中如果Optional对象包装的对象为null,进行相关的调用就不会报NPE,更不需要做一系列的防御式检查(后文会举例),同时也可以精准的传达给调用者一个明确的信息,此对象允许为null。但是并不是所有的业务场景都适用,如果业务要求某个对象必须不为空且有值,此时就不应该使用Optional进行包装,如果产生NPE说明业务代码有问题或者数据有异常,应该在开发阶段就将其fix,而不是使用Optional掩盖为null的事实。

创建Optional对象

创建Optional对象的三种方式

// 创建一个空的Optional
Optional<Car> optCar = Optional.empty();
// 依据非空的对象,创建一个Optional对象
Optional<Car> optCar1 = Optional.of(new Car());
// 依据允许为空的对象,创建一个Optional对象
Optional<Car> optCar2 = Optional.ofNullable(null);

map():从Optional对象中提取和转换值

Optional既然是一个包装对象,那从包装对象中提取对象或者对象的值就是一个常规操作。从前面的基础方法介绍中可知,get()方法是从Optional对象中提取包装对象的基础方法,但是get方法不安全,因为如果包装的对象为null,则会报空指针异常。这个时候map就显得很有用。

map方法源码

从源码可以看出,当Optional对象为“空”时,map什么也不做,返回一个空的Optional对象,否则执行传入的Function,得到结果并包装为Optional对象返回。

思考:至此,我们可以看出,Optionalmap()操作其实和流中的map从模式上来说是一致的,甚至可以把Optioanl看做一个特殊的Stream(最多只能包含一个元素的流),map方法遍历流中的每一个元素,进行某种转换操作(转换操作即输入一个元素A,进行某些操作后返回B),基于Optional这个解释也是ok的。

简单的例子,从insurance中获取名称:

public String getName2(Insurance insurance) {
    if (Objects.nonNull(insurance)) {
        return insurance.getName();
    }
    return null;
}

可以改写为:

public Optional<String> getName(Insurance insurance) {
    Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
    return optInsurance.map(Insurance::getName);
}

关于map就讲这么多。在文章开始的时候,我们举了一个链式调用例子:

获取保险的名称 demo

现在我们学习了map,理所当然我们可以改写成如下方式:

链式调用改造 demo

上述代码在model为示例2的情况下是可以正常运行的,而且实现的很标准。但是这个地方有一个限制存在,如果model如下,这个段代码是没有办法通过编译了:

public class Person {

    private Optional<Car> car;

    public Optional<Car> getCar() {
        return car;
    }
}

public class Car {

    private Optional<Insurance> insurance;

    public Optional<Insurance> getInsurance() {
        return insurance;
    }
}

public class Insurance {

    private Optional<String> name;

    public Insurance(Optional<String> name) {
        this.name = name;
    }

    public Insurance() {}

    public Optional<String> getName() {
        return name;
    }

    public void setName(Optional<String> name) {
        this.name = name;
    }
}

因为第一次调用getCar之后,实际获取到的是Optional<Car>,再通过map包装后就演变成了Optional<Optional<Car>>, 显然这个对象是没有办法引用getInsurance方法的,因此就会报错。总结一下就是说,map在某些场景下可能会存在过度包装的情况。 当然了,这个问题也是可以解决的,这个时候就需要flatMap()方法了

map 会存在过度包装的情况

flatMap()

上面我们讨论了,mapOptional嵌套的时候就略显乏力的,这个时候就需要flatMap来对嵌套的Optional来归一,使其转换为只有一层Optional的包装对象。

首先对上面遇到的问题进行改动,再对实现来进行说明:

public Optional<String> getName2(Person person) {
    Optional<Person> optPerson = Optional.ofNullable(person);

    return optPerson.flatMap(Person::getCar)
            .flatMap(Car::getInsurance)
            .map(Insurance::getName);
}

map我们已经说明了,提取Optional包装的对象或者对象的属性,但是当转换方法直接返回Optional包装对象时就会因为过度包装而变得难以处理。flatMapmap的不同之处在于,flatMap判断内部包装的对象不为空时,会将内部包装的对象传入Function中当做参数执行,且期望内部包装的对象为Optional包装对象。

flatMap源码

上述的解释有点直白,其实想表达的意思就是,flatMap会将多层的Optional对象合并为一个。当然,这里也可以类比流中的flatMap来理解。流中的map是一对一的映射,而flatMap就相当于一对多的映射。

流中flatMapinput Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终output 的新Stream 里面已经没有 List了,都是直接的数字。比如:

Stream<List<Integer>> inputStream = Stream.of(
 Arrays.asList(1),
 Arrays.asList(2, 3),
 Arrays.asList(4, 5, 6)
 );
Stream<Integer> outputStream = inputStream.
flatMap((childList) -> childList.stream());

同理,将Optional看做是一个元素数量最多为1的集合,这里多对一就体现在Optional的双层嵌套了。

注意,这里我将描述改为了双层嵌套,因为flatMap只支持双层,当超过双层的时候,flatMap也没办法了。请不要怀疑为什么没有可以处理超过两层嵌套的这样的方法提供,这种情况发生的时候,应该怀疑一下自己哪里是不是写冗余了,从而导致产生了畸形包装数据,而不是去尝试解构三层解构。

一个错误的示范

Optional的默认行为

实际开发中,我们总是有这样一个需求,当Optional包装对象在一些连续调用和操作后如果有值则返回内部的值,否则返回指定的参数或者执行指定的方法。相当于一个else逻辑,但是写if else不是我们期望的,java8的宗旨就是简化代码逻辑,使其清晰可见。说句装逼的话就是:没有java8一句话实现不了的逻辑,哈哈哈哈哈哈,一个点不够就再多几个点上。

比如刚刚刚刚flatMap的例子我们想要获取的是String而不是Optional<String>的时候, 就可以指定一个默认的行为。

image.png
  • orElse(T value): 当Optional为空的时候,返回value,否则返回内部的包装对象
  • orElseGet(Supplier<? extends T> other) :相当于orElse的延迟调用版,当默认指定返回的对象是一个费时费力操作的时候,就应该考虑lasy初始化,以此来提高程序的性能。
  • orElseThrow(Supplier<? extends X> exceptionSupplier) throws X当然如果有复杂的操作或者可能会产生异常的时候,调用orElseThrow则更合适。

filter

读到这里你可能发现了,抛开知识层面的东西不谈,Optional一个很有特色的特点就是我上文说道的,没有一句话解决不了的逻辑,如果不行,就多点(.XXXMethod())几次。这个也是java8的特色,链式调用。

当一个链式操作很长的时候,我们在很多场景下就不可避免的需要中间某处添加一些判断逻辑,来提前结束不符合业务条件的参数的相关操作,避免后续的业务操作发生错误或者进行无用的运算。这里filter就可以提供支持,来让我们在链式调用中添加一些谓词判断逻辑,满足我们的业务需求。

举个例子,我们之前一直在获取保险的名称。这个时候业务的同学告诉你实际业务的需求变为:之前的逻辑不要了,现在需要对名称为[人寿保险]的保险进行一些业务操作。在这种一改再改的情况下,虽然你心里妈卖批,但是你的代码还是得优雅的实现。

public void doSomethingToRenshou(Person person) {
    Optional<Person> optPerson = Optional.ofNullable(person);

    optPerson.flatMap(Person::getCar)
            .flatMap(Car::getInsurance)
            .map(Insurance::getName)
            .filter(insuranceName -> "人寿保险".equals(insuranceName))
            .ifPresent(insurance -> {
                    //如果满足上面的谓词逻辑,则进行一些操作
                System.out.println("ok, i`m fine!");
            });

}

实际应用

上面都是针对某一个特定的场景来对Optional的方法或者写法来进行说明,但是实际应用中可能需要更加复杂的操作才能满足业务需求。这里的复杂指的并不是实现逻辑,而是说我们需要使用Optional中上述说的一种或者两种技巧来尽量优雅的实现更加的复杂的逻辑。

1. 合并两个Optional对象

Optional<U> do(Optional<T> a, Optional<K> b);

如果方法申明如上所示,即:方法入参上接收两个Optional对象,最后返回一个Optional包装对象,这个时候大多数人都是这么写的:

/**
 * 错误的示范
 * @param person
 * @param car
 * @return
 */
public Optional<Insurance> doSomething(Optional<Person> person, Optional<Car> car) {
    if (person.isPresent() && car.isPresent()) {
        // 我也不知道拿到一个人和车能干啥
        // 假设这里进行了一了一些操作
        return this.findSomething(person.get() , car.get());
    }
    return Optional.empty();
}

经过我看书发现,正确的实现姿势应该是这样:

/**
 * 正确的示范
 * @param person
 * @param car
 * @return
 */
public Optional<Insurance> doSomething2(Optional<Person> person, Optional<Car> car) {
    return person.flatMap(realPerson -> car.flatMap(realCar-> this.findSomething(realPerson, realCar)));
}

这里的实现逻辑第一眼看可能有点绕,但是却十分巧妙。大概做一下解释。

首先思考我们的业务逻辑,人和车 都存在的时候才需要进行一些操作,否则返回空的Optional对象。这里首先第一个是person对象调用flatMap方法,如果此时person是一个空的Optional对象,就什么都不做,直接返回空的Optional对象结束方法,否则将person作为参数,传入后续的操作中。这第一步逻辑很容易理解,也是flatMap的标准操作。然后方法接口中是一个lambda表达式,表达式主体是car调用flatMap方法,此时如果car也存在,则personcar都会作为参数传递给findSomething进行业务操作,否则这个lamdba表达式执行到这里也就结束了,即返回一个空的Optional对象。

2. 用Optional包装已经存在的代码

很多时候,我们需要接收别人代码。而这些代码可能不太符合我们现在提倡的用Optional包装可能为null的对象这一标准,甚至那些代码是基于java7或者更早的版本来实现的,这个时候就需要我们使用装饰模式来对这些老的代码进行包装,使之符合我们正在使用的实现规范,从而顺利的接入新的代码。

首先,明确一点尽量别写if else!!!

public Optional<Object> wrap(Map<String, Object> map) {
        
    Optional<Object> newObject = Optional.ofNullable(map.get("key"));
    return newObject;
}

这里的例子比较简单,但是想表达观点很明确,如果你的代码需要兼容返回null的方法,那就使用兼容null的相关Optional方法来进行操作。如果业务上不允许产生null,那就使用强制非空的方法来进行包装。

3. 关于异常

Optional的使命是来负责解决null和NPE的,当实际生成中遇到了其他的异常逻辑的时候,我们可能需要抽象出Util方法来对常用的操作来进行包装,当产生其他业务异常时或者数据操作异常时,抓住异常,返回空的Optional,从而兼容异常情况,避免代码在实际运行中直接boom。

思考

贯穿全文我想表达几点:

  1. 尽量的避免多重代码块的嵌套,简化代码操作上的逻辑。这一点在实际开发中是十分必要的。
  2. Optional的出现是让我们可以顺畅的完成业务逻辑且可以优雅的避免掉大量的防御式检查。一开始可能难以接受这种操作,但是我觉得首先需要从思维上接受这种操作其实才是重要的,这样就可以将更多的注意力转移到实现业务逻辑上,而不是书写大量的防御式代码。
  3. Optional 从设计上和Stream十分类似,很多时候我们可以类比着Stream来进行理解和学习,可以帮助我们更好的去理解Optional为什么这么设计。

好了,Optional的东西大概就这么多。如果有错误或者不足的地方希望大家可以指出。以上所有的代码都托管在github上:https://github.com/xiaopihai7256/OptionalDemo

参考


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,082评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,231评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,047评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,977评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,893评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,976评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,605评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,888评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,906评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,732评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,513评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,980评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,027评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,232评论 2 339

推荐阅读更多精彩内容

  • Java中常见的NPE错误真实伤害了一大推程序猿,不过JDK8之后,终于出现了,一个可以解决这个问题的API,这个...
    Chinesszz阅读 1,166评论 0 1
  • 自古以来, Java 开发者们都会遇到一个让人又爱又恨的异常: NullPointException, 为了解决这...
    金明浩KS阅读 669评论 0 1
  • 富人与穷人差的是财“商” 看到《穷爸爸与富爸爸》后,有种醍醐灌顶的感觉。迫不及待的用两天晚上读完。这本书彻底颠覆了...
    季小童阅读 241评论 0 0
  • 《壹》 前些时日,某位侦探接到一份委托,跟踪拍摄一位老太太的日常生活,委托人还特意格外强调了,不允许老太太出现任何...
    天空l阅读 840评论 0 50
  • 大家好我叫张宵寒,今天我给大家介绍的口头作文题目是:捉金蝉,大家熟悉金蝉吗?经常是一种会在树上爬也会在洞里...
    美丽彩虹花阅读 217评论 0 0