Optional
是一个容器类,是 JDK 8 提供的一个防止引起空指针异常的工具类,可以更好地封装处理返回值。
为什么更推荐使用 Optional
类封装可能为 null 的返回值?在项目开发过程中,没人绝对清楚调用方法的返回值一定存在,开发者也只是尽力保证返回值不为 null,比如查询用户列表没用户时我们就返回长度为 0 的 ArrayList
,有种对任何值都不信任的编程方式,但这种方式会形成许多冗余代码,让开发者也很累,不这样做会带来讨厌的 Null Pointer Exception(NPE)问题;其次是在多层次取成员变量时,程序员能判断到吐。Optional
的出现很好的改观了这个问题,一两行代码就能代替繁琐的判空。
本文对 Optional
工具的使用技巧进行总结。
热身
开始前你可以先熟悉一下 Optional 的 API,前往菜鸟教程。
坏例子
例子类:
1class User {
2 Long id;
3 String name;
4 Address addr;
5}
一个查询 User
的方法:
1 // 查询方法
2 User findUserById(Long id);
坏例子 1:
1 void method1(Long id) {
2 User u = dao.findUserById(id);
3
4 // 此时开发人员可能使用 User 类对象 u 疏忽了判空
5 u.getName(); // 用户未查到,u 为 null,发生 NPE
6 }
坏例子 2:
1 void method2(Long id) {
2 User u = dao.findUserById(id);
3
4 // 现在要取成员变量 Address 的城市。
5 String city = null;
6 if (u != null ) { // 判空
7 String name = u.getName();
8 Address addr = u.getAddress();
9 // 这里还要判空,如果不判空可能发生 NPE
10 if (addr != null) {
11 prov = addr.getCity();
12 }
13 }
14 // 使用
15 System.out.println(city);
16 }
相信看完这个例子你已经厌恶空指针错误了,在 JDK 1.8 之前,我们通常没有好的解决办法。
这种方式首先在 Guava 上出现,JDK 1.8 吸取了这个良好的设计,接下来我们看下使用 Optional 怎么样避免上面的问题。
改良后的查询方法:
1 // 查询方法
2 Optional<User> findUserById(Long id) {
3 if (id == null) {
4 return Optional.empty();
5 }
6
7 User user = dbUtils.query(id);
8 return Optional.ofNullable(user);
9 }
对例子 1 NPE 调用改造:
1 void method1(Long id) {
2 Optional<User> u = dao.findUserById(id);
3 // before
4 // u.getName(); // 用户未查到,u 为 null,发生 NPE
5
6 // after
7 // 我们拿到了 name,它可能存在,也或者为已经猜测到的 null,中间不存在任何发生 NPE 的风险,
8 String name = u.map(User::getName).orElse(null);
9 }
当我们刻意使用 Optional 包装返回值时,同时也是潜意识的提醒用户:现在返回的这个值,可能为 null。
对例子 2 调用改造:
1 void method2(Long id) {
2 Optional<User> u = dao.findUserById(id);
3
4 // before
5 // if (u != null ) { // 判空
6 // String name = u.getName();
7 // Address addr = u.getAddress();
8 // // 这里还要判空,如果不判空可能发生 NPE
9 // if (addr != null) {
10 // prov = addr.getCity();
11 // }
12 // }
13
14 // after
15 // 现在要取成员变量 Address 的城市。
16 String city = u.map(User::getAddr).map(Address::getCity).orElse("CITY EMPTY");
17
18 // 使用非常明确的对象
19 System.out.println(city);
20 }
Optional 只用一行链式调用就帮我们结束了多层嵌套的判空。
到这里相信你已经理解 Optional 的作用了,它最常用的方式就是上面的例子。
从源码看使用
1、注意点: Optional.of(obj)
只能接收非空 obj 对象,若要传入可能为 null 的对象就使用 Optional.ofNullable(obj)
。
1 // 源码
2
3 // of 方法
4 public static <T> Optional<T> of(T value) {
5 return new Optional<>(value);
6 }
7
8 // of方法用的构造器
9 private Optional(T value) {
10 // 判空,value 为 null 抛异常
11 this.value = Objects.requireNonNull(value);
12 }
2、注意点: get()
不能直接使用。
1 public T get() {
2 // 判空抛异常!
3 if (value == null) {
4 throw new NoSuchElementException("No value present");
5 }
6 return value;
7 }
3、flatMap
与 map
的区别: flatMap
必须传入 Optional 对象,而 map
不用。
使用对比:
1 Optional<User> u = dao.findUserById(id);
2 // 获取 addr 的 city
3
4 // flatMap
5 String city = u.flatMap(user -> Optional.ofNullable(user::getAddr)).flatMap(addr -> Address::getCity).orElse(null);
6
7 // map
8 String city = u.map(User::getAddr).map(Address::getCity).orElse(null);
以下为二者的源码。
flatMap
:
1 public<U> Optional<U> flatMap(Function<? super T, Optional<U>/** 这里限定了Optiona对象 **/> mapper) {
2 Objects.requireNonNull(mapper);
3 if (!isPresent())
4 return empty();
5 else {
6 return Objects.requireNonNull(mapper.apply(value));
7 }
8 }
map
:
1 public<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 }
错误的使用方式
回到原始:
1 Optional<User> u = dao.findUserById(id);
2
3 String name = null;
4 if (u.isPresent()) {
5 User user = u.get();
6 name = user.getName();
7 }
8
9 // 使用 name
这种方式多此一举,简单取成员变量没有必要用 Optional。
1 User u = dao.findUserById(id);
2
3 Optional<User> userOpt = Optional.ofNullable(u);
4 String name = userOpt.map(User::getName).orElse(null);
5
6 // 传统方式一行解决
7 String name = u != null ? u.getName() : null;
8
9 // 使用 name
请记住不要使用这些方式让开发变复杂。
总结
小结一下。
1、Optional 通常只使用在返回值可能为 null 的方法作为返回值。
2、Optional 非常适合取一个对象的多层嵌套成员变量。
3、注意 of
的入参不为 null,这是个炸弹。
4、注意使用 get
前先判断是否为空,这也是个炸弹。
参考: