Java 8新特性 - (15)Optional类

分享到:

文章目录


相关文章: Java 9对Optional类的改进


Java 8中引入了Optional类来解决 NullPointerException 与繁琐的 null 检查。它是一个封装值的类,用于保存类型为T的值,从本质上说它就是一个容器。

变量存在时,Optional类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空” 的Optional对象,由方法Optional.empty()返回。那null和 Optional.empty()的区别在哪呢?从语义上,可以把它们当作一回事儿,但是实际中它们之间的差别非常大:如果尝试解引用一个null, 一定会触发NullPointerException,而使用Optional.empty()就完全没事,它是 Optional类的一个有效对象。

创建 Optional 对象

空Optional

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

1Optional<Car> option = Optional.empty();

因为 empty() 本身代表的就是空对象,所以调用 get 方法会抛出 NoSuchElementException 异常。

非空Optional

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

1Optional<Car> optional = Optional.of(car);

如果 car 是一个 null,这段代码会立即抛出一个 NullPointerException,而不是等到试图访问car的属性值时才返回一个错误。

可为null的Optional

使用静态工厂方法 Optional.ofNullable,可以创建一个允许 null 值的 Optional 对象:

1Optional<Car> optional = Optional.ofNullable(car);

如果 car 是 null,那么得到的 Optional 对象就是个空对象。我们可以查看一下它的实现原理:

1public static <T> Optional<T> ofNullable(T value) {
2    return value == null ? empty() : of(value);
3}

根据它的实现方式,传入的值是空值时,会返回 Optional.empty() 空对象。这有利于封装那些可能为 null 的值。例如,有一个 Map<String, Object> 实例,访问 key 索引时,如果没有与 key 关联的值,则会返回一个 null。因此,可以使用 Optional.ofNullable 方法封装返回值:

1Optional<Object> value = Optional.ofNullable(map.get("key"));

访问Optional对象的值

isPresent & get

在 Optional 类中,isPresent 方法对 Optional 实例进行判断,是否包含值,如果存在值,就返回 true,否则返回 false;与之相对的是 isEmpty 方法Optional 类中还有 get 方法,它是用来获取 Optional 实例中的值。

1Optional<String> optional = Optional.of("is present");
2if (optional.isPresent()) {
3    System.out.println("the value is " + optional.get());
4}

isPresent 与 get 一般组合使用来避免 NullPointerException:

1public boolean isPresent() {
2    return value != null;
3}
4public T get() {
5    if (value == null) {
6        throw new NoSuchElementException("No value present");
7    }
8    return value;
9}

从源码中可看出,get 方法在取值时,要进行判空操作,如果不使用 isPresent 方法,可能会出现空指针异常。但是这种方式和在代码中if(null != value) 没有区别,因此要尽量避免使用该组合

ifPresent

除了 isPresent 的简洁方法,Optional 还提供了接收函数式参数的接口 ifPresent:

1public void ifPresent(Consumer<? super T> action) {
2    if (value != null) {
3        action.accept(value);
4    }
5}

该方法会接收一个消费型函数。如果 Optional 实例中的值不为空,则调用 Consumer 的 accept 方法对 value 进行消费,若为空则不做处理。上面的例子可以使用 ifPresent 重写:

1Optional<String> optional = Optional.of("is present");
2optional.isPresent((val) -> System.out.println("the value is " + val));

返回默认值

orElse

还可以使用 orElse 方法读取 Optional 中的值。

1public T orElse(T other) {
2    return value != null ? value : other;
3}

使用这种方式可以定义一个默认值,这种方式当遭遇 Optional 中的变量为空时,默认值会作为该方法的返回值。

1String optGet = null;
2String orElse = Optional.ofNullable(optGet).orElse("Default");

orElseGet

还可以使用另一种方式 orElseGet:

1public T orElseGet(Supplier<? extends T> supplier) {
2    return value != null ? value : supplier.get();
3}

该方法与 orElse 的区别就是值不存在时,调用实现 Supplier 接口的方法或Lambda表达式来返回默认值。

1String optGet = null;
2String orElse = Optional.ofNullable(optGet).orElse(() -> "Default");

返回异常

orElseThrow

orElseThrow方法是在有值时返回其值,无值的时候会抛出由 Supplier 创建的异常。我们看一下它的实现原理:

1public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
2    if (value != null) {
3        return value;
4    } else {
5        throw exceptionSupplier.get();
6    }
7}

