Java String 对 null 对象的容错处理

最近看到这样一段话:java

Primitives that are fields in a class are automatically initialized to zero, as noted in the Everything Is an Object chapter. But the object references are initialized to null, and if you try to call methods for any of them, you’ll get an exception-a runtime error. Conveniently, you can still print a null reference without throwing an exception.c++

大意是:原生类型会被自动初始化为 0,可是对象引用会被初始化为 null,若是你尝试调用该对象的方法,就会抛出空指针异常。一般,你能够打印一个 null 对象而不会抛出异常。编程

第一句相信你们都会容易理解,这是类型初始化的基础知识,可是第二句就让我很疑惑:为何打印一个 null 对象不会抛出异常?带着这个疑问,我开始了解惑之旅。下面我将详细阐述我解决这个问题的思路,而且深刻 JDK 源码找到问题的daan。app

解决问题的过程函数

能够发现,其实这个问题有几种状况,因此咱们分类讨论各类状况优化

首先,咱们把这个问题分解为三个小问题,逐一解决。ui

第一个问题this

直接打印 null 的 String 对象,会获得什么结果?指针

String s = null;对象

System.out.print(s);

运行的结果是

null

果真如书上说的没有抛出异常,而是打印了null。显然问题的线索在于print函数的源码中。咱们找到print的源码:

public void print(String s) {

    if (s == null) {

        s = "null";

    }

    write(s);

}

看到源码才发现原来就只是加了一句判断而已,简单粗暴,可能你对 JDK 的简单实现有点失望了。放心,第一个问题只是开胃菜而已,大餐还在后面。

第二个问题

打印一个 null 的非 String 对象,例如说 Integer:

Integer i = null;

System.out.print(i);

运行的结果不出意料:

null

咱们再去看看print的源码:

public void print(Object obj) {

    write(String.valueOf(obj));

}

有点不同的了,看来秘密藏在valueOf里面。

public static String valueOf(Object obj) {

    return (obj == null) ? "null" : obj.toString();

}

看到这里,咱们终于发现了打印 null 对象不会抛出异常的秘密。print方法对 String 对象和非 String 对象分开进行处理。

  1. String 对象:直接判断是否为 null,若是为 null 给 null 对象赋值为”null”。

  2. 非 String 对象:经过调用String.valueOf方法,若是是 null 对象,就返回”null”,不然调用对象的toString方法。

经过上面的处理,能够保证打印 null 对象不会出错。

下面咱们来探讨第三个问题。

第三个问题

null 对象与字符串拼接会获得什么结果?

String s = null;

s = s + "!";

System.out.print(s);

结果可能你也猜到了:

null!

为何呢?跟踪代码运行能够发现,这回跟print没有什么关系。可是上面的代码就调用了print函数,不是它会是谁呢?+的嫌疑最大,可是+又不是函数,咱们怎么看到它的源代码?这种状况,惟一的解释就是编译器动了手脚,天网恢恢,疏而不漏,找不到源代码,咱们能够去看看编译器生成的字节码。

 LINENUMBER 27 L0

 ACONST_NULL

 ASTORE 1

L1

 LINENUMBER 28 L1

 NEW java/lang/StringBuilder

 DUP

 INVOKESPECIAL java/lang/StringBuilder.<init> ()V

 ALOAD 1

 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;

 LDC "!"

 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;

 INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;

 ASTORE 1

L2

 LINENUMBER 29 L2

 GETSTATIC java/lang/System.out : Ljava/io/PrintStream;

 ALOAD 1

 INVOKEVIRTUAL java/io/PrintStream.print (Ljava/lang/String;)V

看了上面的字节码是否是一头雾水?这里咱们就要扯开话题,来侃侃+字符串拼接的原理了。

编译器对字符串相加会进行优化,首先实例化一个StringBuilder,而后把相加的字符串按顺序append,最后调用toString返回一个String对象。不信大家看看上面的字节码是否是出现了StringBuilder。详细的解释参考这篇文章 Java细节:字符串的拼接。

String s = "a" + "b";

//等价于

StringBuilder sb = new StringBuilder();

sb.append("a");

sb.append("b");

String s = sb.toString();

再回到咱们的问题,如今咱们知道秘密在StringBuilder.append函数的源码中。

//针对 String 对象

public AbstractStringBuilder append(String str) {

    if (str == null)

        return appendNull();

    int len = str.length();

    ensureCapacityInternal(count + len);

    str.getChars(0, len, value, count);

    count += len;

    return this;

}

//针对非 String 对象

public AbstractStringBuilder append(Object obj) {

    return append(String.valueOf(obj));

}

private AbstractStringBuilder appendNull() {

    int c = count;

    ensureCapacityInternal(c + 4);

    final char[] value = this.value;

    value[c++] = 'n';

    value[c++] = 'u';

    value[c++] = 'l';

    value[c++] = 'l';

    count = c;

    return this;

}

如今咱们恍然大悟,append函数若是判断对象为 null,就会调用appendNull,填充”null”。

总结

上面咱们讨论了三个问题,由此引出 Java 中 String 对 null 对象的容错处理。上面的例子没有覆盖全部的处理状况,算是抛砖引玉。

如何让程序中的 null 对象在咱们的控制之中,是咱们编程的时候须要时刻注意的事情。