Java 编程中哪个异常是你印象最深刻的,那 NullPointerException
空指针可以说是臭名昭著的。不要说初级程序员会碰到,即使是中级,专家级程序员稍不留神,就会掉入这个坑里。
Java 中任何对象都有可能为空,当我们调用空对象的方法时就会抛出 NullPointerException
空指针异常,这是一种非常常见的错误类型。我们可以使用若干种方法来避免产生这类异常,使得我们的代码更为健壮。
1、调用了空对象的实例方法
public class Person {
// 属性
private String name;
private int age;
public void print(){
System.out.println("This is Person Class");
}
public String read(){
System.out.println("This is person read");
return null;
}
public static void main(String[] args)
{
Person person = null;
//调用了空对象的实例方法
person.print();
}
}
2、访问了空对象的属性
Person person = null;
//调用了空对象的属性
System.out.println(person.age);
3、当数组是一个空对象时,取它的长度
Person person = null;
//当数组是一个空对象时,取它的长度
System.out.println(person.name.length());
4、方法的返回值是 null, 调用方直接去使用
Person person = new Person();
//方法的返回值是 null, 调用方直接去使用
System.out.println(person.read().contains("Java"));
1、使用之前一定要初始化,或检查是否初始化;
2、尽量避免在函数中返回 NULL, 或给出详细的注释;
3、外部传值,除非有明细的说明,否则,一定要判断是否为 NULL。
Object类中的equals 方法在非空对象引用上实现相等关系,具有对称性 x.equals(y) 和 y.equals(x) 结果是一样的,但当x == null时会抛出空指针异常。
例如:
String x = null;
String y = "world";
if(x.equals(y)){ // java.lang.NullPointerException
}
if(y.equals(x)){//即便 x 是 null也能避免 NullPointerException
}
所以我们要把确定不为null的对象或值放在前面。
调用null对象的toString()
会抛出空指针异常,使用valueOf()
可以获得相同的值,传递一个 null 给 valueOf() 将会返回null。尤其是在那些包装类,像Integer、Float、Double和BigDecimal。
例如:
Integer i = null;
System.out.println(i.toString()); // 抛出NullPointerException异常
System.out.println(String.valueOf(i)); // 返回 null不会出现异常
Java 8 引入了 Optional 类型,我们可以用它来对函数的返回值进行包装。这种方式的优点是可以明确定义该方法是有可能返回空值的,因此调用方必须做好相应处理,这样也就不会引发空指针异常。但是,也不可避免地需要编写更多代码,而且会产生很多垃圾对象,增加 GC 的压力,因此在使用时需要酌情考虑。
在业务系统中,对象中嵌套对象是经常发生的场景,如下示例代码:
// 最外层对象
class Outer {
Nested nested;
Nested getNested() {
return nested;
}
}
// 第二层对象
class Nested {
Inner inner;
Inner getInner() {
return inner;
}
}
// 最底层对象
class Inner {
String foo;
String getFoo() {
return foo;
}
}
业务中,假设我们需要获取 Outer
对象对底层的 Inner
中的 foo
属性,我们必须写一堆的非空校验,来防止发生 NullPointerException
:
// 繁琐的代码
Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
System.out.println(outer.nested.inner.foo);
}
在 Java8 中,我们有更优雅的解决方式,那就是使用 Optional
是说,我们可以在一行代码中,进行流水式的 map
操作。而 map 方法内部会自动进行空校验:
Optional.of(new Outer())
.map(Outer::getNested)
.map(Nested::getInner)
.map(Inner::getFoo
.ifPresent(System.out::println); // 如果不为空,最终输出 foo 的值
我们可以利用 suppiler
函数来出一个终极解决方案:
public static <T> Optional<T> resolve(Supplier<T> resolver) {
try {
T result = resolver.get();
return Optional.ofNullable(result);
}
catch (NullPointerException e) {
// 可能会抛出空指针异常,直接返回一个空的 Optional 对象
return Optional.empty();
}
}
利用上面的 resolve
方法来重构上述的非空校验代码段:
Outer obj = new Outer();
// 直接调用 resolve 方法,内部做空指针的处理
resolve(() -> obj.getNested().getInner().getFoo());
.ifPresent(System.out::println); // 如果不为空,最终输出 foo 的值
有很多开源库已经为您做了繁重的空指针检查工作。其中最常用的一个的是Apache commons 中的StringUtils
。你可以使用StringUtils.isBlank()
,isNumeric()
,isWhiteSpace()
以及其他的工具方法而不用担心空指针异常。
//StringUtils方法是空指针安全的,他们不会抛出空指针异常
System.out.println(StringUtils.isEmpty(null));
System.out.println(StringUtils.isBlank(null));
System.out.println(StringUtils.isNumeric(null));
System.out.println(StringUtils.isAllUpperCase(null));
输出:
true
true
false
false
断言是用来检查程序的安全性的,在使用之前进行检查条件,如果不符合条件就报异常,符合就继续。
Java 中自带的断言关键字:assert,如:
assert name == null : "名称不能为空";
输出:
Exception in thread "main" java.lang.AssertionError: 名称不正确
不过默认是不启动断言检查的,需要要带上 JVM 参数:-enableassertions 才能生效。
Java 中这个用的很少,建议使用 Spring 中的,更强大,更方便好用。
Spring中的用法:
Assert.notNull(name,"名称不能为空");
一个非常好的技术是创建返回一个空集合的方法,而不是一个null值。你的应用程序的代码可以遍历空集合并使用它的方法和字段,而不会抛出一个NullPointerException。Collections类提供了方便的空 List,Set 和 Map: Collections.EMPTY_LIST
,Collections.EMPTY_SET
,Collections.EMPTY_MAP
。
例如:
public class Example {
private static List<Integer> numbers = null;
public static List<Integer> getList() {
if (numbers == null){
return Collections.EMPTY_LIST;
} else {
return numbers;
}
}
}
1、变量赋值自动拆箱出现的空指针
Long i = null;
//变量赋值自动拆箱出现的空指针
long j = i;
Long 自动拆箱为 long 实质是调用了 longValue 方法,由于以上的 Long 类型变量值为 null,自动拆箱就会报 NPE。
2、方法传参时自动拆箱引起空指针异常
public static int add(int x, int y)
{
return x + y;
}
public static void main(String[] args)
{
Integer left = null;
Integer right = null;
//方法传参时自动拆箱引起空指针异常
System.out.println(add(left, right));
}
以上自动拆箱报 NPE,原因在于装箱的值为 null,调用 null 的任何 方法都会报错。
规避指南:
基本数据类型比包装器类型更加节省时间与空间。
关于怎么有效避免空指针,其实还有更多,如何避免空指针,一是要注意代码编写规范,二是要提高代码素养。