1. 程式人生 > >Stack Overflow 上 370萬瀏覽量的一個問題:如何比較 Java 的字串?

Stack Overflow 上 370萬瀏覽量的一個問題:如何比較 Java 的字串?

在逛 Stack Overflow 的時候,發現了一些訪問量像喜馬拉雅山一樣高的問題,比如說這個:如何比較 Java 的字串?訪問量足足有 370萬+,這不得了啊!說明有很多很多的程式設計師被這個問題困擾過。

PS:系列文章回顧:《Stack Overflow 上250萬瀏覽量的一個問題:你物件丟了》

我們來回顧一下提問者的問題:

截止到目前為止,我一直使用“==”操作符來比較字串,直到程式出現了一個 bug,需要使用 .equals() 方法來解決。這是為什麼呢?“==”操作符和 .equals() 方法之間有什麼區別呢?

和提問者相反,在我剛開始學習 Java 的時候,比較字串一直使用的是 .equals()

方法,因為不管是書本還是老師,都告誡我不要直接使用“==”操作符來比較,會出 bug。至於為什麼,書本和老師都沒有幫我搞清楚。

那借此機會,我就來梳理一下 Stack Overflow 上的高贊答案,我們來一起學習進步,打怪升級。

  • “==”操作符用於比較兩個引用(記憶體中的存放地址)是否相等,它們是否是同一個物件。

  • .equals() 用於比較兩個物件的內容是否相等。

怎麼理解這兩句話呢?我來舉個不恰當又很恰當的例子。

有一對雙胞胎,姐姐叫阿麗塔,妹妹叫洛麗塔。我們普通人的眼睛完全無法分辨誰是姐姐誰是妹妹,可她們的媽媽卻可以輕而易舉地辨認出。

.equals() 就好像我們普通人,看見阿麗塔以為是洛麗塔,看見洛麗塔以為是阿麗塔,看起來一樣就覺得她們是同一個人;“==”操作符就好像她們的媽媽,要求更嚴格,觀察更細緻,一眼就能分辨出誰是姐姐誰是妹妹。

String alita = new String("小蘿莉");
String luolita = new String("小蘿莉");

System.out.println(alita.equals(luolita)); // true
System.out.println(alita == luolita); // false

就上面這段程式碼來說,.equals() 輸出的結果為 true,而“==”操作符輸出的結果為 false——前者沒後者要求那麼嚴格。

大家都知道,Java 的所有類都預設地繼承著 Object 這個超類,該類有一個名為 .equals()

的方法,原始碼如下所示。

public boolean equals(Object obj) {
    return (this == obj);
}

可以看得出,Object 類的 .equals() 方法預設採用的是“==”操作符進行比較。假如子類沒有重寫該方法的話,那麼“==”操作符和 .equals() 方法的功效就完全一樣——比較兩個物件的記憶體地址或者物件的引用是否相等。

但實際情況中,有不少類重寫了 .equals() 方法,因為比較記憶體地址太重了,不太符合現實的場景需求。String 類就重寫了 .equals() 方法,原始碼如下所示。

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

可以看得出,如果兩個字串物件“==”,那麼 .equals() 的結果就為 true;否則的話,就比較兩個字串的內容是否相等。

大家應該都知道了,建立字串物件有兩種寫法,如下所示。

String luolita = "小蘿莉";
String alita = new String("小蘿莉");

第一種是在字串常量池(儲存在方法區)中建立物件,並將物件的引用賦值給 luolita。第二種是通過 new 關鍵字在堆中建立物件,並將物件引用賦值給 alita。

PS:字串作為最基礎的資料型別,使用非常頻繁,如果每次都通過 new 關鍵字進行建立,會耗費高昂的時間和空間代價。Java 虛擬機器為了提高效能和減少記憶體開銷,就設計了字串常量池:相同字面量的物件只有一個。

PPS:Java 虛擬機器在執行程式的過程中會把記憶體區域劃分為若干個不同的資料區域,如下圖所示。

下面我們通過實際程式碼來看看字串的比較。

第一種:

new String("小蘿莉").equals("小蘿莉") // --> true 

.equals() 比較的是兩個字串物件的內容是否相等,所以結果為 true。

第二種:

new String("小蘿莉") == "小蘿莉" // --> false 

“==”操作符左側的物件儲存在堆中,右側的物件儲存在方法區,所以返回 false。

第三種:

new String("小蘿莉") == new String("小蘿莉") // --> false 

new 出來的兩個物件肯定是不相等的,所以返回 false。

第四種:

"小蘿莉" == "小蘿莉" // --> true 

字串常量池中只會有一個物件,所以返回 true。

"小蘿莉" == "小" + "蘿莉" // --> true

由於“小”和“蘿莉”都在字元創常量池,所以編譯器會將其自動優化為“小蘿莉”,所以返回 true。

經過大量例項的分析,我們可以得出如下結論(也是對提問者的回答):

  • 當比較兩個字串物件的內容是否相等時,請使用 .equals() 方法。

  • 當比較兩個字串物件是否相等時,請使用“==”操作符。

當然了,如果要進行兩個字串物件的內容比較,除了 .equals() 方法,還有其他可選的方法。

1)Objects.equals()

Objects.equals() 這個靜態方法的優勢在於不需要在呼叫之前判空。

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

如果直接使用 a.equals(b),則需要在呼叫之前對 a 進行判空,否則可能會丟擲空指標 java.lang.NullPointerException

Objects.equals("小蘿莉", new String("小" + "蘿莉")) // --> true
Objects.equals(null, new String("小" + "蘿莉")); // --> false
Objects.equals(null, null) // --> true

String a = null;
a.equals(new String("小" + "蘿莉")); // throw exception

2)String 類的 .contentEquals()

.contentEquals() 的優勢在於可以將字串與任何的字元序列(StringBuffer、StringBuilder、String、CharSequence)進行比較。

public boolean contentEquals(CharSequence cs) {
    // Argument is a StringBuffer, StringBuilder
    if (cs instanceof AbstractStringBuilder) {
        if (cs instanceof StringBuffer) {
            synchronized(cs) {
               return nonSyncContentEquals((AbstractStringBuilder)cs);
            }
        } else {
            return nonSyncContentEquals((AbstractStringBuilder)cs);
        }
    }
    // Argument is a String
    if (cs instanceof String) {
        return equals(cs);
    }
    // Argument is a generic CharSequence
    char v1[] = value;
    int n = v1.length;
    if (n != cs.length()) {
        return false;
    }
    for (int i = 0; i < n; i++) {
        if (v1[i] != cs.charAt(i)) {
            return false;
        }
    }
    return true;
}

從原始碼上可以看得出,如果 cs 是 StringBuffer,該方法還會進行同步,非常的智慧化。不過需要注意的是,使用該方法之前,需要確保比較的兩個字串都不為 null,否則將會丟擲空指標。

再強調一點,.equals() 方法在比較的時候需要判 null,而“==”操作符則不需要。

System.out.println( null == null); // --> true

好了各位讀者朋友們,以上就是本文的全部內容了。能看到這裡的都是人才,二哥必須要為你點個贊