快捷搜索:  汽车  科技

java空指针如何解决(Java中如何避免空指针异常)

java空指针如何解决(Java中如何避免空指针异常)当我们把大量的代码放在检查空值上面时,会大大的增加代码的长度;每次操作一个对象前对其进行空检查是可行的, 但是这种高强度的检查频率会带来两个问题:null 表示啥都没有,无。现实生活中, 我在对一个人说话时,我们会看看这个人在不在?如果不在, 我们就不说了。写代码时, 我们要做的也是:在对一个对象做一个操作时,先检查一下这个对象在不在。然而,我们在写代码的过程中经常犯的错误就是:对着空气说话。

"null很恶心。" -Doug Lea(道格·利)

"Null 引用一直是个坏主意,从来没发挥过什么正面作用。这是一个令我追悔莫及的错误。" - Sir C. A. R. Hoare(托尼·霍尔) 在评价他对null的发明时说。


1. 什么是NPE?

NPE 是空指针异常 NullPointerException 的缩写,是一个影响非常广泛,破坏性非常强的东西。对于一个Java开发工程师来说,避免NPE是一个值得研究的课题。

作为一名合格的Java开发工程师都,我们需要严肃认真地对待NPE问题,NPE不除,软件质量不提。

2. Java中的null代表什么?

null 表示啥都没有,无。

现实生活中, 我在对一个人说话时,我们会看看这个人在不在?如果不在, 我们就不说了。写代码时, 我们要做的也是:在对一个对象做一个操作时,先检查一下这个对象在不在。

然而,我们在写代码的过程中经常犯的错误就是:对着空气说话

每次操作一个对象前对其进行空检查是可行的, 但是这种高强度的检查频率会带来两个问题:

  • 增加代码长度

当我们把大量的代码放在检查空值上面时,会大大的增加代码的长度;

如:一个方法100行代码,有50行是检查各种参数,30行进行异常处理,20行调用方法进行业务处理。这样的代码将来如何维护、阅读呢?岂不是刚写出来就知道日后定会被其它程序员骂出翔。

  • 减少写代码的乐趣

注意力是非常宝贵的资源,在混乱的办公场景中,嘀嘀响的工作群、一会儿不看就有几百条未读的划水群、吵闹的产品经理、测出Bug的测试人员、分派任务的Leader... ... 我们的注意力无时无刻被打扰,好不容易集中注意力开始写业务代码,大半的时间还要编写各种对空的检查,我们写代码的乐趣全都被消磨掉了。

所以Java中的Null代表着问题、代表着麻烦、代表着各种坑,代表着你在半夜被电话叫醒解决线上问题,代表着你的技术水平被拉低几个段位。

那么如何解决NPE?

3. 如何解决NPE?

答案是:没法解决!!! 或者说没法从根本上解决。

我们能做的是:认真面对每一个业务需求,认真面对每一行代码,写代码时小心谨慎,从态度上重视NPE问题,用各种限制手段降低NPE问题的影响,一定不能在写代码时放飞自我,放飞自我的同时,也将NPE一起放飞了出去。

一定要记住我们是工程师,我们做的是工程,不是玩具。

工程师要严谨、认真,具有三心:细心、耐心、责任心。

下面列出一些经验之谈,虽然不能完全解决NPE问题,用得好的话,还是能最大程度地减少NPE的发生。

3.1 遵守一些开发约定

所有集合对象在声明时即进行实例化

// 使用Guava中的工具类实例化List private List<UserProfile> userList = Lists.newArrayList(); // 直接实例化 private List<UserProfile> userList1 = new ArrayList<>(); // 使用Guava中的工具类实例化Map private Map<String UserProfile> userMap = Maps.newHashMap(); // 直接实例化 private Map<String UserProfile> userMap1 = new HashMap<>();

返回集合类型时,如果没有数据,返回空集合对象。

public List<String> doSomething() { List<String> returnValue; .... return returnValue == null? Lists.newArrayList(): returnValue; }

为了方便,可以自己写一个简单的返回空集合的方法

public <T> T nonNullVal(T val T def) { return Objects.isNull(val)? def: val; } // 或者这样 public <T> T nonNullList(T val) { return val == null? Lists.newArrayList(): val; }

上面的代码就变为下面这样了

