當前位置: 妍妍網 > 碼農

源分碼析,Java String 對 null 物件的容錯處理

2024-04-25碼農

點選「 IT碼徒 」, 關註,置頂 公眾號

每日技術幹貨,第一時間送達!

1

前言

最近在讀【 Thinking in 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.

大意是:原生型別會被自動初始化為 0,但是物件參照會被初始化為 null,如果你嘗試呼叫該物件的方法,就會丟擲空指標異常。通常,你可以打印一個 null 物件而不會丟擲異常。

第一句相信大家都會容易理解,這是型別初始化的基礎知識,但是第二句就讓我很疑惑:為什麽打印一個 null 物件不會丟擲異常?帶著這個疑問,我開始了解惑之旅。下面我將詳細闡述我解決這個問題的思路,並且深入 JDK 源碼找到問題的答案。

2

解決問題的過程

可以發現,其實這個問題有幾種情況,所以我們分類討論各種情況,看最後能不能得到答案。

首先,我們把這個問題分解為三個小問題,逐一解決。

第一個問題

直接打印 null 的 String 物件,會得到什麽結果?

String s = null;System.out.print(s);

執行的結果是

null

果然如書上說的沒有丟擲異常,而是打印了 null 。顯然問題的線索在於 print 函式的源碼中。我們找到 print 的源碼:

publicvoidprint(String s{ if (s == null) { s = "null"; } write(s);}

看到源碼才發現原來就只是加了一句判斷而已,簡單粗暴,可能你對 JDK 的簡單實作有點失望了。放心,第一個問題只是開胃菜而已,大餐還在後面。

第二個問題

打印一個 null 的非 String 物件,例如說 Integer:

Integer i = null;System.out.print(i);

執行的結果不出意料:

null

我們再去看看 print 的源碼:

publicvoid print(Object obj) { write(String.valueOf(obj));}

有點不一樣的了,看來秘密藏在 valueOf 裏面。

publicstaticString 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 函式,不是它會是誰呢? + 的嫌疑最大,但是 + 又不是函式,我們怎麽看到它的原始碼?這種情況,唯一的解釋就是編譯器動了手腳,天網恢恢,疏而不漏,找不到原始碼,我們可以去看看編譯器生成的字節碼。

    L0LINENUMBER 27 L0ACONST_NULLASTORE 1L1LINENUMBER 28 L1NEW java/lang/StringBuilderDUPINVOKESPECIAL java/lang/StringBuilder.<init> ()VALOAD 1INVOKEVIRTUAL 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 1L2LINENUMBER 29 L2GETSTATIC java/lang/System.out : Ljava/io/PrintStream;ALOAD 1INVOKEVIRTUAL java/io/PrintStream.print (Ljava/lang/String;)V

    看了上面的字節碼是不是一頭霧水?這裏我們就要扯開話題,來侃侃 + 字串拼接的原理了。

    編譯器對字串相加會進行最佳化,首先例項化一個 StringBuilder ,然後把相加的字串按順序 append ,最後呼叫 toString 返回一個String物件。不信你們看看上面的字節碼是不是出現了StringBuilder。

    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; returnthis;}//針對非 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; returnthis;}

    現在我們恍然大悟,append函式如果判斷物件為 null,就會呼叫 appendNull ,填充"null"。

    3

    總結

    上面我們討論了三個問題,由此引出 Java 中 String 對 null 物件的容錯處理。上面的例子沒有覆蓋所有的處理情況,算是拋磚引玉。

    如何讓程式中的 null 物件在我們的控制之中,是我們編程的時候需要時刻註意的事情。

    END

    PS:防止找不到本篇文章,可以收藏點贊,方便翻閱尋找哦。

    往期推薦