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; }
不可否认,这种写法适应了代码框架,保证了你的代码更安全了,但是也牺牲掉了开发的灵活性。