还只会 null != obj 判空,10招让你彻底告别空指针异常

Java
227
0
0
2023-12-15
标签   Java基础

那既然处理起来简单,有什么好纠结的呢?老老实实校验不就完了,但整个处理的过程中对程序员来说体验是非常糟糕的;

  • 让代码冗长很多时候,核心的业务逻辑代码量是不大的,但是一旦加上各种判断、校验,就会让代码变的冗长,可读性、维护性随之下降;
  • 纯苦力活像这种机械式的判空、校验本质上就是一些体力活,没有任何编码乐趣可言,长时间编写这种代码,会丧失对编程的激情;
  • 易背锅很多业务需要多人合作,有时候可能会出现侥幸心里,都认为其他人在用的时候会处理;无形中挖了些坑,一不小心就锅从天降;

基于上面这些不太好的体验,让消除的难度增加了不少;

有时候当需求很着急的时候,程序员大部分都会选择以功能为主,一些不太重要的东西总是想着晚点再来补充,先跳过写重要的内容,结果是一跳过就没有然后了;

为了既能解决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)); // 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:)
at com.ehang.helloworld.controller.NullTest.t(NullTest.java:97)
at com.ehang.helloworld.controller.NullTest.main(NullTest.java:) 

2字符串判空

字符串 是开发过程中使用最多一种数据类型,因此对字符串的判断、校验也就必不可少了,原生的方式都是通过空对象,长度进行判断:

 String str = "一行Java"
if ( null != str && s.length() > 0 ){
 // 对str字符串进行使用
} 

但是,对字符串的校验,除了判空之外,还有很多其他的场景,比如判断是不是空串(String str = “” ),是不是只有空格(String str = ” ” )等等,那这些校验,就会麻烦一些了;不过木有关系,现成的工具类已经足够满足了;

Spring StringUtil工具类

org.springframework.util.StringUtils 是String 框架自带的字符串工具类,功能比较单一,在教新的版本中,这个工具类的字符串判空方法已经被弃用了,所以不太建议使用了;

  • String Utils.isEmpty

空对象以及空串的校验;

 String s = null;
String s = "";
String s = " ";
System.out.println(StringUtils.isEmpty(s)); // true
System.out.println(StringUtils.isEmpty(s)); // true
System.out.println(StringUtils.isEmpty(s)); // 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-lang</artifactId>
</dependency> 
  • StringUtils.isEmpty StringUtils.isNotEmpty

判断字符串对象是否为空,以及字符串长度是否为0; isEmpty 和 isNotEmpty 校验结果相反;

 String s = null;
String s = "";
String s = " ";
System.out.println(StringUtils.isEmpty(s)); // true
System.out.println(StringUtils.isEmpty(s)); // true
System.out.println(StringUtils.isEmpty(s)); // false
System.out.println();
System.out.println(StringUtils.isNotEmpty(s)); // false
System.out.println(StringUtils.isNotEmpty(s)); // false
System.out.println(StringUtils.isNotEmpty(s)); // true 
  • StringUtils.isBlank StringUtils.isNotBlank StringUtils.isEmpty StringUtils.isNotEmpty 判断的基础上,还会将字符串开头,结尾的空格去掉之后,判断长度是否大于0
 String s = null;
String s = "";
String s = " ";
String s = " 1 2 ";
System.out.println(StringUtils.isBlank(s)); // true 空对象
System.out.println(StringUtils.isBlank(s)); // true 长度等于0
System.out.println(StringUtils.isBlank(s)); // true 去掉前后空格之后,长度也等于0
System.out.println(StringUtils.isBlank(s)); // false 去掉前后空格(1 2),长度大于0
System.out.println();
System.out.println(StringUtils.isNotBlank(s)); // false
System.out.println(StringUtils.isNotBlank(s)); // false
System.out.println(StringUtils.isNotBlank(s)); // false
System.out.println(StringUtils.isNotBlank(s)); // true 
  • 其他功能

