What is a NullPointerException?

来源:互联网 发布:微博域名修改 编辑:程序博客网 时间:2024/06/16 18:20
  • 什么是NullPointerException?
  • 为什么只有引用类型会产生NullPointerExcepiton?
  • 如何避免NullPointerExcepiton?
  • 基本类型一定不会产生NullPointerException吗?
  • null究竟是什么?
  • String.valueOf(null)与重载方法的调用

这是在Stack Overflow上看到的一个问题:What is a NullPointerException, and how do I fix it?

什么是NullPointerException?

NullPointerException应该是非常简单、也是初学者最常见的一个异常,然而这个问题有400+个star,它的提问者有8000+的reputation,而被采纳的答案有2000+的useful,答题者有80000+的reputation。也许,这个问题值得我们稍微思考一下下。


NullPointerException,空指针异常,源码是这么解释的:当程序在需要一个对象的情况下试图使用null,则抛出此异常。

/** * Thrown when an application attempts to use {@code null} in a * case where an object is required. These include: * <ul> * <li>Calling the instance method of a {@code null} object. * <li>Accessing or modifying the field of a {@code null} object. * <li>Taking the length of {@code null} as if it were an array. * <li>Accessing or modifying the slots of {@code null} as if it *     were an array. * <li>Throwing {@code null} as if it were a {@code Throwable} *     value. * </ul> * <p> * Applications should throw instances of this class to indicate * other illegal uses of the {@code null} object. */publicclass NullPointerException extends RuntimeException {    public NullPointerException() {super();}    public NullPointerException(String s) {super(s);}}