可以看到,它会传入一个Lambda表达式或方法,如果值不存在来抛出异常:

 1class NoValueException extends RuntimeException {
 2    public NoValueException() {
 3        super();
 4    }
 5    @Override
 6    public String getMessage() {
 7        return "No value present in the Optional instance";
 8    }
 9}
10public static Integer orElseThrow() {
11    return (Integer) Optional.empty().orElseThrow(NoValueException::new);
12}
13public static void main(String[] args) {
14    orElseThrow();
15}

orElseThrow 与 orElseGet 的区别就是一个在无值的时候抛出异常,一个在无值的时候使用Lambda表达式来实现默认值。

orElseThrow 只是在无值的时候抛出异常,那本身会抛出异常的方法呢?

现在拿 Integer.parseInt(String) 做个例子:

1public static Integer toInt(String s) {
2    try {
3        // 如果String能转换为对应的Integer,将其封装在Optional对象中返回
4        return Integer.parseInt(s);
5    } catch (NumberFormatException e) {
6        return null;    // 返回null 或者抛出异常
7    }
8}

在将 String 转换为 int 时,如果无法解析到对应的整型,该方法会抛出 NumberFormatException 异常。我们在该方法中使用 try/catch 语句捕获了该异常,不能使用 if 条件判断来控制一个变量的值是否为空。

这时,可以使用 Optional 类,来对无法转换的 String 时返回的非法值进行建模,因此可以对上述方法进行改进:

1public static Optional<Integer> toInt(String s) {
2    try {
3        // 如果String能转换为对应的Integer,将其封装在Optional对象中返回
4        return Optional.of(Integer.parseInt(s));
5    } catch (NumberFormatException e) {
6        return Optional.empty();    // 否则返回一个空的 Optional 对象
7    }
8}

这种返回 Optional 的方式适用很多方法,我们只需要获取被 Optional 包装的值的实例即可。

对值进行转换

map

Optional 提供了 map 方法用于从对象中提取信息,它的工作原理如下:

1public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
2    Objects.requireNonNull(mapper);
3    if (!isPresent())
4        return empty();
5    else {
6        return Optional.ofNullable(mapper.apply(value));
7    }
8}

map 操作会将提供的函数应用于流的每个元素。可以把 Optional 对象看成一种特殊的集合数据,它至多包含一个元素。如果 Optional 包含一个值,那通过实现了 Function 接口的 Lambda 表达式对值进行转换。map 方法示例如下:

1class Car {
2    private String name;
3    private String type;
4    //...省略getter与setter...
5}
6Optional<Car> optional = Optional.ofNullable(car);
7Optional<String> name = optional.map(Car::getName);

flatMap

看下面这段代码,它想使用 map 方法来从被 Optional 类包装的 Person 类中获取 Car 的名称:

 1class Person {
 2    private Optional<Car> car;
 3    public Person(Car car) {
 4        this.car = Optional.of(car);
 5    }
 6    //...省略getter与setter...
 7}
 8Person person = new Person(new Car());
 9Optional<Person> optPerson = Optional.of(person);
10Optional<String> name = optPerson.map(Person::getCar).map(Car::getName);

不幸的是,这段代码无法通过编译。为什么呢?optPerson 是 Optional 类型的变量,调用 map 方法应该没有问题。但 getCar 返回的是一个 Optional 类型的对象,这意味着 map 操作的结果是一个 Optional<Optional> 类型的对象。因此,它对 getName 的调用是非法的,因为最外层的 optional 对象包含了另一个 optional 对象的值,而它当然不会支持 getName 方法。

这里,需要使用 flatMap 方法。该方法接受一个函数作为参数,这个函数的返回值是另一个流。

 1public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
 2    Objects.requireNonNull(mapper);
 3    if (!isPresent()) {
 4        return empty();
 5    } else {
 6        @SuppressWarnings("unchecked")
 7        Optional<U> r = (Optional<U>) mapper.apply(value);
 8        return Objects.requireNonNull(r);
 9    }
10}

参照 map 函数,使用 flatMap 重写上述的示例:

1Optional<String> name = optPerson.flatMap(Person::getCar).map(Car::getName);

filter

有时候我们需要对 Optional 中的值进行过滤,获得我们需要的结果,我们就可以使用 filter 方法:

1public Optional<T> filter(Predicate<? super T> predicate) {
2    Objects.requireNonNull(predicate);
3    if (!isPresent())
4        return this;
5    else
6        return predicate.test(value) ? this : empty();
7}

该方法接受 Predicate 谓词作为参数。如果 Optional 对象的值存在,并且符合谓词的条件,即操作结果为true,filter 方法不做任何改变并返回其值;否则就将该值过滤掉并返回一个空的 Optional 对象。

