首页 > 编程语言 > 详细

JAVA8 使用Optional

时间:2021-01-05 15:14:52      阅读:29      评论:0      收藏:0      [点我收藏+]

Java8实战 第十章--用Optional取代null

一.使用null有什么样的问题

假设要处理下面这样嵌套的对象,这是一个拥有汽车及汽车保险的客户

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; }
}

那么,下面这段代码存在怎样的问题呢?

public String getCarInsuranceName(Person person) {
    return person.getCar().getInsurance().getName();
}

这段代码看起来没什么问题。但是现实生活中,不是每个人都有车。所以在实践中,比较常见的做法是返回一个null引用,表示该值的缺失,即用户没有车。而接下来,null.getInsurance()会导致NPE。

这还不是全部,如果person的值为null会怎样?如果getInsurance的返回值也是null会怎样?

采用防御式检查减少NullPointerException

平常编码时,我们习惯这么来避免NPE

public String getCarInsuranceName(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();
}

然而,这种方案不是很理想,现在方法有4个不同的退出点,使得代码维护变得艰难,之后每个可能的null检查都会新增一个退出点。

null带来的问题

  • 它是错误之源

    • NPE是目前Java程序开发中最典型的异常
  • 它会使你的代码膨胀

    • 它会使你的代码充斥着深度嵌套或者篇幅很长的null检查,代码可读性糟糕。
  • 它自身是毫无意义的

    • null自身没有任何语义,它代表静态类型语言中以一种错误的方式对缺失变量值的建模
  • 它破坏了Java的哲学

    • Java一直试图避免让程序员意识到指针的存在,唯一的例外是null指针。
  • 它在Java的类型系统上开了个口子

    • null并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题,因为当这个变量被传递到系统的另一个部分后,你将无法获知这个null变量最初的赋值到底是什么类型。

二.Optional类入门

Java8引入了一个新的类java.util.Optional<T>,这是一个封装Optional值的类。这个类对可能缺失的值进行建模,变量存在时,Optional类只是对类简单封装;变量不存在时,缺失的值会被建模为一个“空”的Optional对象,由方法Optional.empty()返回。

比如如果你知道一个人可能有也可能没有车,那么Person类中的成员变量car就不该声明为Car,而是应该声明为Optional<Car>

null引用和Optional.empty()有什么本质的区别吗?

首先在语义上可以把它们当作是一回事,但是实际中它们的差别非常大:如果你尝试解引用一个null,那么一定会触发NPE,但使用Optional.empty()就完全没事,因为它是Optional类的一个有效对象。

放在上面的例子中,我们在声明变量时使用Optional<Car>类型,而不是Car类型,这句声明非常清楚地表面了这里允许发生变量缺失,其他程序员看到这里的代码后会留意变量缺失的情况。若是直接使用Car这样的类型,后来人看到之后需要根据自己对业务的理解,来猜测这里变量会不会有null的情况。

由此,现在使用Optional类对上面的类定义代码进行重构,来丰富模型的语义

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 String name;
    public String getName() { return name; }
}

这下代码要表达的意思就很清楚了,清晰地表达了一个person可能拥有也可能没有car的情形,同样,car可能上了保险,也可能没保险。

与此同时,看到保险公司的名称被声明为String类型而不是Optional<Stirng>类型,这也非常清晰地表面每个保险公司必须有名称,这是非常合理的。使用这种方式,一旦insurance.getName发生NPE,就可以非常确定地知道数据出错了,需要调查下数据的问题,而不再需要添加null的检查,因为null的检查只会掩盖问题,并未真正修复问题。

在代码中始终如一地使用Optional,能非常清晰地界定出变量值的缺失是结构上的问题,还是算法上的缺陷,抑或是数据的问题。

三.应用Optional的几种模式

3.1 创建Optional对象

  1. 声明一个空的Optional

    可以通过静态工厂方法Optional.empty,创建一个空的Optional对象

Optional<Car> optCar = Optional.empty();
  1. 依据一个非空值创建Optional

    使用静态工厂方法Optional.of,依据一个非空值创建一个Optional对象

Optional<Car> optCar = Optional.of(car);
// 如果car是一个null,这段代码会立即抛出NPE
  1. 可接受null的Optional
    使用静态工厂方法Optional.ofNullable,可以创建一个允许null值的Optional对象
Optional<Car> optCar = Optional.ofNullable(car);
// 如果car是null,那么得到的Optional对象就是空对象

3.2 使用 map 从 Optional 对象中提取和转换值

Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance:getName);

概念上,这里的map转换和流相差无几。如果Optional包含一个值,那函数就将该值作为参数传递给map,对它进行转换。如果Optional为空,则什么都不做。

技术分享图片

3.3 使用flatMap链接Optional对象

根据3.2,第一反应是我们可以利用map重写之前的代码如下

Optional<Person> optPerson = Optional.of(person);
Optional<String> name = 
        optPerson.map(Person::getCar)
                 .map(Car::getInsurance)
                 .map(Insurance::getName);

但这段代码无法通过编译。因为optPerson是Optional<Person>类型的变量,调用getCar后返回的是一个Optonal<Car>类型的对象,这意味这第一个map操作的结果是一个Optional<Optional<Car>>类型的对象,因此它对getInsurance的调用是非法的。

该问题的解决和流上的解决方法是类似的。使用flatMap方法,可以将Optional嵌套对象转换为单一Optional对象。