public List doSomething() { List returnValue; .... return nonNullVal(returnValue Lists.newArrayList()); // 如果返回的List不会被做插入数据等操作,也可以直接返回空列表 return nonNullVal(returnValue Lists.emptyList()); return nonNullList(returnValue); }

将变量设置为[有业务含义]的默认值

这样会最大程度上减少对一个对象进行初始化的复杂度:你不用手动设置一些默认值了。

// 姓名默认为空字符串,如果在实际业务代码编写时没有填写姓名, // 空字符串也能表明没有填写姓名 private String name = ""; // 如果业务上规定0为没有填写年龄,可以默认将年龄设置为0 private Integer age = 0; // 创建时间在对象实例时,默认赋值为当前 private LocalDateTime createTime = LocalDateTime.now();

对可能为空的变量增加提示信息

1. 增加Spring注解 @NonNull @Nullable ,IDE会作异常提示 ;

2. 在注释中标明参数不可为空,提醒调用者小心NPE

/** * 两个整数相加 * @param a 不能为空 * @param b 不能为空 * @return 相加结果 */ private Integer add(@NonNull Integer a @NonNull Integer b) { return a b; }

在Idea中会进行提示,如下图所示:

java空指针如何解决(Java中如何避免空指针异常)(1)

集合中不存储 `null`;使用 `Map`时,不将 `null`作为Key

一般情况下,列表对象中不存储null,这样就不会给处理列表元素的程序埋下深坑。

Map对象的元素中,Key和Value都不使用null。

使用null有明确的业务含意时,在任何时候、任何地方都可以使用null

3.2 集合为空的检查问题

通常集合检查的方式为:1. 检查集合对象是否为空, 2. 检查集合内元素数量是否为0

List list = null; if (list == null || list.isEmpty()) { System.out.println("List为空"); } Map map = new HashMap(); if (map == null || map.isEmpty()) { System.out.println("Map为空"); } Set set = new HashSet(); if (set == null || set.isEmpty()) { System.out.println("Set为空"); }

因为List、Set都是Collection,所以我们可以统一写验证为空的方法:

public boolean isEmpty(Collection collection) { return collection == null || collection.isEmpty(); } public boolean isEmpty(Map map) { return map == null || map.isEmpty(); }3.3 使用JDK8的Objects来操作对象

因为NPE问题实在是太严重了,所以JDK中出现了Objects,提供了一组避免产生NPE的API。

下面两种情况,Java程序员应该都遇到过:

  • 在将一个对象转为字符串时,对象不存在;

Integer age; .....一堆操作 age.toString(); // 咣,此处NPE

  • 比对两个对象是否相等时,主比较对象为空;

User me = new User(); User loginUser = getLoginUser(); // 如果此方法返回了null if (loginUser.equals(me)) { // 咣,此处NPE }

Objects针对上面的情况,给了一个解决方案,从一定程度上避免了NPE问题。如:

String val1 = null; String val2 = "hello"; if (Objects.equals(val1 val2)) { // 你不用再对null作检查了 // 如果两个值等,进入这里。如果两个值都是null,判定两个值是相等的。 } else { // 如果不相等,会进入这里。 }; String val4 = null; String val5 = Objects.toString(val4);// 如果val4是null,返回 ”null“字符串 // 如果不想变成 "null",可以使用下面的方法,会返回一个替换 String val5 = Objects.toString(val4 "");

除了 Objects.equals() 和 Objects.toString()外,Objects还提供了下面一些方法来对null作检查,在日常的开发中可以尝试一下。

判断一个对象是否为空:

// 这是一个null变量 String value = null; // 检查变量是否为空 if (Objects.isNull(value)) { // 对象为空 进入这里 } // 检查变量是否不为空 if (Objects.nonNull(value)) { // 对象不为空 进入这里 }

【检查一个对象是否为空,如果为空抛出异常】:

这个方法在验证参数是否为空时比较有效,但是文章后面的断言部分会**更有效**。

// 这是一个null变量 String value = null; // 过滤一下变量 如果为空就抛出异常 value = Objects.requireNonNull(value); // 过滤一下变量 如果为空就抛出异常 异常带个消息 value = Objects.requireNonNull(value "抛出的异常带着这个消息"); // 可以在方法中作校验参数使用 public List<String> doSomething(String param) { Objects.requireNonNull(param); // .... }

计算HashCode值,避免对空对象计算Hash值抛NPE:

String value = null; int hashCode = Objects.hashCode(value); // hasCode 结果为13.4 使用Optional来处理空对象

Optional是JDK提供的一个用于处理空对象的实践,使用合理的话能够在一定程度上避免NPE的产生。

可以把 `Optional`看作一个对象的包装对象,通过这个包装对象来操作原对象,一方面Optional强制对原对象作判空检查,另一方面强制开发人员重视对 `null`的处理,从技术与态度两方面来避免NPE的发生。

下面用实例来讲一下Optional的用法:

Optional对象的实例化:

Integer value = 6; Optional<Integer> op = Optional.of(value);

上面的代码中,如果value是null,会抛出NPE,为了避免这种无谓的NPE,可以使用下面的方式来实例化:

// 如果value==null 返回 Optional.empty() Optional<Integer> op = Optional.ofNullable(value);

如何从Optional中取出原对象?

// 直接取值,虽然简单,但是如果为空时,还是避免不了NPE Integer i = op.get() // 增加默认值的取法,如果op不为空,返回op中的原值,如果op为空,返回999 Integer i = op.orElse(999); // 如果默认值需要经过一系列的操作,那么可以使用lambda表达式来完成 Integer i = op1.orElseGet(() -> { .... 一堆操作 return Optional.of(999); // 返回一个Optional对象 }); // 如果为空时不想返回默认值,想直接抛出一个自定义异常呢?按下面的来 Integer i = op.orElseThrow(() -> { return new BusinessException("如果为空 抛出我"); });

如何检查一个对象是否为空

// 最初级的检查方式,比较复杂 if (op1.isPresent()) { // 不为空时进入这里做逻辑处理 } // 使用lambda表示式,简单处理 op1.ifPresent(v -> { // 如果value不为null 执行这里面的代码段. 可以替换 if(op1.isPresent()){....} System.out.println(v); });

Optional的的使用方法已经讲述清楚了,下面看看如何在业务开发中应用Optional呢?

未改造前代码:

public User getLoginUser() { // ... 一堆业务处理 return user; } // 调用代码 User user = getLoginUser(); if (user != null) { // 正常业务逻辑 } else { // 用户为空的处理,比如抛出异常 }

改造后的代码:

public Optional<User> getLoginUser() { // ... 一堆业务处理 return Optional.ofNullable(user); } // 调用代码 Optional<User> user = getLoginUser(); user.XXXX(); // 不能直接操作,因为Optional对User做了包装,强制使用下面的几种方式来处理 // 使用方法1,跟原来相比更复杂了 if (user.isPresent()) { // 正常业务逻辑 User u = user.get(); u.XXXX(); } else { // 用户为空的处理,比如抛出异常 throw new RuntimeException("没有找到用户"); } // 使用方法2,用户为空, 抛出异常 User u = user.orElseThrow(() -> { return BusinessException("没有找到用户"); }); u.XXXX(); // 使用方法3,用户为空,使用默认用户,me是一个默认User对象。 User u = user.orElse(me); u.XXXX();3.5 使用断言类来校验参数

什么是断言

不知道别人是怎么理解断言的,我是很长一段时间都不能理解什么是断言,想理解断言,得从它的英文单词说起:Assert,中文翻译是:

明确肯定; 断言; 坚持自己的主张; 表现坚定; 维护自己的权利(或权威);

我理解断言就是:在程序中明确肯定的一些事情,如果没成功,程序中断

什么是需要明确肯定的呢?一些事关业务成败的条件是要明确肯定的。如果达不到这个条件,业务就无法顺利完成,需要中断业务处理(反正也执行不成功,也没有必要执行下去了。)

具体表现在程序中就是:在业务处理之前的前置条件校验

现在有很多三方库提供了方便的断言工具类,下面挑选出两个应用广泛的,大家在项目中几乎默认引用的两个库:

Guava中的PreConditions断言类

PreCondition类提供的主要的静态方法列表为:

java空指针如何解决(Java中如何避免空指针异常)(2)

下面给出一个使用的示例:

public List<String> doSomething(String param) { // 如果param为空,抛出异常 Preconditions.checkArgument(Objects.nonNull(param) "参数不能为空");

PreConditions的所有方法都没有明确指明是以 `true` 还是 `false` 为标准, 以至于我每次使用时都需要看一下代码的实现。所以从个人角度来说PreConditions容易产生误解,而Spring中的Assert类则没有这个问题,下面大家可以瞅瞅Spring的Assert类。

Spring中的 Assert断言类

Assert类提供的主要的静态方法列表:

java空指针如何解决(Java中如何避免空指针异常)(3)

public List<String> doSomething(String param) { // 如果param为空,抛出异常 Assert.notNull(param "param不能为空"); 总结

避免NPE问题的法宝不是工具,而是态度。我们应该对代码有敬畏之心,重视每一行代码,每一个需求,千万不能怀有“这个很简单”的想法,就像潘加宇老师说的:“所有卖钱的系统就没有简单的”。我们拿着工资写的代码都是要给公司赚钱的,都是不简单的。

猜您喜欢: