JDK1.8 中的 Optional

更安全快捷的值处理接口

Posted by mingfer on May 27, 2019

JDK1.8 引入了一个非常方便的值处理类 —— Optional 类,它除了用于解决臭名昭著的空指针异常,还有更多不止于此的用途。

本质上来说,Optional 是用来包装其他对象的一个包装类。通过包装它提供了空值校验,默认值设置,值转换等功能。而且搭配 JDK1.8 的函数式编程,可以帮助我们实现一些更强劲的功能。

本文代码参见:cn.mingfer.demo.utils.optional.OptionalTest

方法概览

静态方法

方法名 说明
Optional.empty(); 获取到一个空的 Optional 对象,里面没有存储任何的实际值
Optional.of(T t); 包装对象 t,t 不能为空,为空则抛出 NullPointException
Optional.ofNullable(T t); 包装对象 t,t 可以为空,为空时返回一个 Optional.empty();

这三个静态方法均可以用来初始化一个 Optional 实例,具体的用法我们会在后面说明。

成员方法

方法名 说明
get() 获取包装的对象值 t
isPresent() 判断包装的对象是否是 null 值,不是为 true
ifPresent(Consumer<? super T> consumer) 如果对象不为 null 就执行传入的方法
filter(Predicate<? super T> predicate) 过滤对象,如果对象满足 predicate 方法中的条件就返回该对象,否则返回 Optional.empty()
map(Function<? super T, ? extends U> mapper) 当包装的对象不为空的时候,执行 mapper 方法操作包装的对象并返回一个 Optional 对象。如果这里 mapper 返回 Optional 对象,那么会被包装两次变成 Optional<Optional>,此时可以考虑使用 flatMap
flatMap(Function<? super T, Optional> mapper) 当包装的对象不为空的时候,执行 mapper 方法操作包装的对象并返回一个 Optional 对象,注意这里返回的 mapper 对象是 mapper 方法返回的。
orElse(T other) 如果当前包装的值为 null,那么返回备选值 other
orElseGet(Supplier<? extends T> other) 如果当前包装的值为 null,那么返回备选值 other
orElseThrow(Supplier<? extends X> exceptionSupplier) 如果当前包装的值为 null,那么抛出异常 X

使用技巧

空值校验

不允许通过 of 方法设置 null 值:

1
2
3
4
    @Test(expected = NullPointerException.class)
    public void ofNull() {
        Optional.of(null);
    }

通过 isPresent 判断包装的值是否为空值。大多数时候我们无法确定某个方法返回的值是否是 null 的时候,可以使用 ofNullable 包装对应的返回值,然后使用 isPresent 判断返回值是否为 null

1
2
3
4
5
    @Test
    public void ofNullable() {
        Assert.assertFalse(Optional.ofNullable(null).isPresent());
        Assert.assertTrue(Optional.ofNullable("test data").isPresent());
    }

如果包装的值为 null,在尝试获取值的时候回抛出 NoSuchElementException 异常。

1
2
3
4
    @Test(expected = NoSuchElementException.class)
    public void ofNullableTheGetNullValue() {
        Optional.ofNullable(null).get();
    }

值访问

上面已经有说过,Optional 通过 get 方法获取包装的值:

1
2
3
4
5
6
    @Test
    public void get() {
        final Entity entity = new Entity("test-entity", "entity");
        final Optional<Entity> optionalEntity = Optional.of(entity);
        Assert.assertEquals("entity", optionalEntity.get().getName());
    }

设置默认值

当我们在获取某个值,但是该值可能为 null,又希望有个默认值去替换的时候。比如:

1
2
3
4
5
6
7
8
    @Test
    public void getDefaultValue() {
        final Entity entity = new Entity("test", null);
        Assert.assertNull(entity.getName());
        Assert.assertEquals("test", Optional.ofNullable(entity.getName()).orElse("test"));
        Assert.assertNotNull(Optional.empty().orElseGet(() -> new Entity("test", null)));
        Assert.assertNotNull(Optional.ofNullable(entity).orElseGet(() -> new Entity("test", null)));
    }

上边的例子中第二个断言当 entityname 属性为 null 时使用默认值test

