1. 程式人生 > 程式設計 >Java 中基於各種資料型別分析 == 和 equals 的區別

Java 中基於各種資料型別分析 == 和 equals 的區別

前言分析一、int 和 Integer1、Integer 物件使用 new 關鍵字生成2、表面上不是 new 關鍵字生成的 Integer 物件3、兩個 int 變數比較4、new 生成的 Integer 物件和 int 變數比較5、非 new 生成的 Integer 物件和 int 變數比較6、非 new 生成的 Integer 物件和 new Integer()生成的物件7、面試題二、double 和 Double1、new 生成的兩個 Double 物件比較2、表面上非 new 生成的 Double 物件比較3、new 生成的 Double 物件和 double 變數比較三、float 和 Float

四、short 和 Short五、long 和 Long六、char 和 Character七、String 比較1、引用指向常量池中 String 常量時比較2、引用指向堆空間中 String 物件3、String 常量與 String 物件比較4、String 常量做拼接操作後比較5、面試題總結參考連結

前言

Java 中的資料型別,可分為兩類:

  1. 基本資料型別,也稱原始資料型別。byte,short,char,int,long,float,double,boolean 它們之間的比較,應用雙等號(==),比較的是它們的值。
  2. 複合資料型別(類)。當它們用雙等號進行比較的時候,比較的是它們在記憶體中的存放地址,所以,除非是同一個 new 出來的物件,它們的比較後的結果為 true,否則比較後結果為 false。 Java 當中所有的類都是繼承於 Object 這個基類的,在 Object 中的基類中定義了一個 equals 的方法,這個方法的初始行為是比較物件的記憶體地址,但在一些類庫當中這個方法被覆蓋掉了,如 String,Integer,Date 在這些類當中 equals 有其自身的實現(在重寫 equals 方法的時候,有必要重寫物件的 hashCode 方法,從而保證程式完整性),而不再是比較類在堆記憶體中的存放地址了。

對於複合資料型別之間進行 equals 比較,在沒有覆寫 equals 方法的情況下,它們之間的比較還是基於它們在記憶體中的存放位置的地址值的,因為 Object 的 equals 方法也是用雙等號進行比較的,所以比較後的結果跟雙等號的結果相同。

分析

一、int 和 Integer

  • int 是基本資料型別,Integer 是 int 的包裝類,也叫做複合資料型別。
  • Integer 變數必須例項化後才能使用;int 變數不需要;
  • Integer 實際是物件的引用,指向此 new 的 Integer 物件;int 是直接儲存資料值 ;
  • Integer 的預設值是 null;int 的預設值是0。
1、Integer 物件使用 new 關鍵字生成
Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.println("i == j:" + (i == j)); //false
System.out.println("i.equals(j):" + (i.equals(j))); //true
System.out.println("i.hashCode():" + i.hashCode());
System.out.println("j.hashCode():" + j.hashCode());
System.out.println("i,it's memory address:" + System.identityHashCode(i));
System.out.println("j,it's memory address:" + System.identityHashCode(j));
複製程式碼

執行結果為:

i == j:false
i.equals(j):true
i.hashCode():100
j.hashCode():100
i,it's memory address:356573597
j,it's memory address:1735600054
複製程式碼

正如上文提到的那樣,複合資料型別使用雙等號的時候是比較其在記憶體中的地址是否相同。一般而言,Object 的 hashCode()預設是返回記憶體地址的,在本例中直接輸出物件的 hashCode 可以發現兩者是一致的,那為什麼==比較結果為 false呢?原因在於hashCode()可以重寫,所以 hashCode()不能代表物件在記憶體的地址。System.identityHashCode(Object)方法可以得到物件的記憶體地址結果(嚴格意義上來講,System.identityHashCode 的返回值和記憶體地址不相等的,該值是記憶體地址通過演演算法換算的一個整數值),不管該物件的類是否重寫了 hashCode()方法。


如上圖所示,Integer 類中關於 equals()方法和 hashCode()方法進行了重寫,所以如果想比對記憶體地址的不同,需要使用System.identityHashCode(Object)方法。

