java.util.Optional——让你远离NullPointerException的利器

Demon.Lee 2020年11月30日 1,532次浏览

读书笔记:《Java实战(第2版)》第11章 用Optional取代null

第一次接触Optional

印象中,我第一次接触Optional是在写某个DAO层Repository时。我发现findByXXX(…)方法返回的不是具体的POJO,而是一个 Optional,我当时就纳闷了,这是个啥,为啥要这样?但当时的我,没有更进一步的追问,便放过它了,也许这就是我跟优秀同行之间的差距吧。

下面是spring framework中的CrudRepository接口的代码,我们可以看到findById(…)方法的函数签名就是:Optional findById(ID var1);

package org.springframework.data.repository;

import java.util.Optional;

@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S var1);

    <S extends T> Iterable<S> saveAll(Iterable<S> var1);

    Optional<T> findById(ID var1);

    boolean existsById(ID var1);

    Iterable<T> findAll();

    Iterable<T> findAllById(Iterable<ID> var1);

    long count();

    void deleteById(ID var1);

    void delete(T var1);

    void deleteAll(Iterable<? extends T> var1);

    void deleteAll();
}

null的由来

1965年,英国计算机科学家Tony Hoare在设计ALGOL W语言时提出了null引用的想法:用null来为不存在的值建模。多年以后,他为自己的这个设计后悔不已。实际上,null带来的问题远超他想象。因为老的语言有这个设计,新的语言为了兼容不得不采用相同的设计:C,C++, Java, Python, Go等等都是如此。

为数不多的几个例外有:Haskell,ML等函数式语言。

恼人的NullPointerException

现实中,当我们不确定一个对象是否存在时,我们不得不增加额外的代码来保证程序运行过程中不会出现NullPointerException。常见的防御性方式有两种:

  • 深层质疑–使用if多层嵌套
private int getGpuUsedNumOfV1Container(V1Container container) {
    int usedNum = 0;
    V1ResourceRequirements resources = container.getResources();
    if (null != resources) {
        Map<String, Quantity> limits = resources.getLimits();
        if (null != limits) {
            Quantity quantity = limits.get("gpu");
            if(null != quantity){
                if(null != quantity.getNumber()){
                    usedNum = quantity.getNumber().intValue();
                }
            }
        }
    }
    return usedNum;
}
  • 使用return提前返回–过多的退出语句
private int getGpuUsedNumOfV1Container(V1Container container) {
    int usedNum = 0;
    V1ResourceRequirements resources = container.getResources();
    if (null == resources) {
        return 0;
    }
    Map<String, Quantity> limits = resources.getLimits();
    if (null == limits) {
        return 0;
    }
    Quantity quantity = limits.get("gpu");
    if(null == quantity){
        return 0;
    }
    if(null == quantity.getNumber()){
       return 0;
    }
    return quantity.getNumber().intValue();
}

有没有感觉到一丝丝代码的坏味道,但我们好像也没有更好的解决方案。

总结一下null带来的问题:

  1. 因为静态语言针对不存在的值设计的缺陷,java不得不继承了这一缺陷,从而违反了java设计的初衷:不向开发者暴露指针。但现在暴露了唯一的指针:null指针。
  2. 代码量膨胀。
  3. null自身没有任何语义,而且任何类型都可以被赋值为null。这就绕开了类型系统,当向接收一个第三方系统传来的变量是null时,可能我们无法知道最初它代表的是什么类型。

Optional的解决方案

针对上面null所带来的问题,Java 8正式引入了java.util.Optional类。

初识Optional

这个类设计不复杂,我们简单来看一看:

public final class Optional<T> {

    private static final Optional<?> EMPTY = new Optional<>();

    private final T value;

    private Optional() {
        this.value = null;
    }

    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }

    private Optional(T value) {
        this.value = Objects.requireNonNull(value);
    }

    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 T get() {
        if (value == null) {
            throw new NoSuchElementException("No value present");
        }
        return value;
    }


    public boolean isPresent() {
        return value != null;
    }

    /**
     * If a value is  not present, returns {@code true}, otherwise
     * {@code false}.
     *
     * @return  {@code true} if a value is not present, otherwise {@code false}
     * @since   11
     */
    public boolean isEmpty() {
        return value == null;
    }
    
    ...
    ...
    
    public T orElse(T other) {
        return value != null ? value : other;
    }
    ...
    ...
    public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
        if (value != null) {
            return value;
        } else {
            throw exceptionSupplier.get();
        }
    }
    ...
    ...

