最近看到這樣一段話: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 對象分開進行處理。
String 對象:直接判斷是否爲 null,若是爲 null 給 null 對象賦值爲」null」。
非 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 對象在咱們的控制之中,是咱們編程的時候須要時刻注意的事情。