NPE(NullPointerException)是我们代码工作中最常遇到的一个异常,非常的难受,如何优雅的处理它呢。
本篇文章将详细介绍Optional类,以及如何用它消除代码中的null检查。Optional是为了防止NullPointerException,使代码更优雅。
避免使用null
检查
作为Java开发人员,几乎所有人都遇到过NullPointerException
异常,大多数人遇到NullPointerException
异常时都会在异常出现的地方加上if
代码块来判断值不为空,比如下面的代码:
public void bindUserToRole(User user) {
if (user != null) {
String roleId = user.getRoleId();
if (roleId != null) {
Role role = roleDao.findOne(roleId);
if (role != null) {
role.setUserId(user.getUserId());
roleDao.save(role);
}
}
}
}
这是比较普遍的做法,为了避免出现NullPointerException
异常,手动对可能为null
值进行了处理,不过代码看起来非常糟糕,业务逻辑被淹没在if
逻辑判断中,也许下面的代码看起来可读性稍好一些:
public String bindUserToRole(User user) {
if (user == null) {
return;
}
String roleId = user.getRoleId();
if (roleId == null) {
return;
}
Role = roleDao.findOne(roleId);
if (role != null) {
role.setUserId(user.getUserId());
roleDao.save(role);
}
}
上面的代码避免了深层的if
语句嵌套,但本质上是一样的,方法内有三个不同的返回点,出错后调试也不容易,因为你不知道是那个值导致了NullPointerException
异常。
基于上面的原因,Java 8中引入了一个新的类Optional
,用以避免使用null
值引发的种种问题。
Optional
类
java.util.Optional<T>
类是一个封装了Optional
值的容器对象,Optional
值可以为null
,如果值存在,调用isPresent()
方法返回true
,调用get()
方法可以获取值。
创建Optional
对象
Optional
类提供类三个方法用于实例化一个Optional
对象,它们分别为empty()
、of()
、ofNullable()
,这三个方法都是静态方法,可以直接调用。
empty()
方法用于创建一个没有值的Optional
对象:
Optional<String> emptyOpt = Optional.empty();
empty()
方法创建的对象没有值,如果对emptyOpt
变量调用isPresent()
方法会返回false
,调用get()
方法抛出NullPointerException
异常。
of()
方法使用一个非空的值创建Optional
对象:
String str = "Hello World";
Optional<String> notNullOpt = Optional.of(str);
ofNullable()
方法接收一个可以为null
的值:
Optional<String> nullableOpt = Optional.ofNullable(str);
如果str
的值为null
,得到的nullableOpt
是一个没有值的Optional
对象。
提取Optional
对象中的值
如果我们要获取User
对象中的roleId
属性值,常见的方式是直接获取:
String roleId = null;
if (user != null) {
roleId = user.getRoleId();
}
使用Optional
中提供的map()
方法可以以更简单的方式实现:
Optional<User> userOpt = Optional.ofNullable(user);
Optional<String> roleIdOpt = userOpt.map(User::getRoleId);
使用orElse()
方法获取值
Optional
类还包含其他方法用于获取值,这些方法分别为:
orElse()
:如果有值就返回,否则返回一个给定的值作为默认值;orElseGet()
:与orElse()
方法作用类似,区别在于生成默认值的方式不同。该方法接受一个Supplier<? extends T>
函数式接口参数,用于生成默认值;orElseThrow()
:与前面介绍的get()
方法类似,当值为null
时调用这两个方法都会抛出NullPointerException
异常,区别在于该方法可以指定抛出的异常类型。
下面来看看这三个方法的具体用法:
String str = "Hello World";
Optional<String> strOpt = Optional.of(str);
String orElseResult = strOpt.orElse("Hello Shanghai");
String orElseGet = strOpt.orElseGet(() -> "Hello Shanghai");
String orElseThrow = strOpt.orElseThrow(
() -> new IllegalArgumentException("Argument 'str' cannot be null or blank."));
此外,Optional
类还提供了一个ifPresent()
方法,该方法接收一个Consumer<? super T>
函数式接口,一般用于将信息打印到控制台:
Optional<String> strOpt = Optional.of("Hello World");
strOpt.ifPresent(System.out::println);
使用filter()
方法过滤
filter()
方法可用于判断Optional
对象是否满足给定条件,一般用于条件过滤:
Optional<String> optional = Optional.of("lw900925@163.com");
optional = optional.filter(str -> str.contains("164"));
在上面的代码中,如果filter()
方法中的Lambda表达式成立,filter()
方法会返回当前Optional
对象值,否则,返回一个值为空的Optional
对象。
如何正确使用Optional
通过上面的例子可以看出,Optional
类可以优雅的避免NullPointerException
带来的各种问题,不过,你是否真正掌握了Optional
的用法?假设你试图使用Optional
来避免可能出现的NullPointerException
异常,编写了如下代码:
Optional<User> userOpt = Optional.ofNullable(user);
if (userOpt.isPresent()) {
User user = userOpt.get();
// do something...
} else {
// do something...
}
坦白说,上面的代码与我们之前的使用if
语句判断空值没有任何区别,没有起到Optional
的正真作用:
if (user != null) {
// do something...
} else {
// do something...
}
当我们从之前版本切换到Java 8的时候,不应该还按照之前的思维方式处理null
值,Java 8提倡函数式编程,新增的许多API都可以用函数式编程表示,Optional
类也是其中之一。这里有几条关于Optional
使用的建议:
- 尽量避免在程序中直接调用
Optional
对象的get()
和isPresent()
方法; - 避免使用
Optional
类型声明实体类的属性;
第一条建议中直接调用get()
方法是很危险的做法,如果Optional
的值为空,那么毫无疑问会抛出NullPointerException
异常,而为了调用get()
方法而使用isPresent()
方法作为空值检查,这种做法与传统的用if
语句块做空值检查没有任何区别。
第二条建议避免使用Optional
作为实体类的属性,它在设计的时候就没有考虑过用来作为类的属性,如果你查看Optional
的源代码,你会发现它没有实现java.io.Serializable
接口,这在某些情况下是很重要的(比如你的项目中使用了某些序列化框架),使用了Optional
作为实体类的属性,意味着他们不能被序列化。
下面我们通过一些例子讲解Optional
的正确用法:
正确创建Optional
对象
上面提到创建Optional
对象有三个方法,empty()
方法比较简单,没什么特别要说明的。主要是of()
和ofNullable()
方法。当你很确定一个对象不可能为null
的时候,应该使用of()
方法,否则,尽可能使用ofNullable()
方法,比如:
public static void method(Role role) {
// 当Optional的值通过常量获得或者通过关键字new初始化,可以直接使用of()方法
Optional<String> strOpt = Optional.of("Hello World");
Optional<User> userOpt = Optional.of(new User());
// 方法参数中role值不确定是否为null,使用ofNullable()方法创建
Optional<Role> roleOpt = Optional.ofNullable(role);
}
orElse()
方法的使用
return str != null ? str : "Hello World"
上面的代码表示判断字符串str
是否为空,不为空就返回,否则,返回一个常量。使用Optional
类可以表示为:
return strOpt.orElse("Hello World")
简化if-else
User user = ...
if (user != null) {
String userName = user.getUserName();
if (userName != null) {
return userName.toUpperCase();
} else {
return null;
}
} else {
return null;
}
上面的代码可以简化成:
User user = ...
Optional<User> userOpt = Optional.ofNullable(user);
return userOpt.map(User::getUserName)
.map(String::toUpperCase)
.orElse(null);
总结一下,新的Optional
类让我们可以以函数式编程的方式处理null
值,抛弃了Java 8之前需要嵌套大量if-else
代码块,使代码可读性有了很大的提高。
坑
使用OPtional的orElse()问题
项目中有这样一段代码:
return Optional.ofNullable(service.A()).orElse(service.B())
功能显而易见,service.A()如果返回值是null,则返回service.B(),否则直接返回service.A()。
实际使用中发现:
如果service.A()返回非null,最终结果是service.A(),然而service.B()这个方法也被执行了。这样肯定就不对了,如果service.B()中还有插入数据库或者RPC这种操作,问题就大了。刚开始还以为是什么执行顺序问题,后来在Stack Overflow上看到老外讨论orElse()和orElseGet()的区别,其中一点区别就是orElse(T)无论前面Optional容器是null还是non-null,都会执行orElse里的方法,orElseGet(Supplier)并不会,如果service无异常抛出的情况下,Optional使用orElse或者orElseGet的返回结果都是一样的
stack overflow上有人还给出这样一个例子
static String B() {
System.out.println("B()...");
return "B";
}
public static void main(final String... args) {
System.out.println("-----");
System.out.println(Optional.of("A").orElse(B()));
System.out.println("-----");
System.out.println(Optional.of("A").orElseGet(() -> B()));
}
// 输出结果
-----
B()...
A
-----
A
看了上面代码,我就把我的代码改成如下即可:
return Optional.ofNullable(service.A()).orElseGet(() -> service.B())
结论:Optional的orElse(T)若方法不是纯计算型的,有与数据库交互或者远程调用的,都应该使用orElseGet()
引自:
使用Optional的orElse()问题 - liubingyu12345的博客
Java - Difference between `Optional.orElse()` and `Optional.orElseGet()` - Stack Overflow