03.07 Java 中基於各種數據類型分析 == 和 equals 的區別

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 關鍵字生成

<code>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));/<code>

執行結果為:

<code>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/<code>

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


Java 中基於各種數據類型分析 == 和 equals 的區別

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

2、表面上不是 new 關鍵字生成的 Integer 對象

<code>Integer i = 100;
Integer j = 100;
System.out.println("i == j:" + (i == j)); //true
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));/<code>

執行結果為:

<code>i == j:true
i.equals(j):true
i.hashCode():100
j.hashCode():100
i,it's memory address:21685669
j,it's memory address:21685669/<code>

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

<code>Integer ii = 128;
Integer jj = 128;
System.out.println("ii == jj:" + (ii == jj)); //true
System.out.println("ii.equals(jj):" + (ii.equals(jj))); //true
System.out.println("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/<code>

對於兩個非 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 個新對象。

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

其中 IntegerCache 類是 Integer 類的內部類,源代碼如下:

<code>private static class IntegerCache {
static final int low = -128;
static final int high;
static 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;

}
}/<code>

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

3、兩個 int 變量比較

<code>int i = 100;
int j = 100;
System.out.println("i == j:" + (i == j)); //false
System.out.println("i,it's memory address:" + System.identityHashCode(i));
System.out.println("j,it's memory address:" + System.identityHashCode(j));/<code>

執行結果為:

<code>i == j:true
i,it's memory address:21685669
j,it's memory address:21685669/<code>

對於這種簡單數據類型,== 比較符就是比較它們的值大小。

4、new 生成的 Integer 對象和 int 變量比較

<code>Integer i = new Integer(100);
int j = 100;

System.out.println("i == j:" + (i == j));
System.out.println("i.hashCode():" + i.hashCode());
System.out.println("i,it's memory address:" + System.identityHashCode(i));
System.out.println("j,it's memory address:" + System.identityHashCode(j));/<code>

執行結果為:

<code>i == j:true
i.hashCode():100
i,it's memory address:2133927002
j,it's memory address:356573597/<code>

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

5、非 new 生成的 Integer 對象和 int 變量比較

<code>int j = 100;
Integer k = 100;
System.out.println("j == k:" + (j == k));
System.out.println("j,it's memory address:" + 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("ii,it's memory address:" + System.identityHashCode(ii));
System.out.println("jj,it's memory address:" + System.identityHashCode(jj));/<code>

執行結果為:

<code>j == k:true
j,it's memory address:356573597
k,it's memory address:356573597
ii == jj:true
ii,it's memory address:2133927002

jj,it's memory address:1836019240/<code>

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

6、非 new 生成的 Integer 對象和 new Integer()生成的對象

<code>Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false/<code>

因為非 new 生成的 Integer 變量指向的是 Java 常量池中的對象,而 new Integer()生成的變量指向堆中新建的對象,兩者在內存中的地址不同。

7、面試題

<code>        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));/<code>

執行結果為:

<code>i1==i2: true
i1==i2+i3: true
i4==i5: false
i4==i5+i6: true
i7==i2+i3: true/<code>

對於 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 對象比較

<code>Double i = new Double(100.0);
Double j = new Double(100.0);

System.out.println("i == j:" + (i == j));
System.out.println("i.equals(j):" + (i.equals(j)));
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));
/<code>

執行結果為:

<code>i == j:false
i.equals(j):true
i.hashCode():1079574528
j.hashCode():1079574528
i,it's memory address:1163157884
j,it's memory address:1956725890
/<code>

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

2、表面上非 new 生成的 Double 對象比較

<code>Double i = 100.0;
Double j = 100.0;

System.out.println("i == j:" + (i == j));
System.out.println("i.equals(j):" + (i.equals(j)));
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));
/<code>

執行結果為:

<code>i == j:false
i.equals(j):true
i.hashCode():1079574528
j.hashCode():1079574528
i,it's memory address:356573597
j,it's memory address:1735600054
/<code>

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

3、new 生成的 Double 對象和 double 變量比較

<code>Double i = 100.0;
double j = 100.0;

System.out.println("i == j:" + (i == j));
System.out.println("i.equals(j):" + (i.equals(j)));