2、表面上不是 new 關鍵字生成的 Integer 物件
Integer i = 100;
Integer j = 100;
System.out.println(i == j:true
複製程式碼

這裡就不得不提出另一種情況:

Integer ii = 128;
Integer jj = 128;
System.out.println("ii == jj:" + (ii == jj)); "ii.equals(jj):" + (ii.equals(jj))); "ii.hashCode():" + ii.hashCode());
System.out.println("jj.hashCode():" + jj.hashCode());
System.out.println("ii,it's memory address:" + System.identityHashCode(ii));
System.out.println("jj,it's memory address:" + System.identityHashCode(jj));

//結果為
ii == jj:false
ii.equals(jj):true
ii.hashCode():128
jj.hashCode():128
ii,it's memory address:2133927002
jj,it'
s memory address:1836019240
複製程式碼

對於兩個非 new 生成的 Integer 物件,進行比較時,如果兩個變數的值在區間 -128 到 127 之間,則比較結果為 true,如果兩個變數的值不在此區間,則比較結果為 false。通過打印出來的地址可以看出來,當不在指定區間範圍時,實際上是兩個不同的物件。
具體原因: Java 在編譯 Integer i = 100 ;時,會翻譯成為 Integer i = Integer.valueOf(100)。而 Java API 中對 Integer 型別的valueOf 的定義如下,對於-128 到 127 之間的數,會儲存在快取中,Integer i = 127 時,會直接從快取中獲取,下次再 Integer j = 127時,同樣從快取中取,而不會 new 個新物件。

public static Integer valueOf(int var0) {
    return var0 >= -128 && var0 <= Integer.IntegerCache.high ? Integer.IntegerCache.cache[var0 + 128] : new Integer(var0);
}
複製程式碼

其中 IntegerCache 類是 Integer 類的內部類,原始碼如下:

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

      private IntegerCache() {
      }

      static {
          int var0 = 127;
          String var1 = VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
          int var2;
          if (var1 != null) {
              try {
                  var2 = Integer.parseInt(var1);
                  var2 = Math.max(var2, 127);
                  var0 = Math.min(var2, 2147483518);
              } catch (NumberFormatException var4) {
              }
          }

          high = var0;
          cache = new Integer[high - -128 + 1];
          var2 = -128;

          for(int var3 = 0; var3 < cache.length; ++var3) {
              cache[var3] = new Integer(var2++);
          }

          assert high >= 127;

      }
  }
複製程式碼

結合這兩部分程式碼可以看出,當數值大小超過 127 時,就要呼叫 new Integer(Object),重新生成一個 Integer 物件,所以在區間範圍外,== 比較返回結果為 false。

3、兩個 int 變數比較
int i = 100;
int j = 100;
System.out.println(i == j:true
i,159); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-string">'s memory address:21685669
j,it'
s memory address:21685669
複製程式碼

對於這種簡單資料型別,== 比較符就是比較它們的值大小。