简单总结一下:

  1. 该类使用final进行了修饰,也就是说它不能被继承,不想被人改变自身的逻辑。
  2. 类中就一个属性:value,用来存放原始对象的值。
  3. 定义了一个不变的对象:EMPTY,用来表示空值。
  4. 提供了静态方法of(T value)、ofNullable(T value)进行实例化,前者校验原始对象的值不能为空,后者不会。
  5. 提供了isPresent()用来判断原始对象的值是否存在,并在Java 11引入了判断空的isEmpty()。
  6. orElse(T other) :如果原始对象值为空,返回给定的默认值,否则返回原值,这个方法也经常使用。
  7. orElseThrow(…) 与上面的orElse(…)相似,只是原始对象为空时,会抛出一个异常。

其他还有类似于流相关的操作方法,如map、flatmap、filter等等,可以自行查看源码了解。
image

实战

理论的东西终归是停留在纸上,我们动动手,通过几个例子来看看Optional是如何规避NullPointerException的。

这里我们依然使用《Java实战(第2版)》中的例子来说明。

先是领域对象,Person可能有一辆car,但一定有年龄age;Car可能买了保险insurance;Insurance保险一定有名字。代码如下:

/**
 * Person类
 */
public class Person {

    // private Optional<Car> car;
    private Car car;
    private int age;

    public Person(Car car, int age) {
        this.car = car;
        this.age = age;
    }

    // 注意返回的是:Optional<Car>
    public Optional<Car> getCarAsOptional() {
        return Optional.ofNullable(car);
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "car=" + car +
                ", age=" + age +
                '}';
    }
}

/**
 * Car类
 */
public class Car {

    // private Optional<Insurance> insurance;
    private Insurance insurance;

    public Car(Insurance insurance) {
        this.insurance = insurance;
    }

    // 注意返回的是:Optional<Insurance>
    public Optional<Insurance> getInsuranceAsOptional() {
        return Optional.ofNullable(insurance);
    }

    @Override
    public String toString() {
        return "Car{" +
                "insurance=" + insurance +
                '}';
    }
}

/**
 * Insurance类
 */
public class Insurance {

    private String name;

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

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Insurance{" +
                "name='" + name + '\'' +
                '}';
    }
}

  • 使用Optional 作为类的字段?

Java实战(第2版)》中给我们提供了一种思路,即定义属性时,直接将属性定义为Optional t,即上述代码中Person类和Car类中注释掉的car和insurance。但是,我在这里并没有使用这种方式,而是在get方法返回时,使用Optional进行了包装,即getxxxAsOptional()方法所示。之所以要这么做,是因为在POJO中将Optional作为类的字段使用的话,它不支持序列化(没有实现java.io.Serializable接口)。为啥不支持,因为Java研发人员当初在设计Optional时,仅仅是要支持能返回Optional对象的语法,而不是要作为类的字段使用。如果我们强制使用,则会报错java.io.NotSerializableException: java.util.Optional,比如:

public class Person2 implements Serializable {

    private Optional<Car> car;
    private int age;

    public Person2(Optional<Car> car, int age) {
        this.car = car;
        this.age = age;
    }

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

    public void setCar(Optional<Car> car) {
        this.car = car;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person2{" +
                "car=" + car +
                ", age=" + age +
                '}';
    }
}

@Slf4j
public class OptionalTest{
    @Test
    public void testOptionalSerialize() {
        Car car = new Car(new Insurance("太平洋保险"));
        Person2 person2 = new Person2(Optional.ofNullable(car), 22);
        log.info("person2: {}", person2);
        Gson gson = new Gson();
        String person2Str = gson.toJson(person2);
        log.info("person2Str: {}", person2Str);
        log.info("person2Bytes: {}", toBytes(person2));
    }

    byte[] toBytes(Person2 person2) {
        try (ByteArrayOutputStream bstream = new ByteArrayOutputStream();
             ObjectOutputStream ostream = new ObjectOutputStream(bstream)) {
            ostream.writeObject(person2);
            return bstream.toByteArray();
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
        return null;
    }
}

打印输出如下:

12:32:45.864 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - person2: Person2{car=Optional[Car{insurance=Insurance{name='太平洋保险'}}], age=22}
12:32:46.055 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - person2Str: {"car":{"value":{"insurance":{"name":"太平洋保险"}}},"age":22}
java.io.NotSerializableException: java.util.Optional
	at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1185)
	at java.base/java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1553)
	at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1510)
	at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1433)
	at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1179)
	at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:349)
	at com.practice.learn.moderninjava.chapter11.OptionalTest.toBytes(OptionalTest.java:224)
	at com.practice.learn.moderninjava.chapter11.OptionalTest.testOptionalSerialize(OptionalTest.java:218)
	...
	...
12:32:46.087 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - person2Bytes: null
  • Optional.map():获取或转换对象的值,返回一个Optional
@Slf4j
public class OptionalTest {

    @Test
    public void testGetObjectPropertyValue() {
        IntStream.rangeClosed(1, 3).forEach(idx -> {
            Insurance insurance = generateInsurance();
            String name = Optional.ofNullable(insurance).map(Insurance::getName).orElse("unKnown");
            log.info("insurance.name: {}", name);
        });
    }
    
