Java异常体系中有一个我们常见的空指针异常。
为了避免空指针异常,在写代码时我们通常都会开启防御性编程。
代码库里充斥着大量繁琐的类似if (obj != null)这样的判断。
代码看起来很不好看。
典型的代码:
java
public String getCityOfUser(User user) {
if (user != null) {
Address address = user.getAddress();
if (address != null) {
String city = address.getCity();
if (city != null) {
return city;
}
}
}
return "未知";
}
Java8开始引入的java.util.Optional<T>类,就是为了提供一种更优雅、更安全的解决方案。
但是他不是要消灭null,而是用一种容器的设计方案,来明确地表示一个值可能缺失的情况。
从而在编译层面就提醒开发人员处理这种可能性。
一、什么是Optional
你可以把Optional<T>想象成一个盒子,如果盒子里有一个T类型的对象,我们称这个Optional包含一个值。
如果盒子里是空的,我们就称这个Optional为空。
它的核心思想是,通过将可能为空的对象包装在这个盒子里,强制调用者在开箱的时候必须思考如果箱子是空的怎么办?,从而从根本上减少NPE的发生。
它是一个设计工具,能让你的API意图更加明确。
二、创建Optional对象
一般我们不会直接new Optional(),而是使用它提供的三个静态工厂方法来创建实例:
2.1 Optional.of(T value)
如果你百分百确定value不为null的时候使用。如果value是null,它会马上抛出一个NullPointerException。
java
Optional<String> name = Optional.of("lazysnail");
// 抛NPE
// Optional<String> nullName = Optional.of(null);
2.2 Optional.ofNullable(T value)
这是最常用,也是最灵活的方法。
如果value不为null,它会创建一个包含这个值的Optional;
如果value为null,它会创建一个空的Optional。
java
String username = getUserFromDatabase();
Optional<String> optUsername = Optional.ofNullable(username);
getUserFromDatabase方法可能为空。
2.3 Optional.empty()
这个方法直接创建一个明确的、空的Optional实例。
java
Optional<String> emptyOptional = Optional.empty();
三、Optional值的消费
最开始用Optional的时候,有些人会写成这样:
java
Optional<String> optName = ...;
if (optName.isPresent()) {
String name = optName.get();
System.out.println(name);
}
这种写法本质上和if (name != null)没什么区别,只是换了个更啰嗦的方式,完全没有发挥出Optional的优势。
get()方法在Optional为空的时候会抛出NoSuchElementException,还是很危险。
以下几种消费方式才是推荐的:
3.1 ifPresent(Consumer<T> action)
如果值存在,就对它执行给定的操作;如果不存在,就什么也不做。
java
optUsername.ifPresent(name -> System.out.println("用户名: " + name));
optUsername.ifPresent(System.out::println);
3.2 ifPresentOrElse(Consumer<T> action, Runnable emptyAction)
如果值存在,执行action;否则,执行emptyAction。
java
optUsername.ifPresentOrElse(
name -> System.out.println("欢迎, " + name),
() -> System.out.println("游客你好,请先登录。")
);
3.3 orElse(T other)
如果值存在,返回值;否则,返回一个预设的默认值。
java
String finalUsername = optUsername.orElse("游客");
3.4 orElseGet(Supplier<? extends T> supplier)
如果值存在,返回值;否则,通过一个Supplier函数来动态生成一个默认值。
java
String finalUsername = optUsername.orElseGet(() -> expensiveDefault());
只有在optUsername为空的时候,才会执行expensiveDefault()方法。
如果生成默认值的开销很大,orElseGet性能相比于orElse要好点,因为它只有在需要的时候才执行。
orElse不管Optional是不是为空,都会先计算好默认值。
3.5 orElseThrow(Supplier<? extends X> exceptionSupplier)
如果值存在,返回值;否则,抛出一个由Supplier函数生成的异常。
这是处理值必须存在场景最好的方式。
java
String username = optUsername.orElseThrow(
() -> new IllegalStateException("用户名不能为空")
);
四、链式操作
Optional还有一个比较强大的地方就是他可以像Stream一样进行链式操作。
4.1 map(Function<T, U> mapper)
如果Optional不为空,它会把内部的值应用mapper函数进行转换,并返回一个新的Optional<U>。
如果为空,就直接返回一个空的Optional。
java
Optional<String> optName = Optional.of("lazysnail");
Optional<Integer> optLength = optName.map(String::length);
4.2 filter(Predicate<T> predicate)
如果值存在且满足predicate条件,就返回该Optional;
否则,返回一个空的Optional。
java
Optional<String> longName = optName.filter(name -> name.length() > 5);
Optional<String> shortName = optName.filter(name -> name.length() < 5);
4.3 flatMap(Function<T, Optional<U>> mapper)
这个我们在前文中讲到过,flatMap用于处理当你的转换函数本身就返回一个Optional的场景。
如果用map,你会得到一个Optional<Optional>这样的双层盒子。
flatMap则会把结果压平成一个Optional。
java
public Optional<Integer> getAgeByName(String name) { ... }
Optional<String> optName = Optional.of("lazysnail");
Optional<Optional<Integer>> nestedAge = optName.map(this::getAgeByName);
Optional<Integer> age = optName.flatMap(this::getAgeByName);
案例中假设我们有一个方法getAgeByName,它根据名字查找年龄,但可能找不到,所以返回Optional<Integer> 。
如果用map,会得到一个双层Optional,Optional<Optional<Integer>>。
如果用flatMap,结果会被压平,用起来更简单。
还记得本文最开始的那个典型的代码案例吗?
如果用flatMap和map来重写,代码就会变成这样:
java
public String getCityOfUser(User user) {
return Optional.ofNullable(user)
.flatMap(User::getAddress)
.map(Address::getCity)
.orElse("未知");
}
对于初学者或者不熟悉Optional使用的人来说,改写的代码可读性确实没有老式代码那么直观。
但是熟练使用之后,你会喜欢上这种编码方式。
结语
本文通过一些小案例,讲解了Optional这个工具在现代Java开发中的使用方法。
合理的使用这些工具,可以让你写的代码更安全,更简洁。
但是要注意,Optional只是提供了一种强大的类型系统层面的约束,而不是为了取代null的。
下一篇预告
Day34 | Java中的日期时间API
如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!
更多文章请关注我的公众号《懒惰蜗牛工坊》