看一段很简单的代码:

    static int sInt;    static int[] sArr;    public static void main(String[] args) {         int i;         int[] arr;         System.out.println(i);  // 报错         System.out.println(arr);  // 报错         System.out.println("----------");         System.out.println(sInt);  // 0         System.out.println(sArr);  // null         System.out.println("----------");         sInt = 255;         sArr = new int[]{255};         System.out.println(sInt);  // 255         System.out.println(sArr);  // [I@********    }

当声明局部变量时,会在栈中开辟空间存储这个变量。在刚创建的时候,无论基本类型还是引用类型,都没有记录任何的值。当试图使用一个未赋值的局部变量的时候,编译的时候会报错:The local variable ××× may not have been initialized.
这里写图片描述

当变量被声明为成员变量的时候,情况会有一些区别。基本类型的成员变量会默认初始化为0(0L,0.0,0.0F,\u0000,false),这个值就是变量的字面值,是真实存在、可以直接使用的。而引用类型统一默认初始化为null,表示这个变量没有指向任何引用。
这里写图片描述

现在,显式地变量赋值。虽然给变量赋的值是一样的,但是基本类型变量存储的值为字面值(255),而引用类型存储的是该变量在堆内存中引用的内存地址(0x********),而真正的值存储在这个地址中。
这里写图片描述

因为引用类型变量存储的是一个内存地址(Pointer),当我们使用这个变量的时候,实际上是在使用它所指向的这个内存地址中所存储的对象。所以,当使用了一个没有任何指向的引用类型变量的时候,就会抛出NullPointerException。那么,问题来了。

为什么只有引用类型会产生NullPointerExcepiton?

这个问题也可以有另外一种问法:为什么基本类型的值不可能为空?变量为空,一般是如下三种情况:
- 内部声明的成员变量,未显式赋值。基本类型的默认值是有意义的;而引用类型的默认值是无意义,它只表示”我现在还没有值“。
- 内部声明的变量,接受了一个值为空的赋值。当赋值的时候,基本类型是不接受null这个值的,但是引用类型却可以。
- 外部传入的变量,传入值为空。同上,当接受参数的时候,基本类型是不接受null的,但是引用类型却接受。


如何避免NullPointerExcepiton?

避免NullPointerException的最通常的做法是以下两种:
- 当调用.equal()方法时,如果其中一个变量是常量,则应写为final.equal(variable)而不是variable.equal(final);类似的,如果可以基本确定A不为空,而B不确定,则应写成A.equal(B)。
- 如果不确定一个变量是否为空,则先进行if(null == variable)的判断。


基本类型一定不会产生NullPointerException吗?

是的,一定不会,因为基本类型一定会有一个字面值。但是,有些情况我觉得还是挺有意思的。

    public static void test(int a){        System.out.println(a + 1);    }    public static void test(Integer a){        int b = a + 1;        System.out.println(b);    }

上面的代码有什么不同?想一下,如果传入的是一个空的Integer对象会有怎样的结果?
- test(int a),空值传不进来,调用时直接抛出NullPointerException。
- test(Integer a),接受传值,在计算int b = a + 1时抛出NullPointerException。

也就是说,基本类型的包装类在自动拆箱时必须有一个真实有意义的值。这也提示我们,虽然包装类在很多情况下可以和基本类型混用,但永远不能忘记他们是引用类型这个事实。


null究竟是什么?

    public static void main(String[] args) {         Object object = null;         System.out.println(null instanceof Object);    // false         System.out.println(object instanceof Object);  // false         System.out.println(String.valueOf(object));    // null          System.out.println(String.valueOf(null));  // throw NullPointerException    }
  • null instanceof Object结果为fasle,也就是说null不是Object的对象,而我们知道Java中所有对象都是Object的对象,所以null不是一个对象。
  • object instanceof Object结果为false,很容易理解,因为此时object实际上为null。这提示我们,obj instanceof TypeName可以用来代替if(null == obj)同时做类型检查和非空检查。
  • String.valueOf(object)输出null,显然,接受传参时只检查变量的类型,而不会检查引用的真实性。至于方法内部也没有报错,是因为valueOf()对null值做了例外处理。
    public static String valueOf(Object obj) {        return (obj == null) ? "null" : obj.toString();    }
  • Java的参数传递,实质上都是值传递(地址值也是一个值),null可以被JVM传递,而它又不是一个地址值,所以我认为null应该是一个特殊的字面值,表示这是一个引用,但没有指向任何内存地址。
  • 既然如此,那String.valueOf(null)直接抛出NullPointerException又是为什么呢?再看:

String.valueOf(null)与重载方法的调用

public class Demo1 {    public static void main(String[] args) {        test1(null); //报错    }    public static void test(Child c){        if(c == null){            System.out.println("Null Child");        }else{            System.out.println("Not Null Child");        }    }       public static void test(Super s){        if(s == null){            System.out.println("Null Super");        }else{            System.out.println("Not Null Super");        }    }}class Child{}class Super{}
public class Demo2 {    public static void main(String[] args) {        test1(null); // Null Child    }    public static void test(Child c){        if(c == null){            System.out.println("Null Child");        }else{            System.out.println("Not Null Child");        }    }       public static void test(Super s){        if(s == null){            System.out.println("Null Super");        }else{            System.out.println("Not Null Super");        }    }}class Child extends Super{}class Super{}
public class Demo3 {        public static void main(String[] args) {        Super s = null;        test1(s); // Null Super    }    public static void test(Child c){        if(c == null){            System.out.println("Null Child");        }else{            System.out.println("Not Null Child");        }    }    public static void test(Super s){        if(s == null){            System.out.println("Null Super");        }else{            System.out.println("Not Null Super");        }    }}class Child extends Super{}class Super{}
public class Demo4 {    public static void main(String[] args) {        System.out.println(((Super)new Child()).getClass()); // class Child         test((Super)new Child()); //Not Null Super    }    public static void test(Child c){        if(c == null){            System.out.println("Null Child");        }else{            System.out.println("Not Null Child");        }    }    public static void test(Super s){        if(s == null){            System.out.println("Null Super");        }else{            System.out.println("Not Null Super");        }    }}class Child extends Super{}class Super{}

上面的四段代码:
- Demo1编译时直接报错”The method test(Child) is ambiguous for the type Demo1.”Demo1这类中有两个重载的方法符合test(null)的调用条件,JVM无法确定究竟调用哪一个。
- Demo2输出Null Child,这里Child定义为Super的子类,Child的范围更小,所以test(Child)比test(Super)更符合调用条件,于是JVM调用了test(Child)。
- Demo3输出Null Super,虽然实际传入的值是null,但存储它的变量被声明为Super,JVM在此认为这是一个Super类型值。
- Demo4输出Not Null Super,虽然实际创建的是Child对象,但已将其类型强制声明为Super。

于是我们得出以下结论:
- null在作为参数传递时可以匹配任何类型,与此同时通过instanceof运算符它不能匹配任何类型。
- 当同时有多个方法匹配调用条件时,JVM会调用最匹配的方法,如果有多个方法同等匹配,则编译报错。
- JVM在匹配参数时,根据对象的声明类型,而不是运行时类型做匹配。

再回到String.valueOf(null),我们发现String有两个重载的valueOf():

    public static String valueOf(Object obj) {        return (obj == null) ? "null" : obj.toString();    }    public static String valueOf(char data[]) {        return new String(data);    }

String.valueOf(object)调用的是valueOf(Object obj),这个方法内部对null做了特殊处理,所以传入任何值都是可以的。而String.valueOf(null)调用的是valueOf(char data[]),遗憾的是它并没有处理null,于是导致报错。

原创粉丝点击