4、new 生成的 Integer 物件和 int 變數比較
new Integer(100);
"i == j:" + (i == j));
System.out.println(i.hashCode():100
i,it's memory address:2133927002
j,it's memory address:356573597
複製程式碼

基本資料型別 int 和它的包裝類 Integer 比較時,Java 會自動拆包裝為 int(將複合資料型別轉化為基本資料型別),然後進行比較,實際上就變為兩個 int 變數的比較。即使打印出來的地址不同,但是比較結果仍為 true,主要原因是因為不是通過比較記憶體地址進行判斷的。

5、非 new 生成的 Integer 物件和 int 變數比較
int j = 100;
Integer k = 100;
System.out.println("j == k:" + (j == k));
System.out.println( + System.identityHashCode(j));
System.out.println("k,it's memory address:" + System.identityHashCode(k));

int ii = 128;
Integer jj = 128;
System.out.println("ii == jj:" + (ii == jj));
System.out.println( + System.identityHashCode(jj));
複製程式碼

執行結果為:

j == k:true
j,it's memory address:356573597
k,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:356573597
ii == jj:true
ii,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:2133927002
jj,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:1836019240
複製程式碼

比較結果都為 true,因為同 4 一樣,由於自動拆箱的特性,其實是進行值的比較,所以結果為 true。接著分析列印的記憶體地址,當在[-128,127]區間範圍內時,Integer 陣列(參考 Integer 類中的 IntegerCache)是存放在常量池中的,而 int 變數同樣也是,所以值相等時,記憶體地址一致。

6、非 new 生成的 Integer 物件和 new Integer()生成的物件
new Integer(100);
Integer j = 100;
System.out.print(i == j); //false
複製程式碼

因為非 new 生成的 Integer 變數指向的是 Java 常量池中的物件,而 new Integer()生成的變數指向堆中新建的物件,兩者在記憶體中的地址不同。

7、面試題
        Integer i1 = 125;
        Integer i2 = 125;
        Integer i3 = 0;
        Integer i4 = new Integer(127);
        Integer i5 = new Integer(127);
        Integer i6 = new Integer(0);

        System.out.println("i1==i2:\t" + (i1 == i2));
        System.out.println("i1==i2+i3:\t" + (i1 == i2 + i3));
        System.out.println("i4==i5:\t" + (i4 == i5));
        System.out.println("i4==i5+i6:\t" + (i4 == i5 + i6));

        i3 = 5;
        Integer i7 = 130;
        System.out.println("i7==i2+i3:\t" + (i7 == i2 + i3));
複製程式碼

執行結果為:

i1==i2:    true
i1==i2+i3:    true
i4==i5:    false
i4==i5+i6:    true
i7==i2+i3:    true
複製程式碼

對於 i1 == i2 + i3 、 i4 == i5 + i6 和 i7 == i2 + i3 結果為 true,是因為,Java 的數學計算是在記憶體棧裡操作的,Java 會對 i5、i6 進行拆箱操作,其實比較的是基本型別(127=127+0),他們的值相同,因此結果為 true。對 i2+i3 來說,結果是在記憶體棧中(同 int 基本型別一樣),所以不管是與 i1 還是 i7 比較,返回結果都為 true。

二、double 和 Double

1、new 生成的兩個 Double 物件比較
Double i = new Double(100.0);
Double j = new Double(100.0);

System.out.println("i.equals(j):" + (i.equals(j)));
System.out.println(i.hashCode():1079574528
j.hashCode():1079574528
i,it's memory address:1163157884
j,it's memory address:1956725890
複製程式碼

分別生成了兩個不同的物件,地址也不同,所以比較結果範圍為 false。此外,看到列印的 hashCode() 結果一致,再去看一下 Double 類原始碼可以發現,也重寫了 equals 方法和 hashCode 方法。

2、表面上非 new 生成的 Double 物件比較
Double i = 100.0;
Double j = 100.0;

System.out.println(複製程式碼

自動裝箱,解析為 Double i = new Double(100.0);因此實際上還是兩個不同的物件。

3、new 生成的 Double 物件和 double 變數比較
Double i = 100.0;
double j = 100.0;

System.out.println(i == j:true
i.equals(j):'s memory address:21685669
j,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:2133927002

複製程式碼

自動拆箱,轉換為 double 變數進行值比較。

三、float 和 Float

與 double 比較一致,只是兩者的範圍大小有差異,double 型別的取值範圍更廣。

四、short 和 Short

Short i = new Short(new Integer(100).shortValue());
Short j = new Integer(100).shortValue());
System.out.println( + System.identityHashCode(j));
複製程式碼

執行結果:

複製程式碼

new 生成的 Short 物件是兩個獨立的,所以比較結果為 false。

五、long 和 Long

同 Integer 類一樣,Long 類中也有一個內部類 LongCache,原始碼如下:

    LongCache {
        final Long[] cache = new Long[256];

        LongCache() {
        }

        static {
            int var0 = 0; var0 < cache.length; ++var0) {
                cache[var0] = new Long((long)(var0 - 128));
            }

        }
    }
複製程式碼

使用同 Integer 一樣,在[-128,127]區間範圍內,也是使用 Long.valueOf(),在這個區間範圍內的比較返回結果為 true。更多使用參考第一部分。

六、char 和 Character

char 型別存放的是字元型資料,常用範圍:大寫字母(A-Z):65 (A)~ 90(Z);小寫字母(a-z):97(a) ~ 122(z);字元數字('0' ~ '9'):48('0') ~ 57('9')。char 和 int 直接可以相互轉換,所以在使用上很相似,不同的是 Character i = 'a';這樣宣告時,區間範圍為[0,127]。

七、String 比較

前面講了那麼多,終於來到 String 比較,本意上是記錄關於 String 用==比較的情況,但是在學習的過程中,又重新瞭解了其他的資料型別,所以一併記錄下來。

1、引用指向常量池中 String 常量時比較
        String s1 = "abc";
        String s2 = "abc";

        System.out.println("s1 == s2:"+(s1 == s2));
        System.out.println("s1.equals(s2):"+s1.equals(s2));
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());
        System.out.println("s1,it's memory address:" + System.identityHashCode(s1));
        System.out.println("s2,it's memory address:" + System.identityHashCode(s2));
複製程式碼

執行結果為:

s1 == s2:true
s1.equals(s2):true
96354
96354
s1,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:1163157884
s2,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:1163157884
複製程式碼

首先說 s1 和 s2,在棧中開闢兩塊空間存放引用 s1 和 s2,在給 s1 賦值的時候去常量池中查詢,第一次初始化的常量池為空的,所以是沒有的,則在字串常量池中開闢一塊空間,存放 String 常量"abc",並把引用返回給 s1,當 s2 也是這樣的過程,在常量池中找到了,所以 s1 和 s2 指向相同的引用,即 s1==s2 和 s1.equals(s2)都為 true。

String 類中重寫了 equals 方法和 hashCode 方法,原始碼如下:

    boolean equals(Object var1) {
        if (this == var1) {
            return true;
        } else {
            if (var1 instanceof String) {
                String var2 = (String)var1;
                int var3 = this.value.length;
                if (var3 == var2.value.length) {
                    char[] var4 = this.value;
                    char[] var5 = var2.value;

                    int var6 = 0; var3-- != 0; ++var6) {
                        if (var4[var6] != var5[var6]) {
                            false;
                        }
                    }

                    true;
                }
            }

            false;
        }
    }
   int hashCode() {
        int var1 = this.hash;
        if (var1 == 0 && this.value.length > 0) {
            char[] var2 = this.value;

            int var3 = 0; var3 < this.value.length; ++var3) {
                var1 = 31 * var1 + var2[var3];
            }

            this.hash = var1;
        }

        return var1;
    }
