NPE异常相信 Java 程序员都很熟悉,是 NullPointerException 的缩写;最近业务需求开发的有点着急,测试环境就时不时的来个NPE异常,特别的头疼;作为出镜率最高的异常之一,一旦入行 Java 开发,可以说它将伴随着你整个职业生涯;不管是新手小白、还是老司机,对NPE异常那是又“爱”又狠,爱的主要原因是处理起来简单,恨当然是一个不小心就会踩坑;为了提高代码的质量,NPE异常是必须要消灭掉的;
那既然处理起来简单,有什么好纠结的呢?老老实实校验不就完了,但整个处理的过程中对程序员来说体验是非常糟糕的;
-
让代码冗长
很多时候,核心的业务逻辑代码量是不大的,但是一旦加上各种判断、校验,就会让代码变的冗长,可读性、维护性随之下降;
-
纯苦力活
像这种机械式的判空、校验本质上就是一些体力活,没有任何编码乐趣可言,长时间编写这种代码,会丧失对编程的激情;
-
易背锅
很多业务需要多人合作,有时候可能会出现侥幸心里,都认为其他人在用的时候会处理;无形中挖了些坑,一不小心就锅从天降;
基于上面这些不太好的体验,让消除的难度增加了不少;
有时候当需求很着急的时候,程序员大部分都会选择以功能为主,一些不太重要的东西总是想着晚点再来补充,先跳过写重要的内容,结果是一跳过就没有然后了;
为了既能解决NPE问题,又不影响我们的开发效率;JDK、三方框架为我们提供了很多优秀的工具类,大可不必自己耗时耗力去再造轮子了;
下面就通过10个妙招,来彻底解决NPE问题:
1Objects 工具类
既然要解决空指针,自然就是提前对对象进行判空校验;通常情况下,会使用if( null != obj )
进行对象校验;在 Java 7 中,专门提供工具类java.util.Objects
,让对象的判空校验更加简单;
特点
-
Java 7 自带,不需要额外的依赖
-
静态方法,使用简单
-
仅支持对象判空
示例
-
Objects.isNull
判断对象是否为空,为
null
返回true
,否则返回false
Object obj = null; System.out.println(Objects.isNull(obj)); // trueobj = new Object(); System.out.println(Objects.isNull(obj)); // false
-
Objects.nonNull
和
Objects.isNull
相反;判断对象不为空,为null
返回false
,否则返回true
Object obj = null; System.out.println(Objects.nonNull(obj)); // falseobj = new Object(); System.out.println(Objects.nonNull(obj)); // true
-
Objects.requireNonNull
校验非空,一旦对象为空,就会抛出空指针异常(NullPointerException),改方法可以自定义异常描述,方便异常之后能快速定位问题所在:
Object obj = null; Objects.requireNonNull(obj); // 自定义错误描述 Objects.requireNonNull(obj,"obj 对象为空");
执行输出:
Exception in thread "main" java.lang.NullPointerException: obj 对象为空at java.util.Objects.requireNonNull(Objects.java:228)at com.ehang.helloworld.controller.NullTest.t5(NullTest.java:97)at com.ehang.helloworld.controller.NullTest.main(NullTest.java:23)
2字符串判空
字符串是开发过程中使用最多一种数据类型,因此对字符串的判断、校验也就必不可少了,原生的方式都是通过空对象,长度进行判断:
String str = "一行Java"
if ( null != str && s1.length() > 0 ){// 对str字符串进行使用
}
但是,对字符串的校验,除了判空之外,还有很多其他的场景,比如判断是不是空串(String str = ""
),是不是只有空格(String str = " "
)等等,那这些校验,就会麻烦一些了;不过木有关系,现成的工具类已经足够满足了;
Spring StringUtil工具类
org.springframework.util.StringUtils
是String 框架自带的字符串工具类,功能比较单一,在教新的版本中,这个工具类的字符串判空方法已经被弃用了,所以不太建议使用了;
-
StringUtils.isEmpty
空对象以及空串的校验;
String s1 = null; String s2 = ""; String s3 = " "; System.out.println(StringUtils.isEmpty(s1)); // true System.out.println(StringUtils.isEmpty(s2)); // true System.out.println(StringUtils.isEmpty(s3)); // false
apache lang3 StringUtil工具类
apache lang3 StringUtil 工具类(org.apache.commons.lang3.StringUtils
) 相比于Spring 框架带的工具类,要强大太对了,涵盖了对String 操作的所有封装;
判空校验的话主要有4个StringUtils.isEmpty
、StringUtils.isNotEmpty
、StringUtils.isBlank
、StringUtils.isNotBlank
-
依赖
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId> </dependency>
-
StringUtils.isEmpty
和StringUtils.isNotEmpty
判断字符串对象是否为空,以及字符串长度是否为0;isEmpty 和 isNotEmpty 校验结果相反;
String s1 = null; String s2 = ""; String s3 = " "; System.out.println(StringUtils.isEmpty(s1)); // true System.out.println(StringUtils.isEmpty(s2)); // true System.out.println(StringUtils.isEmpty(s3)); // false System.out.println(); System.out.println(StringUtils.isNotEmpty(s1)); // false System.out.println(StringUtils.isNotEmpty(s2)); // false System.out.println(StringUtils.isNotEmpty(s3)); // true
-
StringUtils.isBlank
、StringUtils.isNotBlank
在
StringUtils.isEmpty
和StringUtils.isNotEmpty
判断的基础上,还会将字符串开头,结尾的空格去掉之后,判断长度是否大于0String s1 = null; String s2 = ""; String s3 = " "; String s4 = " 1 2 "; System.out.println(StringUtils.isBlank(s1)); // true 空对象 System.out.println(StringUtils.isBlank(s2)); // true 长度等于0 System.out.println(StringUtils.isBlank(s3)); // true 去掉前后空格之后,长度也等于0 System.out.println(StringUtils.isBlank(s4)); // false 去掉前后空格(1 2),长度大于0 System.out.println(); System.out.println(StringUtils.isNotBlank(s1)); // false System.out.println(StringUtils.isNotBlank(s2)); // false System.out.println(StringUtils.isNotBlank(s3)); // false System.out.println(StringUtils.isNotBlank(s4)); // true
-
其他功能
本文主要是探讨判空校验,lang3 的 StringUtil 工具类几乎涵盖了所有关于String操作的封装,大大降低了我们处理 String 的复杂度,更多功能可参考官方文档
https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/StringUtils.html
3字符串比较
在对字符串进行比较的时候,也需要特别注意NPE异常;
如下示例:
public Boolean isEhang(String name) {if (name.equals("ehang")) {return true;}return false;
}
当如果name为null的时候,就会出现NPE异常;
可以做如下调整:
if ("ehang".equals(name))
...
这样就算name为null,即不会出现NPE异常,也能正常的判断;
4Map、List、Set 判空
Map、List、Set 是经常会用到的数据结构,虽然他们都包含有isEmpty()
方法,能判断容器中是否包含了元素,但是无法判断自生对象是否为空,一旦对象没有实例化时,调用isEmpty()就会报空指针异常;Spring 为我们提供了一个org.springframework.util.CollectionUtils
工具类,其中的isEmpty
就会优先判断对象是否为空,然后再通过isEmpty()判断是否存在元素,能大大减少因为对象为空带来的空指针异常;
Map map = null;
System.out.println(map.isEmpty()); // 空指针异常
System.out.println(CollectionUtils.isEmpty(map)); // true
map = new HashMap();
System.out.println(map.isEmpty()); // true
System.out.println(CollectionUtils.isEmpty(map)); // true
map.put("1", "2");
System.out.println(CollectionUtils.isEmpty(map)); // false
System.out.println(map.isEmpty()); // falseList list = null;
System.out.println(list.isEmpty()); // 空指针异常
System.out.println(CollectionUtils.isEmpty(list)); // true
list = new ArrayList();
System.out.println(list.isEmpty()); // true
System.out.println(CollectionUtils.isEmpty(list)); // true
list.add("1");
System.out.println(CollectionUtils.isEmpty(list)); // false
System.out.println(list.isEmpty()); // falseSet set = null;
System.out.println(set.isEmpty()); // 空指针异常
System.out.println(CollectionUtils.isEmpty(set)); // true
set = new TreeSet();
System.out.println(set.isEmpty()); // true
System.out.println(CollectionUtils.isEmpty(set)); // true
set.add("1");
System.out.println(CollectionUtils.isEmpty(set)); // false
System.out.println(set.isEmpty()); // false
除了判空之外,该工具类还包含了很多很实用的方法,比如获取第一个元素:firstElement() 、最后一个元素:lastElement()、是否包含某个元素:contains() 等等
hutool的CollectionUtil
单纯判空,前面Spring的CollectionUtils已经足够,其他的功能也够满足绝大部分的使用场景;hutool的CollectionUtil
提供了更加完善的功能,如果需要,也可以选用;
依赖:
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.22</version>
</dependency>
5赋初始值、尽量不要返回null对象
当定于局部变量,定义对象的属性时,能赋初始值的就尽量带上初始值;
Map map = new HashMap();private Integer age = 0;
当方法有返回值的时候,非必要的情况下,尽量不要返回null;
比如一个方法的执行最终返回的是List,当List没有值的时候,可以不返回null对象,而是可以返回一个空的List
public List select(){// 这里处理其他逻辑// 一旦返回的是null是,返回一个空List对象return Collections.emptyList();
}
6Optional
Optional 是 Java 8 提供的一个对象容器,目的就是为了能有效的解决这个烦人的空指针异常,我们可以将 Optional 看成一个对象给包装类;
-
实例化 Optional 对象
Object o1 = null; Optional<Object> op1 = Optional.of(o1); Optional<Object> op2 = Optional.ofNullable(o1);
Optional.of()
当对象为null时,创建过程就会抛出NPE异常
Optional.ofNullable()
当对象为null时,也能正常返回 Optional 对象
-
判空 isPresent()
Integer i1 = null; Optional<Integer> op1 = Optional.of(i1); System.out.println(op1.isPresent()); // falseInteger i2 = 123; Optional<Integer> op2 = Optional.ofNullable(i2); System.out.println(op2.isPresent()); // true op2.ifPresent(i->{System.out.println(i); });
isPresent() 当对象为null返回true,不为空时返回false
lambda表示式的链式处理:
op2.ifPresent(obj->{System.out.println(obj); });
-
取值
// 取出原值,如果原对象为null会报NoSuchElementException异常 Integer integer = op2.get(); // 取出原值,如果原值为空,则返回指点的默认值 Integer integer1 = op1.orElse(456); // 取出原值,如果原值为空,返回默认值,不过在返回之前还需要做一些其他的事情 Integer integer2 = op2.orElseGet(() -> {// 在这里做一些其他的操作return 456; });// 取出原值,如果原值为空,就抛出指定的异常 op2.orElseThrow(RuntimeException::new); op2.orElseThrow(() -> new RuntimeException("不好,我的值是空的!"));
-
map() 和 flatMap()
编码过程中,经常会出现:a.xxx().yyy().zzz().mmm() 这样链式调用,这个过程,一旦中间有任意一环出现问题,就会NPE异常,因此,我们就可以借助map() 和 flatMap()来避免这个问题;
测试对象:
@Data @NoArgsConstructor @AllArgsConstructor static class User {private String name;private Integer age;private Optional<String> addr; }
测试:
// 得到姓名的长度,如果没有姓名就返回0 Integer nameLen = Optional.of(new User(null, 10, null)).map(User::getName).map(String::length).orElse(0); System.out.println(nameLen);// 得到地址的长度,如果没有姓名就返回0 Integer addr = Optional.of(new User(null, 10, Optional.of("北京"))).flatMap(User::getAddr).map(String::length).orElse(0); System.out.println(addr);
map会将返回的对象封装成Optional对象,如果返回的对象本身就是一个Optional对象了,那就使用flatMap()
7断言
Spring 中的 org.springframework.util.Assert
翻译为中文为"断言",它用来断定某一个实际的运行值和预期项是否一致,不一致就抛出异常。借助这个类,同样也可以做判空检验;
Assert 类提供了以下的静态方法:
方法名 | 描述 | 失败时抛出异常 |
---|---|---|
isNull(Object object, String message) | object 不为空,抛出异常 | IllegalArgumentException |
notNull(Object object, String message) | object 为空,抛出异常 | IllegalArgumentException |
hasLength(String text, String message) | text 是空字符串,抛出异常 | IllegalArgumentException |
hasText(String text, String message) | 不包含空白字符串,抛出异常 | IllegalArgumentException |
doesNotContain(String textToSearch, String substring, String message) | textToSearch 中包含 substring,抛出异常 | IllegalArgumentException |
notEmpty(Object[] array, String message) | array 为空或长度为 1,抛出异常 | IllegalArgumentException |
noNullElements(Object[] array, String message) | array 中包含 null 元系,抛异常 | IllegalArgumentException |
notEmpty(Collection collection, String message) | collection 不包含元素,抛出异常 | IllegalArgumentException |
notEmpty(Map map, String message) | map 中包含 null,抛出异常 | IllegalArgumentException |
isInstanceOf(Class type, Object obj, String message) | 如果 obj 不是 type 类型,抛出异常 | IllegalArgumentException |
isAssignable(Class superType, Class subType, String message) | subType 不是 superType 子类,抛出异常 | IllegalArgumentException |
state(boolean expression, String message) | expression 不为 true 抛出异常 | IllegalStateException |
isTrue(boolean expression, String message) | expression 不为 true 抛出异常 | IllegalArgumentException |
Integer i1 = null;
Assert.notNull(i1,"i1 不为空");Map map = null;
Assert.notEmpty(map,"map 不为空");
异常:
Exception in thread "main" java.lang.IllegalArgumentException: map 不为空at org.springframework.util.Assert.notEmpty(Assert.java:555)at com.ehang.helloworld.controller.NullTest.t6(NullTest.java:119)at com.ehang.helloworld.controller.NullTest.main(NullTest.java:23)
特别注意:
Assert 用来断定某一个实际的运行值和预期项是否一致,所以他和其他工具类的校验方式是反着在;比如isNull
方法是期望对象为null,如果不为空的时候,就会报错;notNull
表示期望对象不为空,当对象为空时,就会报错;
8局部变量使用基本数据类型
在之前的文章《阿里为何禁止在对象中使用基本数据类型》中,从性能的角度,推荐局部变量的定义尽量使用基本数据类型,能不用包装类就不用;那么从今天文章的角度来说,使用基本数据类型也能有效的避免空指针异常;
如下实例:
int x;
Integer y;
System.out.println( x + 1 ); // 编译失败
System.out.println( y + 1 ); // 编译失败int i = 1;
Integer j = null;
System.out.println( i + 1 ); // 正常
System.out.println( j + 1 ); // 空指针异常
int m = i; // 正常
int n = j; // 空指针异常
当变量x、y 只定义、不赋值的时候,x + 1 和 y + 1 是没办法通过编译的;而包装类 j 是可以指定null
对象,当包装类参与运算的时候,首先会做拆箱操作,也就是调用 intValue() 方法,由于对象是空的,调用方法自然就会报空指针;同时,将一个包装类赋值给一个基本数据类型时,同样也会做拆箱操作,自然也就空指针异常了;
但是,基本数据类型就必须指定一个具体值,后续不管运算、还是赋值操作,都不会出现空指针异常;
9提前校验参数
后台数据,绝大部分都是通过终端请求传递上来的,所以需要在最接近用户的地方,把该校验的参数都校验了;比如StringBoot项目,就需要在Controller层将客户端请求的参数做校验,一旦必传的参数没有传值,就应该直接给客户端报错并提醒用户,而不是将这些不符合要求的null值传到Service甚至保存到数据库,尽早的校验并拦截,就能大大降低出问题的概率
之前介绍的hibernate-validator
就能完美解决参数校验问题,详见:SpringBoot!你的请求、响应、异常规范了吗?
10IDEA提醒
IDEA 对空对象或者可能会出现null值的对象会有提醒,可以根据提醒来提前感知并预防
public static String t1(int i){String name1 = null;String name2 = null;if(i>0){name2 = "ehang";}t2(name1);t2(name2);return name2;
}