NPE
异常相信 Java
程序员都很熟悉,是 NullPointerException
的缩写;最近业务需求开发的有点着急,测试环境就时不时的来个NPE
异常,特别的头疼;作为出镜率最高的异常之一,一旦入行Java
开发,可以说它将伴随着你整个职业生涯;不管是新手小白、还是老司机,对NPE
异常那是又“爱”又狠,爱的主要原因是处理起来简单,恨当然是一个不小心就会踩坑;为了提高代码的质量,NPE
异常是必须要消灭掉的;
下面就通过10个妙招,来彻底解决NPE
问题:
1.Objects 工具类
既然要解决空指针,自然就是提前对对象进行判空校验;通常情况下,会使用if( null != obj )
进行对象校验;在 Java 7
中,专门提供工具类java.util.Objects
,让对象的判空校验更加简单;
特点
- Java 7 自带,不需要额外的依赖
- 静态方法,使用简单
- 仅支持对象判空
示例
Objects.isNull
判断对象是否为空,为null返回true,否则返回false
Object obj = null;
System.out.println(Objects.isNull(obj)); // true
obj = 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)); // false
obj = 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
判断的基础上,还会将字符串开头,结尾的空格去掉之后,判断长度是否大于0
String 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
异常,也能正常的判断;
4.Map、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()); // false
List 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()); // false
Set 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();
}
6.Optional
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()); // false
Integer 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()
Optional<String> s = Optional.of(new User(null, 10, Optional.of("北京")))
.flatMap(User::getAddr);
Optional<Optional<String>> s1 = Optional.of(new User(null, 10, Optional.of("北京")))
.map(User::getAddr);
System.out.println("flatMap: "+s1);
System.out.println("map: "+s);
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
就能完美解决参数校验问题
10.IDEA提醒
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;
}
public static String t2(String s){
System.out.println(s.length());
return s;
}