複製程式碼

因此當值內容相同時,計算得到的 hashCode() 值也是一致的。

2、引用指向堆空間中 String 物件
String s1 = new String("abc");
String s2 = "abc");

System.out.println("s1 == s2:"+(s1 == s2));
System.out.println("s1.equals(s2):"+s1.equals(s2));
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
System.out.println( + System.identityHashCode(s1));
System.out.println( + System.identityHashCode(s2));

//sb.toString()相當於生成一個新的String物件
StringBuffer sb = new StringBuffer("abc");
String s3 = sb.toString();
System.out.println("s1 == s3:"+(s1 == s3));
System.out.println("s3,it's memory address:" + System.identityHashCode(s3));
複製程式碼

執行結果為:

false
s1.'s memory address:1956725890
s2,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:356573597
s1 == s3:false
s3,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:1735600054
複製程式碼

首先在棧中開闢兩塊塊空間存放引用 s1 和 s2,然後是建立兩個物件,在建立物件的時候是在堆裡面開闢了一個空間,兩個物件自然地地址空間就不相同,這點從列印結果上就可以看出,所以在 s1==s2 是為 false。另外有時會使用到 StringBuffer 物件,再呼叫 toString() 方法,根據原始碼可知,該方法也是建立一個新的物件,所以 s1==s3結果為 false。

synchronized String toString() {
        this.toStringCache == null) {
            this.toStringCache = Arrays.copyOfRange(this.value, 0this.count);
        }

        new String(this.toStringCache,153); font-weight: bold; word-wrap: inherit !important; word-break: inherit !important;" class="hljs-keyword">true);
    }
複製程式碼
3、String 常量與 String 物件比較
"abc";
        String s2 = "abc");

        System.out.println( + System.identityHashCode(s2));

        String s3 = s2.intern();
        System.out.println("s1 == s3:"+(s1 == s3));
        System.out.println('s memory address:1956725890
s1 == s3:true
s3,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:1163157884

複製程式碼

s1 指向的是字串常量池中的“abc”,s2 指向堆中的 String 物件“abc”,所以地址不相同,比較結果也就為 false。再者,String 類中的 intern()方法會從常量池中查詢是否存在這樣的值,如果存在則直接返回,不存在則往常量池中插入一個新的這樣的值,然後返回。


在講解上圖之前,先看一組程式碼:

String s2 = "abc");
String s3 = s2.intern();
String s1 = "abc";    //由於此時常量池中已經有”abc“常量,所以s1直接指向”abc“
System.out.println(s1 == s3);
System.out.println( + System.identityHashCode(s3));