    private Insurance generateInsurance() {
        Random random = new Random();
        int val = random.nextInt(100);
        if (val > 75) {
            return new Insurance("太平洋保险");
        } else if (val > 25) {
            return new Insurance("国元保险");
        } else {
            return null;
        }
    }
}

输出:

{
    06:34:23.481 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: unKnown
    06:34:23.485 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: 国元保险
    06:34:23.485 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: 国元保险
}

可以看到,使用map()和orElse()方法配合,让我们不再写if…else…这样繁冗的代码了。

  • Optional.flatMap(): 可以用于链接Optional对象
@Slf4j
public class OptionalTest {
    @Test
    public void testFlatmap() {
        for (int i = 0; i < 10; i++) {
            Optional<Person> person = Optional.ofNullable(generatePerson());

            String insuranceName = person.flatMap(Person::getCarAsOptional)
                    .flatMap(Car::getInsuranceAsOptional)
                    .map(Insurance::getName)
                    .orElse("unKnown");
            log.info("insurance.name: {}", insuranceName);
        }
    }

    private Car generateCar() {
        Random random = new Random();
        int val = random.nextInt(100);
        if (val < 51) {
            return new Car(generateInsurance());
        } else {
            return null;
        }
    }

    private Person generatePerson() {
        Person person = null;
        Random random = new Random();
        int val = random.nextInt(100);
        if (val > 51) {
            int age = ThreadLocalRandom.current().nextInt(18, 60);
            person = new Person(generateCar(), age);
        }
        return person;
    }
}

输出:

06:43:49.967 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: 国元保险
06:43:49.971 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: unKnown
06:43:49.971 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: unKnown
06:43:49.971 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: unKnown
06:43:49.971 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: unKnown
06:43:49.971 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: 国元保险
06:43:49.971 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: unKnown
06:43:49.972 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: unKnown
06:43:49.972 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: 国元保险
06:43:49.972 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - insurance.name: 国元保险

这里需要注意flatMap与map的区别,map是对获取的值在外面包装一层Optional,而flatMap平铺则是将传入的mapper函数返回的Optional直接返回,外面不再包装一层Optional,如果再包装就变成了Optional<Optional>,看函数定义就明白了:

public final class Optional<T> {
    ...
    ...
    /**
     * If a value is present, returns the result of applying the given
     * {@code Optional}-bearing mapping function to the value, otherwise returns
     * an empty {@code Optional}.
     * ...
     * ...
     */
    public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
        Objects.requireNonNull(mapper);
        if (!isPresent()) {
            return empty();
        } else {
            @SuppressWarnings("unchecked")
            Optional<U> r = (Optional<U>) mapper.apply(value);
            return Objects.requireNonNull(r);
        }
    }
    ...
    
    /**
     * If a value is present, returns an {@code Optional} describing (as if by
     * {@link #ofNullable}) the result of applying the given mapping function to
     * the value, otherwise returns an empty {@code Optional}.
     * ...
     * ...
     */
    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));
        }
    }
    ...
}
  • Optional.stream():Java 9 引入的新方法,可以将Optional的值转换成一个流
    /**
     * 获取personList中拥有一辆车的人所使用的保险公司的名字列表
     */
    @Test
    public void testOptionalStream() {
        List<Person> personList = IntStream.rangeClosed(1, 20)
                .mapToObj(i -> generatePerson())
                .collect(Collectors.toList());

        // 常规做法,在一个map里面将其转换(不剔除重复)
        Collection<String> nameList = personList.stream()
                .filter(Objects::nonNull)
                .map(person -> person.getCarAsOptional()
                        .flatMap(Car::getInsuranceAsOptional)
                        .map(Insurance::getName)
                        .orElse(null))
                // .filter(Objects::nonNull)
                .collect(Collectors.toList());
                // .collect(Collectors.toSet());
        log.info("1 names: {}", nameList);

        // 通过stream的方式获取name(剔除重复)
        Set<String> insuranceNames = personList.stream()
                .filter(Objects::nonNull)
                .map(Person::getCarAsOptional)
                .map(optCar -> optCar.flatMap(Car::getInsuranceAsOptional))
                .map(optIns -> optIns.map(Insurance::getName))
                .flatMap(Optional::stream)
                .collect(Collectors.toSet());
        log.info("2 names: {}", insuranceNames);
    }

输出:

07:15:16.132 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - 1 names: [国元保险, null, 国元保险, null, 太平洋保险, null, 太平洋保险, null, null, 国元保险, null, null]
07:15:16.139 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - 2 names: [太平洋保险, 国元保险]

