侧边栏壁纸
博主头像
why

一个主要敲代码,经常怼文章,偶尔拍视频的成都人。

  • 累计撰写 181 篇文章
  • 累计创建 11 个标签
  • 累计收到 88 条评论
JVM

为什么 return null 的时候,程序不会抛出 NPE 呢?

why
why
2021-05-21 / 0 评论 / 2 点赞 / 147 阅读 / 3,198 字
温馨提示:
关注公众号why技术,第一时间接收最新文章。

你好呀,我是why。

今天逛知乎,看到这个问题的时候我一下都懵逼了。

为什么 return null 的时候,程序不会抛出 NPE 呢?

好像有千言万语,又不知从何说起。

我把它归结到常识问题这一类,要对一个常识问题进行解释,还真的不太好组织语言。

但是R大回答了这个问题,看看他是怎么解释这个问题的。

R大的回答

Java语言层面:null值自身是不会引起任何问题的。它安安静静的待在某个地方(局部变量、成员字段、静态字段)不会有任何问题;它从一个地方被搬运到另一个地方也不会有任何问题(变量赋值、返回值等)。

唯一会因为null值而引起NullPointerException的动作是“解引用”(dereference)——也就是通过这个引用要对其引用的对象做操作。俗话说就是所有隐含“obj.xxx”的操作中,obj为null值的情况。

在Java里,下述操作隐含对引用的解引用:

  • 读字段(字节码 getfield):x.y,当x为null时抛NPE;
  • 写字段(字节码 putfield):x.y = z,当x为null时抛NPE。注意:z的值是什么没关系;
  • 读数组长度(字节码 arraylength):a.length,当a为null时抛NPE;
  • 读数组元素(字节码 aload,为类型前缀):a[i],当a为null时抛NPE;
  • 写数组元素(字节码 astore,为类型前缀):a[i] = x,当a为null时抛NPE。注意:x的值时什么没关系;
  • 调用成员方法(字节码 invokevirtual、invokeinterface、invokespecial):obj.foo(x, y, z),当obj为null时抛NPE。注意:参数的值是什么没关系;
  • 增强for循环(也叫foreach循环),对数组时(实际隐含a.length操作):for (E e : a) , 当a为null时抛NPE;对Iterable时(实际隐含对Iterable.iterator()的调用):for (E e : es) ,当es为null时抛NPE;
  • 自动拆箱(实际隐含 .Value() 的调用,为包装类型名,为对应的原始类型名): (int) integerObj,当integerObj为null时抛NPE;
  • 对String做switch(实际隐含的操作包含对String.hashCode()的调用):switch (s) { case "abc": ... } ,当s为null时抛NPE;
  • 创建内部类对象实例(字节码 new,但这里特指创建内部类实例的情况):outer.new Inner(x, y, z),当outer为null时抛NPE;
  • 抛异常(字节码 athrow):throw obj,当obj(throw表达式的参数)为null时抛NPE;
  • 用synchronized关键字给对象加锁(字节码 monitorenter / monitorexit):synchronized (obj) ,当obj为null时抛NPE。

Java语言里所有其它语法结构都不会因为null值而隐含抛NPE的语义。当然,用户可以在自己需要的地方显式检查null值然后自己抛出NPE,就像:

  • java.util.Objects.requireNonNull(Object)
    /**
     * Checks that the specified object reference is not {@code null}. This
     * method is designed primarily for doing parameter validation in methods
     * and constructors, as demonstrated below:
     * <blockquote><pre>
     * public Foo(Bar bar) {
     *     this.bar = Objects.requireNonNull(bar);
     * }
     * </pre></blockquote>
     *
     * @param obj the object reference to check for nullity
     * @param <T> the type of the reference
     * @return {@code obj} if not {@code null}
     * @throws NullPointerException if {@code obj} is {@code null}
     */
    public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }

自己主动throw new NullPointerException()这种情况JVM管不着,用户代码主动指定的,用户想怎么搞就怎么搞。

趣味题:在Java语言里,只使用Java语言及标准库的功能而不依赖第三方库,检查一个引用obj是否为null并在null时抛NPE的代码是什么?

答案:obj.getClass()。这是因为getClass()是java.lang.Object类上的方法,因而无论什么引用类型都可以使用。这在Java源码层面和在Java字节码层面上都是最短的。

当然这是个很邪恶的歪招,然而在OpenJDK的标准库内部实现中并不少见。大家…还是用Objects.requireNonNull()就好。

P.S. 技术问题请不要依赖百度百科 >_<

why哥说

这个问题下面还有这样的一个回复:

从语言层面和工程层面来解读了这个问题。

说真的,我也认为在程序里面引入 return null 不太好,加入这样一行代码后势必会带来一个非空判断,是不太优雅的。

还不如抛出一个异常。

而关于这个问题,在《阿里巴巴JAVA开发手册》里面是这样描述的:

总之,求同存异,目的都是为了防止 NPE 的出现。

防止 NPE 是程序员的基本修养。

最后,R大说的这个事:

之前我是不知道还有这样的骚操作的。

于是我去看了一下 obj.getClass() 方法的调用处,还真是有这样的操作。

比如这里,要是没有 R 大的解释,是不是得把自己看懵逼:

最后,关于自动拆箱装箱:

给你分享一个题目:

https://github.com/mercyblitz/tech-daily-questions/issues/1?from=timeline

public class Parsing {
    /**
     * Returns Integer corresponding to s, or null if s is null.
     * @throws NumberFormatException if s is nonnull and
     * doesn't represent a valid integer
     */
    public static Integer parseInt(String s) {
        return (s == null) ?
                (Integer) null : Integer.parseInt(s);
    }
    public static void main(String[] args) {
        System.out.println(parseInt("-1") + " " +
                parseInt(null) + " " +
                parseInt("1"));
    }
}

以上程序输出内容是?

  • (a) 运行时异常
  • (b) -1 null 1
  • (c) -1 0 1
  • (d) 编译错误

感觉这一题大家都被(Integer) null误导了,其实(Integer) null是不会报空指针的,真正的原因来自于隐藏的拆箱操作,因为三元表达式前后类型不一致,前面是Integer类型,后面是int类型,所以这里会先将(Integer) null的值拆成int类型,把一个null赋值给一个int类型的变量就会存在空指针。这里如果把三元表达式后面的parseInt 方法改成valueOf方法就没有空指针。

0

评论区