技术分享图片

在有了3.2以及3.3的知识了解后,我们可以进行一些应用了。使用Optional获取car的保险公司名称

public String getCarInsuranceName(Optional<Person> person) {
    return person.flatMap(Person::getCar)
                 .flatMap(Car::getInsurance)
                 .map(Insurance::getName)
                 .orElse("Unknown")
}

从这段代码中可以看到,在处理潜在可能缺失的值时,使用Optional有明显优势。可以用非常容易却普适的方法实现之前期望的效果--不再需要使用那么多的条件分支,也不会增加代码的复杂性。并且,它通过类型系统让你的域模型中隐藏的知识显式地体现在你的代码中。

下图对这种流水线式的操作进行了说明

技术分享图片

3.4 默认行为以及解引用Optional对象

Optional类提供了多种方法读取Optional实例中的变量值

  • get()是这些方法中最简单又最不安全的方法。如果变量存在,它直接返回封装的变量值,否则就抛出一个NoSuchElementException异常。
  • orElse(T other)。它允许在Optional对象不包含值时提供一个默认值。
  • orElseGet(Supplier<? extends T> other)是orElse方法的延迟调用版,Supplier方法只有在Optional对象不含值时才执行调用。如果创建默认值是一个耗时费力的工作,应该考虑采用这种方式。
  • orElseThrow(Supplier<? extends X> exceptionSupplier)和get方法非常类似,它们遭遇Optional对象为空时都会抛出一个异常,但使用orElseThrow可以定制希望抛出的异常类型。
  • ifPresent(Consumer<? super T>)让你能在变量值存在时执行一个作为参数传入的方法,否则就不进行任何操作。

3.5 两个Optional对象的组合

比如我们想实现一个加法的函数,但输入可能为空,为了实现一个null-安全的版本,我们可能会像下面这么写。其中,Optional类提供了一个isPresent方法,如果Optional对象包含值,该方法就返回true。

public Optional<Integer> nullSafeCount(Optional<Integer> i1, Optional<Integer> i2) {
    if (i1.isPresent() && i2.isPresent()) {
        return Optional.of(i1.get() + i2.get());
    } else {
        return Optional.empty();
    }
}

这个方法有明显的优势,即我们从它的签名就能得知两个Integer的值都可能为空。但该方法的实现和之前实现的null检查太相似了,我们需要更优雅的解决方案。

我们可以结合之前的map和flatMap方法,用一行语句实现该方法如下

public Optional<Integer> nullSafeCount(Optional<Integer> i1, Optional<Integer> i2) {
    return i1.flatMap(i -> i2.map(j -> i + j));
}

这段代码中,对第一个Optional对象调用flatMap方法,如果它是个空值,传递给它的Lambda表达式就不会执行,这次调用会直接返回一个空的Optional对象。反之,如果i1有值,这次调用就会将其作为函数的输入。这个函数的函数体会对第二个Optional对象执行map操作,如果i2是空值,函数就会返回一个空的Optional对象,整个nullSafeCount方法的返回值也是一个空的Optional对象。最后,如果i1和i2都有值,作为参数传递给map方法的Lambda表达式能够计算,完成期望的操作。

3.6 使用filter剔除特定的值

filter方法和流中的使用非常类似,接受一个谓词作为参数。如果Optional对象的值为空,它不做任何操作,反之,它就对Optional对象中包含的值施加谓词操作。如果该操作的结果为true,它不做任何改变,直接返回该Optional对象,否则就将该值过滤掉,将Optional的值置空。

假设在上面定义的Person/Car/Insurance模型中,Person还提供了一个方法可以取得Person对象的年龄,使用下面的签名改写getCarInsuranceName方法,找出年龄大于或者等于minAge参数的Person所对应的保险公司列表。

public String getCarInsuranceName(Optional<Person> person, int minAge)

其实就是对Person进行filter操作,设置相应的条件谓词,代码如下

public String getCarInsuranceName(Optional<Person> person,int minAge) {
    return person.filter(p -> p.getAge >= minAge)
                 .flatMap(Person::getCar)
                 .flatMap(Car::getInsurance)
                 .map(Insurance::getName)
                 .orElse("Unknown")
}

下表对Optional类中的方法进行了分类和概括

方法 描述
empty 返回一个空的Optional实例
filter 如果值存在并满足提供的谓词,就返回包含该值的Optional对象,否则返回一个空的Optional对象
flatMap 如果值存在,就对该值执行提供的mapping函数调用,返回一个Optional类型的值,否则就返回一个空的Optional对象
get 如果该值存在,将该值用Optional封装返回,否则抛出异常
ifPresent 如果值存在,就执行使用该值的方法调用,否则什么都不做
isPresent 如果值存在就返回true,否则返回false
map 如果值存在,就对该值只想提供的mapping函数调用
of 将指定值用Optional封装后返回,如果该值为null,抛NPE
ofNullable 将指定值用Optional封装后返回,如果该值为null,则返回一个空的Optional对象
orElse 如果有值则将其返回,否则返回一个默认值
orElseGet 如果有值则将其返回。否则返回一个由指定的Supplier接口生成的值
orElseThrow 如果有值则将其返回。否则返回一个由指定的Supplier接口生成的异常

JAVA8 使用Optional

原文:https://www.cnblogs.com/yanch01/p/java8-optional.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!