//執行結果
true
s1,159); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-string">'s memory address:1163157884
s3,it'
s memory address:1163157884
複製程式碼

黃色箭頭的含義:當通過 new 生成字串物件時,會先去常量池中查詢是否存在”abc“值,如果沒有則會在常量池中新建一個,然後堆中再建立一個常量池中此”abc”值的拷貝物件。

4、String 常量做拼接操作後比較
        String s = "abc";
        //第一種情況
        String s2 = "ab";
        String s4 = s2 + "c";
        String s6 = "ab");
        String s7 = s6 + "c";
        System.out.println("s4,it's memory address:" + System.identityHashCode(s4));
        System.out.println("s7,it's memory address:" + System.identityHashCode(s7));
        System.out.println("s == s4:"+(s == s4));
        System.out.println("s == s7:"+(s == s7));

        //第二種
        final String s1 = "ab";
        String s3 = s1 + "c";
        String s5 = "ab" + "s,it's memory address:" + System.identityHashCode(s));
        System.out.println( + System.identityHashCode(s3));
        System.out.println("s5,it's memory address:" + System.identityHashCode(s5));
        System.out.println("s == s3:"+(s == s3));
        System.out.println("s == s5:"+(s == s5));
        //第三種
        final String s8 = getData();
        String s9 = s8 + "s == s9:"+(s == s9));
        System.out.println("s9,it's memory address:" + System.identityHashCode(s9));
複製程式碼

執行結果為:

s4,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:1163157884
s7,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:1956725890
s == s4:false
s == s7:false
s,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:356573597
s3,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:356573597
s5,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:356573597
s == s3:true
s == s5:true
s == s9:false
s9,21); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-comment">'s memory address:1735600054
複製程式碼

分析:

情況 1,JVM 對於字串引用,由於在字串的"+"連線中,有字串引用存在,而引用的值在程式編譯期是無法確定的,即 s2+"c" 或 s6+"c"無法被編譯器優化,只有在程式執行期來動態分配並將連線後的新地址賦給 s4 和 s7。所以上面程式的結果也就為 false。

情況 2,和 1 唯一不同的是 s1 字串加了 final 修飾,對於 final 修飾的變數,它在編譯時被解析為常量值的一個本地拷貝儲存到自己的常量池中或嵌入到它的位元組碼流中。所以此時 s1+"c" 和 "ab"+"c" 效果是一樣的,故上面的結果為 true。

情況 3,JVM 對於字串引用 s8,它的值在編譯期無法確定,只有在程式執行期呼叫方法後,將方法的返回值和”c“來動態連線並分配地址為 s9,故上面程式的結果為 false。

5、面試題
public AAA {

    void main(String[] args) {
        // TODO Auto-generated method stub

        String hello = "Hello", lo = "lo";
        System.out.print((hello == "Hello") + " ");    
        System.out.print((Other.hello == hello) + " ");
        System.out.print((other.Other.hello == hello) + " ");
        System.out.print((hello == ("Hel" + "lo")) + "Hel" + lo)) + " ");
        System.out.println(hello == ("Hel" + lo).intern());

        System.out.println(System.identityHashCode(hello ));
        System.out.println(System.identityHashCode(Other.hello ));
        System.out.println(System.identityHashCode(other.Other.hello));
    }
}

Other {
    static String hello = "Hello";
}
複製程式碼
package other;

Other { 
    "Hello";
}
複製程式碼

執行結果為:

true false true
1163157884
1163157884
1163157884
複製程式碼

重點說一下,在同包不同類下,引用自同一 String 物件相比較結果為 true。在不同包不同類下,依然引用自同一 String 物件。

結論:

字串是一個特殊包裝類,其引用是存放在棧裡的,而物件內容必須根據建立方式不同定(常量池和堆).有的是編譯期就已經建立好,存放在字串常量池中,而有的是執行時才被建立,使用new關鍵字,存放在堆中。

總結

平時經常使用的資料型別,沒有多去了解一下其內部的結構,對於一些概念性東西只是靠記憶,沒有想過為何會是這樣的結果。通過這次學習,不僅認識到了 int 和 Integer 的特殊,也對 String 型別有了一個較好的瞭解,對於資料在記憶體中的儲存也有了一定的認識,從而對於“ == 和 equals 的區別”這樣的問題也是豁然開朗。

參考連結