前言
作为Java程序员,NullPointException是我们在开发调试中最常见的一个异常,原因也很简单,我们使用了一个没有被任何对应引用的值,它的值为null
,所以在获取其对象时,系统会抛出NullPointException,就像这样:
public class TestNullPointException{
@Test
public void test_null_point_exception(){
UserInfo userInfo = getUserInfoFromRemote()
String userName = userInfo.getUserName;
Assert.equeals(userInfo,"Jack");
}
}
如何避免上述代码npe的发生?
为了让程序更加健壮,我们不得不对可能为null的引用进行判断,让以上代码不发生异常,我们得这样修改下:
public class TestNullPointException{
@Test
public void test_null_point_exception(){
UserInfo userInfo = getUserInfoFromRemote();
String userName;
if(userInfo!=null){
userName= userInfo.getUserName()
}else{
userName="";
}
Assert.equeals(isEQ,false);
}
}
再比如有更加复杂的嵌套判断,如果有多个对象嵌套,我们要不断判断当前对象是否为null
,然后再取它的引用。
- 我们来先引入一个嵌套的实体,以用户–>订单–>商品信息建模。获取该用户下某个订单的某个商品信息的名称。
//用户信息
class UserInfo {
private int userId;
private OrderInfo orderInfo;
}
//订单信息
class OrderInfo {
private int orderId;
private ProductInfo productInfo;
}
//商品信息
class ProductInfo {
private int productId;
private String productName;
private double money;
}
- 获取商品名称的方法
private String getProductName(UserInfo userInfo) {
if (userInfo != null) {
OrderInfo orderInfo = userInfo.getOrderInfo();
if (orderInfo != null) {
ProductInfo productInfo = orderInfo.getProductInfo();
if (productInfo != null) {
return productInfo.getProductName();
}
}
}
return "";
}
以上代码为了获取商品名称,使用了三个判断null
的嵌套操作,在代码优雅性和可读性上都不好,如果嵌套对象过多,这样的判断可能会更多,可能有比较优雅的写法,前置判断来过滤为null的值。
private String getProductNameByPreAssert(UserInfo userInfo) {
if (userInfo == null) {
return "";
}
OrderInfo orderInfo = userInfo.getOrderInfo();
if (orderInfo == null) {
return "";
}
ProductInfo productInfo = orderInfo.getProductInfo();
if (productInfo == null) {
return "";
}
return productInfo.getProductName();
}
这样写不会有嵌套的null值判断,优雅性和逻辑都比第一种嵌套判断好一些,但是代码中还有很多null值判断,如果少了一环的判断,就可能出现NPE。有没有更好的方式去建模来避免NPE的出现?答案是有的,Java从8开始,内置了java.util.Optioal类来对可能为null的值进行建模,来尽可能避免NPE出现。
使用Optioal来替代null值的判断
在JDK中对Optional的描述中,Optional
是一个容器,可能包含null
值,或非null
值,里面包含了对null值的封装的API。
其实使用Optional来对null
值进行建模或者包装是对null
自身设计"缺陷"的一个补丁,当然,这个补丁修补的不是bug,而是能让使用者的或者能让语言自身使用起来更易用,更优雅。
我们来使用Optional
来对小结一中的例子二进行重构:
private String getProductNameWithOptional(UserInfo userInfo) {
return Optional.ofNullable(userInfo) ①
.map(UserInfo::getOrderInfo) ②
.map(OrderInfo::getProductInfo) ③
.map(ProductInfo::getProductName) ④
.orElse(""); ⑤
}
这个场景使用Optional是不是感觉比小结一种的代码清晰易懂,又简洁呢?
我们来逐步分析下以上代码,第一步,不能确认userInfo是否有值,所以我们使用Optioinal
容器对其进行包装,得到一个Optional<UserInfo>
的返回值,第二步我们使用Optional.map
获取订单信息,同样得到的订单信息也会包装到Optional
容器中,在②, ③,④中任意一部获取到的上一个引用值如果为null,则都会返回Optional.empty()
,在⑤的时候会对结果进行判断,如果Optional里面的值存在,则返回,否则返回默认给定的空字符串。
Optional提供的其他API
除了上面使用到的Optional.map
和Optional.orElse
,Optional 还提供了其他方便快捷的API
Optional.orElseGet
和orElse比较类似,但是orElse不会延迟执行,不论Optional是否有值,而orElseGet会延迟执行,当Optional.empty()为true的时候才会执行传入的函数。
private OrderInfo getOrderInfo(UserInfo userInfo) {
return Optional.ofNullable(userInfo)
.map(UserInfo::getOrderInfo)
.orElseGet(this::getOrderInfoFromRemote);
}
//远程调用获取OrderInfo
private OrderInfo getOrderInfoFromRemote() {
OrderInfo orderInfo = new OrderInfo();
orderInfo.setOrderId(1);
orderInfo.setProductInfo(new ProductInfo());
return orderInfo;
}
Optional.filter
filter操作和Stream的filter有异曲同工之妙,Stream中的filter是对流的操作,可以是一个元素也可以是n个元素,但是Optional.filter 是对Optional容器中的某个元素进行过滤。
🌰比如我们要对UserInfo
(可能为null)中userId<=0的用户进行过滤,如果有小于0的则抛出异常:
private UserInfo filterUserId(UserInfo userInfo) {
return Optional.ofNullable(userInfo)
.filter(user -> user.getUserId() > 0)
.orElseThrow(() -> new IllegalArgumentException("userId 不合法"));
}
Optional.flatMap
flatMap是扁平操作,故,有嵌套容器包装这的对象,需要将它结构成真正的对象。
🌰:我们需要根据UserInfo和ProductInfo获取productName,使用以下方法需要判断ProductInfo和UserInfo是否为空,UserInfo为空则直接返回空字符串
private String getProductNameWithUserInfoAndProductInfo(UserInfo userInfo, ProductInfo productInfo) {
if (userInfo != null && productInfo != null) {
return this.productName(userInfo, productInfo);
}
return "";
}
private String productName(UserInfo userInfo, ProductInfo productInfo) {
return "mock Name";
}
使用Optional
获取
private String getProductNameWithUserInfoAndProductInfoWithOpt(UserInfo userInfo, ProductInfo productInfo) {
Optional<UserInfo> userInfoOpt = Optional.ofNullable(userInfo);
Optional<ProductInfo> productOpt = Optional.ofNullable(productInfo);
return userInfoOpt.flatMap(u -> productOpt.map(p -> productName(u, p)))
.orElse("");
}
Optional.orElseThrow
我们想要从UseInfo中的OrderInfo下的ProductInfo中获取productName,如果其中某个对对象为空,则抛出响应的异常:
private String getProductNameWithOptional(UserInfo userInfo) {
Optional<UserInfo> userInfoOpt = Optional.ofNullable(userInfo);
return userInfoOpt.map(userInfo1 -> Optional.ofNullable(userInfo1.getOrderInfo()))
.orElseThrow(() -> new RuntimeException("用户信息不存在"))
.map(orderInfoOpt -> Optional.ofNullable(orderInfoOpt.getProductInfo()))
.orElseThrow(() -> new RuntimeException("订单信息不存在"))
.map(prodcutOpt -> Optional.ofNullable(prodcutOpt.getProductName()))
.orElseThrow(() -> new RuntimeException("商品信息不存在"))
.orElse("44234");
}
还有一些其他的API就不一一介绍了,学到了上面的API,其他的看官方文档文档应该也能很快理解。
注意事项
总结
Java在JDK8 以上为Java语言的判空操作提供了安全的Optional
容器,帮助我们在操作对象时避免NPE异常,阅读Optional类提供的方法我们也可以发现,每一个对Optional的操作其实JDK帮我们判断了当前操作的对象是否为空。
比如:Optional.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.filter
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent()) {
return this;
} else {
return predicate.test(value) ? this : empty();
}
}
但是在使用是也需要注意,不是所有的对象和操作用Optional容器包装起来都是安全或者最好的做法,应该按照实际情况,合理的使用Optional类提供的方法。