orElseorElseGet 都是用于设置默认值的。区别在于 orElseGet 接收一个 Supplier 参数,当包装的值不为 null 的时候,orElseGet 不会执行 new Entity 去构造一个实例,但是 orElse 会先构造 new Entity 实例。如果这段代码需要频繁调用或者 new 一个实例成本比较高的时候,在性能上的区别是非常大的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    public Entity(String id, String name) {
        System.out.println("execute construction.");
        this.id = id;
        this.name = name;
    }

    @Test
    public void orElseAndOrElseGet() {
        final Entity entity = new Entity("test", null);
        Assert.assertNotNull(Optional.ofNullable(entity).orElse(new Entity("test", null)));
        Assert.assertNotNull(Optional.ofNullable(entity).orElseGet(() -> new Entity("test", null)));
    }

# 结果
execute construction.
execute construction.

如果我们在包装的值不为空的时候不想设置默认值,而是直接抛出一个不允许为空的异常:

1
2
3
4
5
6
    @Test(expected = IllegalArgumentException.class)
    public void orThrow(){
        final Entity entity = new Entity("test", null);
        Optional.ofNullable(entity.getName())
                .orElseThrow(() -> new IllegalArgumentException("Entity.name must not be null."));
    }

这里 orElseThrow 也是传入一个 Supplier 封装的异常,避免了在值不为空的时候也创建异常对象,创建异常对象的时候由于需要 thread dump 性能是非常低的。

值转换

Optional 中 mapflatMap 用于转换 Optional 包装的值的类型。

mapflatMap 的区别在于:map 会对执行结果进行 Optional 包装,flatMap 要求执行的结果必须是一个 Optional 对象:

下面的例子是:我们需要获取 entity 的 Name 属性,但是不知道 name 是否为 null,如果为 null 我们希望返回默认值 mingfer。

1
2
3
4
5
6
7
8
9
10
11
12
13
    @Test
    public void mapAndFlatMap() {
        final Entity entity = new Entity("test", null);

        Optional<String> name = Optional.of(entity).map(Entity::getName);
        Assert.assertEquals("mingfer", name.orElse("mingfer"));

        Optional<Optional<String>> optionalName = Optional.of(entity).map(Entity::getOptionalName);
        Assert.assertEquals("mingfer", optionalName.map(Optional::get).orElse(Optional.of("mingfer").get()));

        Optional<String> nameByFlatMap = Optional.of(entity).flatMap(Entity::getOptionalName);
        Assert.assertEquals("mingfer", nameByFlatMap.orElse("mingfer"));
    }

值过滤

Optional 中 filter 用于过滤包装的值,filter 接口接受一个 Predicate 参数,如果包装的值满足 Predicate 条件,返回该值,否则返回一个 empty 的 Optional 对象。

1
2
3
4
5
6
    @Test
    public void filter() {
        final Entity entity = new Entity("test", "mingfer");
        Assert.assertTrue(Optional.of(entity).filter(e -> "test".equals(e.getId())).isPresent());
        Assert.assertFalse(Optional.of(entity).filter(e -> "test-2".equals(e.getId())).isPresent());
    }

使用 Optional 的优势

不用 Optional 的时候,进行对象属性访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    @Test
    public void disuseOptional() {
        final User user = new User(new User.Address(new User.Country("China")));

        // 可能出现空指针异常的调用方式
        String countryName = user.getAddress().getCountry().getName().toUpperCase();
        Assert.assertEquals("CHINA", countryName);


        // 为了避免空指针异常需要做的判断
        if (user != null) {
            User.Address address = user.getAddress();
            if (address != null) {
                User.Country country = address.getCountry();
                if (country != null) {
                    String name = country.getName();
                    if (name != null) {
                        countryName = countryName.toUpperCase();
                    }
                }
            }
        }
        Assert.assertEquals("CHINA", countryName);
    }

使用 Optional 进行对象的属性访问:

1
2
3
4
5
6
7
8
9
10
11
12
    @Test
    public void useOptional() {
        final User user = new User(new User.Address(new User.Country("China")));
        String result =
                Optional.ofNullable(user)
                        .map(User::getAddress)
                        .map(User.Address::getCountry)
                        .map(User.Country::getName)
                        .map(String::toLowerCase)
                        .orElse("CHINA");
        Assert.assertEquals("CHINA", result);
    }

对比两份代码可以看到 Optional 明显的减少了代码量。