前言
equals() 和 hashCode() 都是 Object 對(duì)象中的非 final 方法泌霍,它們?cè)O(shè)計(jì)的目的就是被用來覆蓋(
override
)的,所以在程序設(shè)計(jì)中還是經(jīng)常需要處理這兩個(gè)方法的漆改。而掌握這兩個(gè)方法的覆蓋準(zhǔn)則以及它們的區(qū)別還是很必要的,相關(guān)問題也不少准谚。
首先看看==
和 equals() 的不同籽懦。
對(duì)于基本數(shù)據(jù)類型,==
比較的是它們的值氛魁。
Example
int num1 = 1, num2 = 1;
char ch1 = 'a', ch2 = 'a';
if (num1 == num2 && ch1 == ch2) {
System.out.println(num1 == num2);
System.out.println(ch1 == ch2);
} else {
System.out.println(num1 == num2);
System.out.println(ch1 == ch2);
}
輸出結(jié)果:
true
true
對(duì)于引用類型,==
比較的是它們的內(nèi)存地址厅篓。以 String 為例:
String str1 = "Hello World";
String str2 = "Hello World";
if (str1 == str2) {
System.out.println("str1和str2的地址相同");
} else {
System.out.println("str1和str2的地址不同");
}
輸出結(jié)果:
str1和str2的地址相同
根據(jù)String的源碼解釋:
The String class represents character strings. All string literals in Java programs, such as
"abc"
, are implemented as instances of this class.Strings are constant; their values cannot be changed after they are created.
可見秀存,直接這樣初始化的字符串屬于 String 類的實(shí)現(xiàn),并且 String 類型的字符串自被創(chuàng)建后就是不可變的了羽氮。并且 Java 也推薦這樣初始化 String 或链,這是為什么呢?
在這一構(gòu)造函數(shù)的注釋中這樣寫道:
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
Initializes a newly created String object so that it represents the same sequence of characters as the argument; in other words, the newly created string is a copy of the argument string. Unless an explicit copy of original is needed, use of this constructor is unnecessary since Strings are immutable.
大意就是說這樣初始化 String 對(duì)象档押,其字符序列是與參數(shù)相同的澳盐;也就是說新創(chuàng)建的字符串是參數(shù)字符串的副本祈纯。 因此除非需要顯式的原始副本,否則不必使用此構(gòu)造函數(shù)叼耙,因?yàn)樽址遣豢勺兊摹?/p>
那為什么字符串是不可變的呢腕窥?
在 String 源碼中可以看到,其實(shí)字符串是被存放到了一個(gè) char 類型的數(shù)組中筛婉,且該數(shù)組被 final
關(guān)鍵字修飾簇爆,因此創(chuàng)建好的字符串是不可變的也就可以想通了。
/** The value is used for character storage. */
private final char value[];
因此下面的語(yǔ)句是等價(jià)的:
String str = "abc";
//str等價(jià)于:
char [] data = {'a', 'b', 'c'};
String str = new String(data);
這樣理解了一番之后爽撒,就可以得出一些結(jié)論:
- 如果要初始化字符串入蛆,直接給 String 類型的變量賦值即可;
- 但如果要轉(zhuǎn)換數(shù)組為字符串硕勿, String 類提供了不少相應(yīng)的構(gòu)造函數(shù)哨毁,可以查閱文檔選擇合適的使用。
- 用 String 類創(chuàng)建的字符串是不可變的(
constant
)源武。
回到最開始的問題扼褪,又有一個(gè)新的問題:String是怎么判斷兩個(gè)字符串引用地址的呢?
在String的源碼中提到了字符串緩沖池(A pool of strings
):
A pool of strings, initially empty, is maintained privately by the class String.
...
if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.
這個(gè)緩沖池是由 String 類維護(hù)的软能,當(dāng)緩沖池中已經(jīng)存在一個(gè)和傳入字符串相同的字符串(通過調(diào)用 equals() 方法來確定是否相同)迎捺,那么就返回緩沖池中的字符串。否則查排,就把傳入的新字符串添加到緩沖池中并返回這個(gè)字符串的引用凳枝。
結(jié)合之前提到的使用構(gòu)造函數(shù)來創(chuàng)建新字符串的方式,這種方式新創(chuàng)建的字符串是傳入字符串的副本跋核,現(xiàn)在對(duì)這句話進(jìn)行解釋岖瑰。看下面的例子:
String str1 = "Hello";
String str2 = new String("Hello");
if (str1 == str2) {
System.out.println("str1和str2的地址相同");
} else {
System.out.println("str1和str2的地址不同");
}
輸出結(jié)果為:
str1 和 str2 的地址不同
- str1 創(chuàng)建的字符串被加入到字符串緩沖池中去砂代, str1 指向緩沖池中的 "Hello" 蹋订;
- str2 調(diào)用構(gòu)造函數(shù)傳入 "Hello" ,實(shí)際上進(jìn)行了兩步:
- 先去緩沖池中找有沒有相同的字符串(通過調(diào)用 equals() 方法來確定是否相同)刻伊;
- 發(fā)現(xiàn)緩沖池中有相同的字符串露戒,那就不需要新創(chuàng)建一遍了;而如果沒有捶箱,就新創(chuàng)建一個(gè)字符串智什。這里的情況顯然是第一種。但是由于 str2 中調(diào)用了構(gòu)造函數(shù)丁屎,因此要在內(nèi)存的堆空間上新建一個(gè)對(duì)象荠锭,而這個(gè) str2 正是指向內(nèi)存堆上的一個(gè)地址空間。
因此 str1 和 str2 通過 ==
判斷到的地址空間是不同的晨川。
所有類中的 equals() 方法都是繼承自或重寫 Object 類中 equals() 方法的证九。Object類提供的 equals() 方法如下:
public boolean equals(Object obj) {
return (this == obj);
}
可見最原始的 equals() 方法其實(shí)就是調(diào)用的==
删豺。對(duì)于任何非空(non-null
)的引用變量 x 和 y ,當(dāng)且僅當(dāng) x 和 y 指向同一個(gè)對(duì)象的時(shí)候愧怜,equals() 方法就返回 true 呀页。
重寫 equals() 的準(zhǔn)則,這個(gè)在 Object 類中有提到過:
- 自反性:
It is reflexive: x.equals(x) should return true;- 對(duì)稱性:
It is symmetric: x.equals(y) should return true if and only if y.equals(x) returns true;- 傳遞性:
It is transitive: if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.- 一致性:
It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.- 非空性:
x.equals(null) should return false.
那么問題來了叫搁,哪些情況下會(huì)違反對(duì)稱性和傳遞性赔桌?
- 違反對(duì)稱性
對(duì)稱性就是x.equals(y)時(shí),y也得equals x渴逻,很多時(shí)候疾党,我們自己覆寫equals時(shí),讓自己的類可以兼容等于一個(gè)已知類惨奕,比如下面的例子:
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
if (s == null)
throw new NullPointerException();
this.s = s;
}
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensiticeString)
return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
if (o instanceof String)
return s.equalsIgnoreCase((String) o);
return false;
}
}
這個(gè)想法很好雪位,想創(chuàng)建一個(gè)無(wú)視大小寫的String,并且還能夠兼容String作為參數(shù)梨撞,假設(shè)我們創(chuàng)建一個(gè)CaseInsensitiveString:
CaseInsensitiveString cis = new CaseInsensitiveString("Case");
那么肯定有 cis.equals("case")
,問題來了雹洗,"case".equals(cis)
嗎? String 并沒有兼容 CaseInsensiticeString 卧波,所以 String 的 equals() 也不接受 CaseInsensiticeString 作為參數(shù)时肿。
所以有個(gè)準(zhǔn)則,一般在覆寫 equals() 只兼容同類型的變量港粱。
- 違反傳遞性
傳遞性就是A等于B螃成,B等于C,那么A也應(yīng)該等于C查坪。
假設(shè)我們定義一個(gè)類Cat寸宏。
public class Cat(){
private int height;
private int weight;
public Cat(int h, int w)
{
this.height = h;
this.weight = w;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Cat))
return false;
Cat c = (Cat) o;
return c.height == height && c.weight == weight;
}
}
名人有言,不管黑貓白貓抓住老鼠就是好貓偿曙,我們又定義一個(gè)類ColorCat:
public class ColorCat extends Cat{
private String color;
public ColorCat(int h, int w, String color)
{
super(h, w);
this.color = color;
}
我們?cè)趯?shí)現(xiàn) equals 方法時(shí)氮凝,可以加上顏色比較,但是加上顏色就不兼容和普通貓作對(duì)比了望忆,這里我們忘記上面要求只兼容同類型變量的建議罩阵,定義一個(gè)兼容普通貓的 equals 方法,在“混合比較”時(shí)忽略顏色启摄。
@Override
public boolean equals(Object o) {
if (! (o instanceof Cat))
return false; //不是Cat或者ColorCat永脓,直接false
if (! (o instanceof ColorCat))
return o.equals(this);//不是彩貓,那一定是普通貓鞋仍,忽略顏色對(duì)比
return super.equals(o)&&((ColorCat)o).color.equals(color); //這時(shí)候才比較顏色
}
假設(shè)我們定義了貓:
ColorCat whiteCat = new ColorCat(1,2,"white");
Cat cat = new Cat(1,2);
ColorCat blackCat = new ColorCat(1,2,"black");
此時(shí)有whiteCat
等于cat
,cat
等于blackCat
搅吁,但是whiteCat
不等于blackCat
威创,所以不滿足傳遞性要求落午。
源碼注釋中還提到,無(wú)論何時(shí)當(dāng) equals() 方法被重寫的時(shí)候肚豺,都有必要去重寫一下 hashCode() 方法以便維持 hashCode() 方法的通用契約(general contract
)溃斋,這個(gè)契約就是相同的對(duì)象必須具有相同的哈希值。
類庫(kù)提供的 equals() 方法吸申,如果已經(jīng)重寫的話梗劫,那么比較的也許就不止是地址空間了,這就看具體類庫(kù)是怎么實(shí)現(xiàn)的了截碴。以 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;
}
- 首先會(huì)判斷當(dāng)前對(duì)象和傳入對(duì)象是否指向相同的地址空間,如果是就直接返回 true 日丹;
- 如果指向的地址空間不同走哺,檢測(cè)傳入對(duì)象是否為 String 類的實(shí)例,如果是就比較兩個(gè)對(duì)象中值是否相同哲虾,如果相同丙躏,返回 true ;否則返回 false 束凑;
- 如果傳入對(duì)象不是 String 類的實(shí)例晒旅,返回 false 。
Example
String str1 = "Hello";
String str2 = new String("Hello");
if (str2.equals(str1)) {
System.out.println("str1 equals to str2");
} else {
System.out.println("str1 doesn't equals to str2");
}
輸出結(jié)果為:
str1 equals to str2
原因顯而易見汪诉。