0.背景

在java中,经常能遇见NullPointerException,俗称“空指针”。这个异常是最常见的异常了,一般表示遇到了null值,导致无法进行下去。第一个原因是开发人员的习惯不好,考虑不够周全,开发过程中没有意识到这个问题。第二个原因是设计人员在设计之初就没有想到某个地方可能出现NullPointerException的情况。那么,空指针的根源到底是什么?怎么才能避免空指针呢?这篇文章简单介绍一下。

1.问题的根源

数据类型这个名词相信很多人在接触编程的时候都接触过,Java和C++一样,是强类型一样,对每一种数据类型都有比较严格的定义。比如下面的操作:

String id=123456;

稍微有点编程基础的人都能看出,这行代码一定会报错,因为我们不能将一个int类型的数据直接赋值给一个String类型。那么就会报错“int cannot to converted to java.lang.String”

当然,我们当然可以用强制类型转换,但是这不是问题的关键,问题的关键是什么?

问题的关键是Null值破坏了Java类型的安全性,强类型的问题得不到保证。因为Null值表示没有被初始化的对象,没有被初始化的对象就可以被初始化成任何值,也就是说,Null值可以分配给任何数据类型。比如下面这样:

String id=null;
Object obj =null;

看似很正常,甚至编译都是没问题的, 但是如果我们操作下面的代码:

System.out.println(id.length());

就会直接报错:NullPointerException。

可以看出,Null值导致了不可能的一些操作。

其实,解决Null主要从两个方面进行考虑:

  • 初始化对象
  • 验证对象

下面说说实际开发中避免NullPointerException的一些方法。

2.使用 !=Null

这个大概是用的最多的一种方法了吧,原理很简单,在返回的对象中直接使用 !=null 来判断非空即可。比如:

if(student !=null){
......
}
// 或者针对于其属性进行判空操作
if(student!=null && student.age!=null){
   System.out.printlen("age=",student.age)
}
3.使用 Optional和map

直接上代码:

Optional.ofNullable(student).map(Student::getAge).map(Age::getAgeNum)
        .ifPresent((ageNum)->{
            System.out.println("ageNum="ageNum)
        });

使用optional既有优点,也有缺点。优点是Optional向我们保证我们在ifPresent lambda 中使用数据不为空。但是,当用户或我们索取的地址为空时,ifPresent将被静默忽略。

而且,即使,我们忘记使用Optional功能,这个想法也会突出现实 .get() ,提醒我们为设计提供检查。

缺点吗,也比较明显,就是会使代码变的冗长且复杂。Java本身就非常冗长了,使用Optional 会变的非常冗长。其次,在map/faltmap/ifpresentbei后,你可能会失去逻辑的意义。再者,Optional本身可能会导致开发人员创建更多的NPE,例如使用Optional.of(nullable)。

4.使用注解 @NotNull @Nullable

首先介绍的是大名鼎鼎的Lombok。其中@Notnull 注解用于生成可以阻止执行但仅在Runtime中的非空检查。这个注解的原理类似于下面这样。

注:IDEA中也提供了@NotNull的注解。

5.使用检查器框架

使用这个的好处就是我们在编译过程中就会给与相应的提示信息,让我们编译过不去。

Checker Framwork提供了 @NotNull 和 @Nullable 注解及可以识别潜在空检查的编译器处理器步骤。该框架可以通过强制开发人员指定Nullability来找到潜在的空值。因此,每当返回某些内容时,必须显示声明返回的结果可以是Nullable还是NotNullable。比如下面这个例子:

public static String getPotentiallyNullableString(){
    if(new Random().nextBoolean()){
        return null;
    }
    return "real string";
}

这个时候,我们使用Checker 框架,看下能否正常编译通过。那么他会直接报一个错误:

ERROR type of expression:null
ERROR method return type:@Initialized @NonNull String

这个报错的意思是,它说我们返回一个可能为空的字符串,并且它没有用@Nullable 注解标注。那么我们加上它。

public static @Nullable String getPotentiallyNullableString(){
    if(new Random().nextBoolean()){
        return null;
    }
    return "real string";
}

同时调用这个方法:

String potentiallyNullableString = getPotentiallyNullableString();
if(potentiallyNullableString!=null){
    System.out.printlen(potentiallyNullableString.length())
}

这个时候就会编译通过了。

当然,检查器框架也有限制。我们创建一个类,并标记为@NonNull。

static class User{
    @NonNull
    private String id;
    private int age;

    //此处省略get和set
}

但是,使用检查器框架会编译失败。Checker Framework强制我们有一个初始化id值的构造函数。如下:

public User(String id){
    this.id =id;
}

不可否认,这种写法适应了代码框架,保证了你的代码更安全了,但是也牺牲掉了开发的灵活性。