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