1Optional<String> optionalS = Optional.of("13846901234");
2optionalS = optionalS.filter(s -> s.contains("138"));
3/**
4 * 上述 `filter` 方法满足条件可以返回同一个Optional,否则返回空Optional
5 */

注意事项

  1. Optional的包装和访问都有成本,因此不适用于一些特别注重性能和内存的场景。
  2. 不要将null赋给Optional,应赋以Optional.empty()。
  3. 避免调用isPresent()和get()方法,而应使用ifPresent()、orElse()、 orElseGet()和orElseThrow()。举一isPresent()用法示例:
 1private static boolean isIntegerNumber(String number) {
 2    number = number.trim();
 3    String intNumRegex = "\\-{0,1}\\d+";
 4    if (number.matches(intNumRegex)) {
 5        return true;
 6    } else {
 7        return false;
 8    }
 9}
10// Optional写法1(含NPE修复及正则表达式优化)
11private static boolean isIntegerNumber1(String number) {
12    return Optional.ofNullable(number)
13            .map(String::trim)
14            .filter(n -> n.matches("-?\\d+"))
15            .isPresent();
16}
17// Optional写法2(含NPE修复及正则表达式优化,不用isPresent)
18private static boolean isIntegerNumber2(String number) {
19    return Optional.ofNullable(number)
20            .map(String::trim)
21            .map(n -> n.matches("-?\\d+"))
22            .orElse(false);
23}
  1. Optional应该只用处理返回值,而不应作为类的字段(Optional类型不可被序列化)或方法(包括constructor)的参数。
  2. 不要为了链式方法而使用Optional,尤其是在仅仅获取一个值时。例如:
1// good
2return variable == null ? "blablabla" : variable;
3// bad
4return Optional.ofNullable(variable).orElse("blablabla");
5// bad
6Optional.ofNullable(someVariable).ifPresent(this::blablabla)
  1. 滥用Optional不仅影响性能,可读性也不高。应尽可能避免使用null引用。
  2. 避免使用Optional返回空的集合或数组,而应返回Collections.emptyList()、emptyMap()、emptySet()或new Type[0]。注意不要返回null,以便调用者可以省去繁琐的null检查。
  3. 避免在集合中使用Optional,应使用getOrDefault()或computeIfAbsent()等集合方法。
  4. 针对基本类型,使用对应的OptionalInt、OptionalLong和OptionalDouble类。
  5. 切忌过度使用Optional,否则可能使代码难以阅读和维护。
  6. 常见的问题是Lambda表达式过长,例如:
 1private Set<String> queryValidUsers() {
 2    Set<String> userInfo = new HashSet<String>(10);
 3
 4    Optional.ofNullable(toJSonObject(getJsonStrFromSomewhere()))
 5            .map(cur -> cur.optJSONArray("data"))
 6            .map(cur -> { // 大段代码割裂了"思路"
 7                for (int i = 0; i < cur.length(); i++) {
 8                    JSONArray users = cur.optJSONObject(i).optJSONArray("users");
 9
10                    if (null == users || 0 == users.length()) {
11                        continue;
12                    }
13
14                    for (int j = 0; j < users.length(); j++) {
15                        JSONObject userObj = users.optJSONObject(j);
16                        if (!userObj.optBoolean("stopUse")) { // userObj可能为null!
17                            userInfo.add(userObj.optString("userId"));
18                        }
19                    }
20                }
21                return userInfo;
22            });
23
24    return userInfo;
25}

通过简单的抽取方法,可读性得到很大提高:

 1private Set<String> queryValidUsers() {
 2    return Optional.ofNullable(toJSonObject(getJsonStrFromSomewhere()))
 3            .map(cur -> cur.optJSONArray("data"))
 4            .map(this::collectNonStopUsers)
 5            .orElse(Collections.emptySet());
 6}
 7
 8private Set<String> collectNonStopUsers(JSONArray dataArray) {
 9    Set<String> userInfo = new HashSet<String>(10);   
10    for (int i = 0; i < dataArray.length(); i++) {
11        JSONArray users = dataArray.optJSONObject(i).optJSONArray("users");
12        Optional.ofNullable(users).ifPresent(cur -> {
13            for (int j = 0; j < cur.length(); j++) {
14                Optional.ofNullable(cur.optJSONObject(j))
15                        .filter(user -> !user.optBoolean("stopUse"))
16                        .ifPresent(user -> userInfo.add(user.optString("userId")));
17            }
18        });
19    }
20    return userInfo;
21}

参考: