【源碼分析】面試問爛的equals和各類字符串、Integer比較

今天在空閒時間聊天時發現,面試題中的equals問題,以及String、Integer等的判等問題仍是討論的比較激烈並且混亂。。。(滑稽)java

其實網上有很是多關於這種面試題的文章或者博客,其實多去看看就能夠。面試

不過。。。看了好多,都是通篇理論,感受很乾。思考以後,決定開一個新的模塊,經過源碼來解釋面試題中、或者常見的一些問題中的原理以及本質,幫助有疑惑的小夥伴更容易、更深刻的理解原理,以及相應的源碼設計。數組

說到正題,這篇文章討論的是關於equals在不一樣對象、以及特殊類型String、Integer上的實際原理。緩存

1. ==與equals

這一部分屬於J2SE最基礎的東西了,算是常識性的,也沒什麼好說的。app

  • 基本數據類型,只有==,它是比較兩個變量的值是否一致ui

  • 引用數據類型的比對須要區別對待this

    • ==:比較兩個對象的內存地址是否相同
    • equals:調用一個對象的equals方法,與另外一個對象比對

就由於這些最基本的知識,引起了不少面試題,我們一一列舉編碼


2. String的常見面試題

public class Demo {
   public static void main(String[] args) throws Exception {
       String str1 = "123";
       String str2 = "123";
       String str3 = new String("123");
       String str4 = new String(str1);
       StringBuilder sb = new StringBuilder("123");
       System.out.println(str1 == str2);
       System.out.println(str1 == str3);
       System.out.println(str1 == str4);
       System.out.println(str3 == str4);
       System.out.println(str1.equals(str2));
       System.out.println(str1.equals(str3));
       System.out.println(str1.equals(sb.toString()));
       System.out.println(sb.equals(str1));
   }
}

上面的源碼列舉了最多見的幾種判斷String是否相等的狀況,小夥伴們能夠先不要往下拉,先思考一下輸出結果都應該是怎樣的。。。設計

。。。。。。code

。。。。。。

。。。。。。

。。。。。。

。。。。。。

。。。。。。

輸出結果:1 5 6 7爲true,其他爲false(你都答對了嗎)

下面我們來一一分析。

2.1 "123" == "123"

String str1 = "123";
        String str2 = "123";
        System.out.println(str1 == str2); // true
        System.out.println(str1.equals(str2)); // true

第一種狀況,要了解Java虛擬機在處理String時的特殊機制:字符串常量池。

簡單來講,若是某一個字符串在程序的任意位置被聲明出來,則Java虛擬機會將該字符串放入字符串常量池緩存起來,若是後續還有其餘字符串變量須要引用該字符串,只須要將該變量引用指向常量池內的字符串常量便可。

固然,這個機制的前提是:String類必須是不可變的。

【源碼】String類的聲明:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

String被聲明爲final,也就是不容許有子類,同時一個String就是一個不可變的對象。

因此從上面的解釋中,也就明白了,兩個對象其實都是指向了字符串常量池中的同一個字符串常量!因此用==比對的結果是true。

那==都是true了,對於equals方法來說,那確定也是true。

2.2 「123」 == new String("123")

String str1 = "123";
        String str3 = new String("123");
        String str4 = new String(str1);
        System.out.println(str1 == str3); // false
        System.out.println(str1 == str4); // false
        System.out.println(str3 == str4); // false
        System.out.println(str1.equals(str3)); // true

這幾種狀況是面試問的最多的。。。

網上大片的理論核心都是說:由於構造方法總會建立新的對象,因此上面的str1, str3, str4都各不相同。。。

可爲何是這樣呢?

【源碼】String的構造方法

/** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
    
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

能夠看到,傳入String的構造方法,實際上是將參數中String的value賦給當前正在實例化的String對象,而這個value是一個char[]。

也就是說,這兩個對象,是兩個徹底不一樣的對象,但共享同一個char[]

因此==比對的是內存地址,天然也就不一樣。但爲何equals方法返回的是true呢?

【源碼】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;
    }

String類重寫了equals方法,因此再也不是僅僅調用==而已。。。

上面也提了,若是兩個對象的內存地址都相同,那天然是同一個對象,因此在equals方法中也處理了這一步。

關鍵是下面的if結構:

在這裏,針對String進行額外的比對,也就是比對char[]中的內容是否徹底一致

而上面的幾種狀況,都是由"123"這個字符串複製而來,因此char[]天然都是同一個,也就證實這三個String對象,內容相同,但內存地址不一樣

2.3 String與StringBuilder

String str1 = "123";
        StringBuilder sb = new StringBuilder("123");
        System.out.println(str1.equals(sb.toString())); // true
        System.out.println(sb.equals(str1)); // false

StringBuilder能夠理解是一種字符串拼接中間件吧,它的內部其實也是維護了一個char[]。

【源碼】StringBuilder與其父類的部分源碼

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence{
    
    public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }
    
    //......
}

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;
    
    //......
}

說點題外話,原題中這種狀況下,這個StringBuilder對象sb,在實例化時將一個String傳入,實際上是將這個String塞入內部的char[],並額外擴容16個空間

也就是:[123 ](16個空格)

回到正題上。那在比較一個String對象和一個StringBuilder對象時,就須要看StringBuilder有沒有重寫equals方法。

查看源碼,使用IDE的方法搜索,並無找到StringBuilder有重寫equals方法,父類亦如此。

StringBuilder中沒有重寫equals方法

AbstractStringBuilder也沒有重寫equals方法

也就是說,最終調用的仍是Object的equals方法,而。。。

【源碼】Object的equals方法

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

Object的equals方法,也就是==

顯而易見,一個是String,一個是StringBuilder,類型都不相同,確定就不一樣了。。。

那爲何str1.equals(sb.toString())會返回true呢?

【源碼】StringBuilder的toString方法

public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

在這裏,它從新建立了一個String對象,這個對象是截取了StringBuilder內部維護的char[]的一部分,這個count是最後一個有字符的數組索引+1,那天然就是有內容的那一部分。

那原來的str1,內容就是"123",上面構造的StringBuilder,內容也是"123",那調用toString方法時,返回的天然也就是"123",最後兩個字符串進行比對,天然返回true。


3. Integer的常見面試題

public class Demo {
    public static void main(String[] args) throws Exception {
        Integer num1 = 100;
        Integer num2 = 100;
        Integer num3 = 200;
        Integer num4 = 200;
        Integer num5 = new Integer(100);
        Integer num6 = new Integer("100");
        Integer num7 = Integer.valueOf(100);
        Integer num8 = Integer.valueOf("100");
        Integer num9 = Integer.parseInt("100");
        Long l1 = 100L;
        Integer num10 = l1.intValue();
        
        System.out.println(num1 == num2); 
        System.out.println(num3 == num4); 
        System.out.println(num1 == num5); 
        System.out.println(num1 == num6); 
        System.out.println(num1 == num7); 
        System.out.println(num1 == num8); 
        System.out.println(num1 == num9); 
        System.out.println(num1 == num10);
        System.out.println(num5 == num6); 
        System.out.println(num5 == num7); 
    }
}

上面的源碼列舉了最多見的幾種判斷Integer對象是否相等的狀況,小夥伴們能夠先不要往下拉,先思考一下輸出結果都應該是怎樣的。。。

。。。。。。

。。。。。。

。。。。。。

。。。。。。

。。。。。。

。。。。。。

輸出結果:1 5 6 7 8爲true,其他爲false(你都答對了嗎)

下面我們來一一分析。

3.1 自動裝箱

Integer num1 = 100;
        Integer num2 = 100;
        Integer num3 = 200;
        Integer num4 = 200;
        System.out.println(num1 == num2); // true
        System.out.println(num3 == num4); // false

咱們都知道,jdk1.5後引入了自動裝箱機制,基本數據類型能夠在編碼時直接隱式轉換爲引用數據類型(包裝類)。

實際上,咱們編寫的這段源碼,通過編譯再反編譯,拿到的反編譯源碼 ↓

【反編譯】上述源碼的反編譯後的結果:

Integer num1 = Integer.valueOf(100);
        Integer num2 = Integer.valueOf(100);
        Integer num3 = Integer.valueOf(200);
        Integer num4 = Integer.valueOf(200);

那既然在進行==對比時還打印了true,惟一的可能性是Integer.valueOf方法,兩次返回了同一個對象!

那咱們就須要追到valueOf方法中一探究竟。

【源碼】Integer的部分源碼:

public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

能夠看到,在valueOf方法中,其實是先判斷傳入的數是否在一個範圍內:若是在,會從IntegerCache中取,不然直接建立新的Integer對象

這個IntegerCache是一個高速緩衝區,它緩衝了從-128到127之間的全部整數的包裝對象(源碼中low=-128, high=127)

傳入的數爲100,在緩衝區範圍內,故兩次都取的緩衝區的對象,天然也就是同一個對象了。

傳入的數爲200,不在緩衝區範圍內,故兩次都調用了構造方法,天然就是兩個不一樣的對象。

3.2 構造方法

Integer num1 = 100;
        Integer num5 = new Integer(100);
        Integer num6 = new Integer("100");
        System.out.println(num1 == num5); // false
        System.out.println(num1 == num6); // false
        System.out.println(num5 == num6); // false

構造方法,一定是建立新對象,因此上述實際是一個高速緩衝區的對象和兩個獨立建立的對象

【源碼】Integer的兩個構造方法:

public Integer(int value) {
        this.value = value;
    }

    public Integer(String s) throws NumberFormatException {
        this.value = parseInt(s, 10);
    }

可見兩種構造方法都是將值賦給了即將實例化的對象的value成員中,那天然就是不一樣對象了。

3.3 valueOf與parseInt

Integer num1 = 100;
        Integer num8 = Integer.valueOf("100");
        Integer num9 = Integer.parseInt("100");
        System.out.println(num1 == num8); // true
        System.out.println(num1 == num9); // true

valueOf不說了,上面提到了,是從高速緩衝區取對象,可是parseInt方法呢?

注意:不要被這種陷阱迷惑!parseInt的返回值是int!

public static int parseInt(String s) throws NumberFormatException {
        return parseInt(s,10);
    }

也就是說,返回值爲int,還不能直接將值賦給num9,還須要多作一步,也就是valueOf。。。

說白了仍是從高速緩衝區拿。。。

【反編譯】上述源碼反編譯的結果:

Integer num1 = Integer.valueOf(100);
    Integer num8 = Integer.valueOf("100");
    Integer num9 = Integer.valueOf(Integer.parseInt("100"));

分明就是執行了三次valueOf!

3.4 Integer與Long

Integer num1 = 100;
        Long l1 = 100L;
        Integer num10 = l1.intValue();
        System.out.println(num1 == num10); // true

調用Long對象的intValue操做,其實看方法名也能看出來,返回的是int。。。又是int。。。

好了,不用我說了,都明白這種騷套路了吧,又是valueOf。。。

也甭看源碼了,直接看反編譯的結果吧。。。

【反編譯】上述源碼反編譯的結果:

Integer num1 = Integer.valueOf(100);
    Long l1 = Long.valueOf(100L);
    Integer num10 = Integer.valueOf(l1.intValue());

好吧,原來到最後都是valueOf方法,這波騷套路我算是記下來了。。。

(完)

相關文章
相關標籤/搜索