本文主要是探讨判空校验,lang3 的 StringUtil 工具类几乎涵盖了所有关于String操作的封装,大大降低了我们处理 String 的复杂度,更多功能可参考官方文档

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("", "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("");
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("");
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>.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 o = null;
Optional<Object> op = Optional.of(o1);
Optional<Object> op = Optional.ofNullable(o1); 

Optional.of()

当对象为null时,创建过程就会抛出NPE异常

Optional.ofNullable()

当对象为null时,也能正常返回 Optional 对象

  • 判空 isPresent()
 Integer i = null;
Optional<Integer> op = Optional.of(i1);
System.out.println(op.isPresent()); // false

 Integer  i2 = 123;
Optional<Integer> op = Optional.ofNullable(i2);
System.out.println(op.isPresent()); // true
op.ifPresent(i->{
    System.out.println(i);
}); 

isPresent() 当对象为null返回true,不为空时返回false

lambda表示式的链式处理:

 op.ifPresent(obj->{
    System.out.println(obj);
}); 
  • 取值
 // 取出原值,如果原对象为null会报NoSuchElementException异常
Integer integer = op.get();
// 取出原值,如果原值为空,则返回指点的默认值
Integer integer = op1.orElse(456);
// 取出原值,如果原值为空,返回默认值,不过在返回之前还需要做一些其他的事情
Integer integer = op2.orElseGet(() -> {
    // 在这里做一些其他的操作
    return;
});

// 取出原值,如果原值为空,就抛出指定的异常
op.orElseThrow(RuntimeException::new);
op.orElseThrow(() -> new RuntimeException("不好,我的值是空的!")); 
  • map() 和 flatMap()
  • 编码过程中,经常会出现:a.xxx().yyy().zzz().mmm() 这样链式调用,这个过程,一旦中间有任意一环出现问题,就会NPE异常,因此,我们就可以借助map() 和 flatMap()来避免这个问题;
  • 测试对象:
 @Data
@NoArgs Constructor 
@AllArgsConstructor
 static  class User {
    private String name;

    private Integer age;

    private Optional<String> addr;
} 

测试:

 // 得到姓名的长度,如果没有姓名就返回
Integer nameLen = Optional.of(new User(null,, null))
.map(User::getName)
.map(String::length)
.orElse();
System.out.println(nameLen);

// 得到地址的长度,如果没有姓名就返回
Integer addr = Optional.of(new User(null,, Optional.of("北京")))
.flatMap(User::getAddr)
.map(String::length)
.orElse();
System.out.println(addr); 

map会将返回的对象封装成Optional对象,如果返回的对象本身就是一个Optional对象了,那就使用flatMap()

7断言

Spring 中的 org.springframework.util.Assert 翻译为中文为” 断言 “,它用来断定某一个实际的运行值和预期项是否一致,不一致就抛出异常。借助这个类,同样也可以做判空检验;

Assert 类提供了以下的静态方法:

 Integer i = null;
Assert.notNull(i,"i1 不为空");

Map map = null;
Assert.notEmpty(map,"map 不为空"); 

异常:

 Exception in thread "main" java.lang.IllegalArgumentException: map 不为空
 at org.springframework.util.Assert.notEmpty(Assert.java:)
 at com.ehang.helloworld.controller.NullTest.t(NullTest.java:119)
 at com.ehang.helloworld.controller.NullTest.main(NullTest.java:)

特别注意:

Assert 用来断定某一个实际的运行值和预期项是否一致,所以他和其他工具类的校验方式是反着在;比如 isNull 方法是期望对象为null,如果不为空的时候,就会报错; notNull 表示期望对象不为空,当对象为空时,就会报错;

8 局部变量 使用基本数据类型

在之前的文章《 阿里为何禁止在对象中使用基本数据类型 》中,从性能的角度,推荐局部变量的定义尽量使用基本数据类型,能不用包装类就不用;那么从今天文章的角度来说,使用基本数据类型也能有效的避免空指针异常;

如下实例:

 int x;
Integer y;
System.out.println( x + );  // 编译失败
System.out.println( y + );  // 编译失败

int i =;
Integer j = null;
System.out.println( i + );  // 正常
System.out.println( j + );  // 空指针异常
int m = i; // 正常
int n = j; // 空指针异常

当变量x、y 只定义、不赋值的时候,x + 1 和 y + 1 是没办法通过编译的;而包装类 j 是可以指定 null 对象,当包装类参与运算的时候,首先会做拆箱操作,也就是调用 intValue() 方法,由于对象是空的,调用方法自然就会报空指针;同时,将一个包装类赋值给一个基本数据类型时,同样也会做拆箱操作,自然也就空指针异常了;

但是,基本数据类型就必须指定一个具体值,后续不管运算、还是赋值操作,都不会出现空指针异常;

9提前校验参数

后台数据,绝大部分都是通过终端请求传递上来的,所以需要在最接近用户的地方,把该校验的参数都校验了;比如StringBoot项目,就需要在Controller层将客户端请求的参数做校验,一旦必传的参数没有传值,就应该直接给客户端报错并提醒用户,而不是将这些不符合要求的null值传到Service甚至保存到数据库,尽早的校验并拦截,就能大大降低出问题的概率

之前介绍的 hibernate -validator 就能完美解决参数校验问题,详见: SpringBoot!你的请求、响应、异常规范了吗?

10 IDEA 提醒

IDEA 对空对象或者可能会出现null值的对象会有提醒,可以根据提醒来提前感知并预防

 public static String t(int i){
    String name = null;
    String name = null;
    if(i>){
        name = "ehang";
    }
    t(name1);
    t(name2);
    return name;
}

相信通过这10招,既能轻松解决NPE问题,又不会因此而带来任何的编程负担;简直妙不可言!