如果没有使用IntelliJ IDEA 编辑器的话,是可以看到每一步的输出格式的,如下截图所示:
image

  • 两个Optional对象的组合:flatMap与map的完美配合
    /**
     * 测试用例:通过Person和Car,找到最便宜的保险,如果入参中person或car为空,则返回空,否则一定可以找到合适的保险公司
     */
    @Test
    public void testOptionalCompose() {
        Optional<Person> optPerson = Optional.ofNullable(generatePerson());
        Optional<Car> optCar = Optional.ofNullable(generateCar());

        Optional<Insurance> optInsurance1 = nullSafeFindCheapestInsurance1(optPerson, optCar);
        Optional<Insurance> optInsurance2 = nullSafeFindCheapestInsurance2(optPerson, optCar);
        log.info("optInsurance1: {}", optInsurance1.orElse(null));
        log.info("optInsurance2: {}", optInsurance2.orElse(null));
    }

    /**
     * 方案1:常规做法
     */
    private Optional<Insurance> nullSafeFindCheapestInsurance1(Optional<Person> optPerson, Optional<Car> optCar) {
        if (optPerson.isPresent() && optCar.isPresent()) {
            return Optional.of(findCheapestInsurance(optPerson.get(), optCar.get()));
        } else {
            return Optional.empty();
        }
    }

    /**
     * 方案2: 使用Optional组合:flatMap, map
     */
    private Optional<Insurance> nullSafeFindCheapestInsurance2(Optional<Person> optPerson, Optional<Car> optCar) {
        return optPerson.flatMap(person -> optCar.map(car -> findCheapestInsurance(person, car)));
    }

    private Insurance findCheapestInsurance(Person person, Car car) {
        // 查询各种保险公司数据,这里省略
        // 比对数据,这里省略
        return new Insurance("太平洋保险");
    }

输出:

07:26:12.650 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - optInsurance1: Insurance{name='太平洋保险'}
07:26:12.683 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - optInsurance2: Insurance{name='太平洋保险'}

这里的方案2,就是组合使用Optional的典型,因为传入的两个值都有可能为空,常规的做法就是将他们依次做判断(即方案1)。而使用flatMap及map,它们内部会先进行空值判断,如果为空,就直接返回Optional.empty(),不会走后续的操作了。

  • Optional.filter(Predicate<? super T> predicate):根据给定的Predicate进行过滤
    /**
     * 测试用例:找出年龄大于或等于给定参数minAge的Person所对应的保险公司名称
     */
    @Test
    public void testOptionalFilter() {
        for (int i = 0; i < 5; i++) {
            Optional<Person> optPerson = Optional.ofNullable(generatePerson());
            String insuranceName = getCarInsuranceName(optPerson, 30);
            log.info("optPerson: {}, insuranceName: {}", optPerson.orElse(null), insuranceName);
        }
    }

    private String getCarInsuranceName(Optional<Person> optPerson, int minAge) {
        return optPerson.filter(person -> person.getAge() >= minAge)
                .flatMap(Person::getCarAsOptional)
                .flatMap(Car::getInsuranceAsOptional)
                .map(Insurance::getName)
                .orElse("UnKnown");
    }

输出:

07:35:38.245 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - optPerson: null, insuranceName: UnKnown
07:35:38.248 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - optPerson: null, insuranceName: UnKnown
07:35:38.249 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - optPerson: Person{car=null, age=48}, insuranceName: UnKnown
07:35:38.278 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - optPerson: null, insuranceName: UnKnown
07:35:38.278 [main] INFO com.practice.learn.moderninjava.chapter11.OptionalTest - optPerson: null, insuranceName: UnKnown
  • 使用Optional对异常进行封装

比如我们需要将String类型转换为int,而这可能会抛出NumberFormatException,为了将繁冗的try…catch…通用代码规避掉,我们可以将其包装为一个工具类,如下:

@Slf4j
public class OptionalUtil {

    public static Optional<Integer> stringToInt(String str) {
        try {
            return Optional.of(Integer.parseInt(str));
        } catch (NumberFormatException ex) {
            log.error("NumberFormatException: {}", ex.getMessage());
        }
        return Optional.empty();
    }
}

小Tips

  1. 不建议使用基础类型的Optional对象(如:OptionalInt, OptionalLong, OptionalDouble, etc),一是因为Optional只封装了一个值,基础类型的自动装箱和拆箱成本并不高,二是因为它不支持flatMap, map, filter等重要的操作。
  2. 使用Optional可以帮助我们设计更好的API,因为阅读方法签名,我们就了解了这个参数值可能为空,迫使我们对Optional对象进行解引用,从而有效遏制NullPointerException的出现。

参考资料

  1. [英] 拉乌尔-加布里埃尔 • 乌尔玛 / [意] 马里奥 • 富斯科 / [英] 艾伦 • 米克罗夫特.陆明刚 / 劳佳 译. Java实战(第2版). 人民邮电出版社. 2019-11