System.out.println("i,it's memory address:" + System.identityHashCode(i));
System.out.println("j,it's memory address:" + System.identityHashCode(j));
/<code>

執行結果為:

<code>i == j:true
i.equals(j):true
i,it's memory address:21685669
j,it's memory address:2133927002
/<code>

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

三、float 和 Float

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

四、short 和 Short

<code>Short i = new Short(new Integer(100).shortValue());
Short j = new Short(new Integer(100).shortValue());
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));
/<code>

執行結果:

<code>i == j:false
i.equals(j):true
i.hashCode():100
j.hashCode():100
i,it's memory address:1163157884
j,it's memory address:1956725890
/<code>

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

五、long 和 Long

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

<code>    private static class LongCache {
static final Long[] cache = new Long[256];

private LongCache() {
}

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

}
}
/<code>

使用同 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 常量時比較

<code>        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));
/<code>

執行結果為:

<code>s1 == s2:true
s1.equals(s2):true
96354
96354
s1,it's memory address:1163157884
s2,it's memory address:1163157884
/<code>

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

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

<code>    public 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;

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

return true;
}
}

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

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

this.hash = var1;
}

return var1;
}
/<code>

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

2、引用指向堆空間中 String 對象

<code>String s1 = new String("abc");
String s2 = new String("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));

//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));
/<code>

執行結果為:

<code>s1 == s2:false
s1.equals(s2):true
96354
96354
s1,it's memory address:1956725890
s2,it's memory address:356573597
s1 == s3:false
s3,it's memory address:1735600054
/<code>

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

<code>    public synchronized String toString() {
if (this.toStringCache == null) {
this.toStringCache = Arrays.copyOfRange(this.value, 0, this.count);

}

return new String(this.toStringCache, true);
}
/<code>

3、String 常量與 String 對象比較

<code>        String s1 = "abc";
String s2 = new String("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));

String s3 = s2.intern();
System.out.println("s1 == s3:"+(s1 == s3));
System.out.println("s3,it's memory address:" + System.identityHashCode(s3));
/<code>

執行結果為:

<code>s1 == s2:false
s1.equals(s2):true
96354
96354
s1,it's memory address:1163157884
s2,it's memory address:1956725890
s1 == s3:true
s3,it's memory address:1163157884
/<code>

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

Java 中基於各種數據類型分析 == 和 equals 的區別

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

<code>String s2 = new String("abc");
String s3 = s2.intern();
String s1 = "abc";\t//由於此時常量池中已經有”abc“常量,所以s1直接指向”abc“
System.out.println(s1 == s3);
System.out.println("s1,it's memory address:" + System.identityHashCode(s1));
System.out.println("s3,it's memory address:" + System.identityHashCode(s3));

//執行結果
true
s1,it's memory address:1163157884
s3,it's memory address:1163157884
/<code>

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

4、String 常量做拼接操作後比較

<code>        String s = "abc";
//第一種情況
String s2 = "ab";
String s4 = s2 + "c";
String s6 = new String("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" + "c";
System.out.println("s,it's memory address:" + System.identityHashCode(s));
System.out.println("s3,it's memory address:" + 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 + "c";
System.out.println("s == s9:"+(s == s9));
System.out.println("s9,it's memory address:" + System.identityHashCode(s9));
/<code>

執行結果為:

<code>s4,it's memory address:1163157884
s7,it's memory address:1956725890
s == s4:false
s == s7:false
s,it's memory address:356573597
s3,it's memory address:356573597
s5,it's memory address:356573597
s == s3:true
s == s5:true
s == s9:false
s9,it's memory address:1735600054
/<code>

分析:

情況 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、面試題

<code>public class AAA {

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

String hello = "Hello", lo = "lo";
System.out.print((hello == "Hello") + " ");\t
System.out.print((Other.hello == hello) + " ");
System.out.print((other.Other.hello == hello) + " ");
System.out.print((hello == ("Hel" + "lo")) + " ");
System.out.print((hello == ("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));
}
}

class Other {
static String hello = "Hello";
}

package other;

public class Other {
static String hello = "Hello";
}
/<code>

執行結果為:

<code>true true true true false true
1163157884
1163157884
1163157884

/<code>

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

結論:

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

總結

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

關注我私信“編程”,一起來當個快樂的程序員吧!


分享到:


相關文章: