前言:
本來以為是個錯誤使用的問題,稍微那麼深究一下,發現腦海中,關于這個部分的知識庫存已經告急了,可不能啊。
removeAll() 失效重制
今天做一個批量删除的功能,我使用了 List.removeAll()這個方法,但是該代碼執行前後,被操作的清單的 size 并沒由發生改變。
排查了一下,是因為兩個清單中存儲對象不同的原因。
為了更加清楚的了解,我寫了簡單的小例子,複現了錯誤的場景:
實體類:
public class Bean {
private int id;
private String name;
private String address;
public Bean(int id, String name, String address) {
this.id = id;
this.name = name;
this.address = address;
}
}
建構場景:
ArrayList<Bean> allStudents = new ArrayList<>();
ArrayList<Bean> boyStudents = new ArrayList<>();
for (int i = 0; i < 10 ; i++) {
Bean bean = new Bean(i,"name is "+i,"address is "+i);
allStudents.add(bean);
}
for (int i = 0; i < 5 ; i++) {
Bean bean = new Bean(i,"name is "+i,"address is "+i);
boyStudents.add(bean);
}
System.out.println("allStudents.size()------before-------------->"+allStudents.size());
System.out.println("remove result : "+allStudents.removeAll(boyStudents));
System.out.println("allStudents.size()-------after-------------->"+allStudents.size());
輸出結果是:
allStudents.size()------before-------------->10
remove result : false
allStudents.size()-------after-------------->10
但是,換 String 對象執行 removeAll() 竟然可以成功!
因為操作對象不同,這是一個很簡單的原因,但是接下來要實驗的另一個小例子,絕對讓你非常吃驚,我們講Bean 替換成 String 字元串試一下。
ArrayList<String> allStudents = new ArrayList<>();
ArrayList<String> boyStudents = new ArrayList<>();
for (int i = 0; i < 10 ; i++) {
String bean = "name is "+i+"address is "+i;
allStudents.add(bean);
}
for (int i = 0; i < 5 ; i++) {
String bean = "name is "+i+"address is "+i;
boyStudents.add(bean);
}
System.out.println("allStudents.size()------before-------------->"+allStudents.size());
System.out.println("remove result : "+allStudents.removeAll(boyStudents));
System.out.println("allStudents.size()-------after-------------->"+allStudents.size());
輸出結果是 :
allStudents.size()------before-------------->10
remove result : true
allStudents.size()-------after-------------->5
揭開這一切的面紗
從列印結果很明白的看到,removeAll() 成功執行。String也是對象,為什麼會這樣?代碼不會說謊,我們去源碼中去尋找答案。
從源碼中發現,ArrayList 執行 removeAll() 方法流程如下圖所示:
通過控制變量法分析,很容易就聚焦到
equals()
這個方法,這個方法是 Object 的方法,預設實作是比較對象在記憶體的位址。
public boolean equals(Object obj) {
return (this == obj);
}
再看一下 String 中 equals() 方法,重寫了 Object 的這個方法,不再是比較位址,而是比較字元串是否相同。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = count;
if (n == anotherString.count) {
int i = 0;
while (n-- != 0) {
if (charAt(i) != anotherString.charAt(i))
return false;
i++;
}
return true;
}
}
return false;
}
這樣的話,引發了一個思考,也就是說,如果自定義對象,重寫 equals() 中的實作,也是可以實作非相同對象的情況下,成功 removeAll()的。這裡我用 上面例子的 Bean 實體類簡單實驗一下:
public class Bean {
private int id;
private String name;
private String address;
public Bean(int id, String name, String address) {
this.id = id;
this.name = name;
this.address = address;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Bean bean = (Bean) o;
if (id != bean.id) return false;
if (!name.equals(bean.name)) return false;
return address.equals(bean.address);
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + name.hashCode();
result = 31 * result + address.hashCode();
return result;
}
}
再次執行第一個例子的程式,ArrayList 成功 removeAll,列印資訊如下:
allStudents.size()------before-------------->10
remove result : true
allStudents.size()-------after-------------->5
重寫 equals() 方法一定要符合規範!
但是這裡我們要特别注意的是,當我們重寫 equals() 方法的時候,一定要遵守它的規範,否則在程式使用中,使用錯誤實作 equals() 的類時可能出現無法預料的問題,而且很有可能,找了很久都定位不到問題在哪!,想想都後背發冷。
以下是 Object 中 equals()的方法說明注釋,絕對原汁原味!但是我相信肯定有人看了一遍還是不知道說的啥,非常正常,我看兩遍也是,英語渣沒辦法,隻能花更多時間去了解它,因為真的真重要。
/**
* Indicates whether some other object is "equal to" this one.
* <p>
* The {@code equals} method implements an equivalence relation
* on non-null object references:
* <ul>
* <li>It is <i>reflexive</i>: for any non-null reference value
* {@code x}, {@code x.equals(x)} should return
* {@code true}.
* <li>It is <i>symmetric</i>: for any non-null reference values
* {@code x} and {@code y}, {@code x.equals(y)}
* should return {@code true} if and only if
* {@code y.equals(x)} returns {@code true}.
* <li>It is <i>transitive</i>: for any non-null reference values
* {@code x}, {@code y}, and {@code z}, if
* {@code x.equals(y)} returns {@code true} and
* {@code y.equals(z)} returns {@code true}, then
* {@code x.equals(z)} should return {@code true}.
* <li>It is <i>consistent</i>: for any non-null reference values
* {@code x} and {@code y}, multiple invocations of
* {@code x.equals(y)} consistently return {@code true}
* or consistently return {@code false}, provided no
* information used in {@code equals} comparisons on the
* objects is modified.
* <li>For any non-null reference value {@code x},
* {@code x.equals(null)} should return {@code false}.
* </ul>
* <p>
* The {@code equals} method for class {@code Object} implements
* the most discriminating possible equivalence relation on objects;
* that is, for any non-null reference values {@code x} and
* {@code y}, this method returns {@code true} if and only
* if {@code x} and {@code y} refer to the same object
* ({@code x == y} has the value {@code true}).
* <p>
* Note that it is generally necessary to override the {@code hashCode}
* method whenever this method is overridden, so as to maintain the
* general contract for the {@code hashCode} method, which states
* that equal objects must have equal hash codes.
*
* @param obj the reference object with which to compare.
* @return {@code true} if this object is the same as the obj
* argument; {@code false} otherwise.
* @see #hashCode()
* @see java.util.HashMap
*/
public boolean equals(Object obj) {
return (this == obj);
}
equals 方法實作的時候必須要滿足的特性:
1.(reflexive)自反性:
對于任何非 null 的引用值 x,x.equals(x) 必須為 true;
2.(symmetric)對稱性:
對于任何非 null 的引用值 x,y,當且僅當 y.equals(x) 傳回 true 時,x.equals(y) 也要傳回 true 。
3.(transitive)傳遞性:
對于任何非 null 的引用值 x,y,z, 如果 x.equals(y) 傳回 true,y.equals(z) 傳回 true,那麼 x.equals(z) 一定要傳回 true。
4.(consistent)一緻性:
對于任何非 null 的引用值 x,y,隻要 equals() 方法沒有修改的前提下,多次調用 x.equals(y) 的傳回結果一定是相同的。
5.(non-nullity)非空性
對于任何非 null 的引用值 x,x.equals(null) 必須傳回 false。
這些特性限制我們重寫 equals()的時候,寫條件判斷一定要謹慎,下面是提供的一個模版:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
(MyClass) myclass = (MyClass) o;
if (this.xx != bean.myclass.xx) return false;
return myclass.equals(myclass.xx);
}
重要!覆寫 equals 時,一定要同時覆寫 hashCode
這樣做的目的是保證每一個 equals()傳回 true 的兩個對像,要有兩個相同的 hashCode 。
在上面示範的例子中,不覆寫 hashCode ,equals 方法表現的也很好,調用 List.removeAll 也能成功執行。看似是沒有什麼問題,但是當我們試圖使用 hashMap 做存取操作的時候,就會出現問題。
HashMap<Bean,String> allStudents = new HashMap<>();
for (int i = 0; i < 10 ; i++) {
Bean bean = new Bean(i,"name is "+i,"address is "+i);
allStudents.put(bean,"i :"+i);
}
Bean bean = new Bean(1,"name is 1","address is 1");
System.out.println(" allStudents.get(bean)----------------------->"+ allStudents.get(bean));
輸出結果:
Bean 中不正确覆寫 hashCode(),取不到值:
allStudents.get(bean)----------------------->null
Bean 中正确覆寫 hashCode(),能取到值:
allStudents.get(bean)----------------------->i :1
原因在于,HashMap 執行 get() 操作的時候是通過散列碼,也就是對象的 HashCode 來搜尋資料的。是以,當不重寫 hashCode() 方法或者重寫的不規範的時候,就會出現這樣的問題。
使用散列碼判斷對象的,有 HashMap ,HashSet,HashTable 等,是以,覆寫 equals() 時,一定要同時覆寫 hashCode()。
快速生成euqals() and hashCode()!
看到上面的解釋,估計大家都感覺覆寫這倆方法的時候都有點害怕了,但是告訴大家一個好消息,Android Studio 有這兩個方法的快速生成模版,使用方法是 右鍵->generate->euqals() and hashCode(),也可以直接使用快捷鍵 command+N ->euqals() and hashCode()。
最後
到這裡,我想到,一個在面試的時候,經常被問到的 java 基礎題:
java 中 == 和 equals 和 hashCode 的差別?
我想現在,如果再被問到這個問題,我肯定可以比之前回答的要好一點了。
java 中有兩種類型,值類型和引用類型。其中,
==
值類型和引用類型都可以運算,equals 和 hashCode 是引用類型特有的方法。
對于值類型,
==
比較的是它們的值,對于引用類型,
==
比較的是兩者在記憶體中的實體位址。
equals() 是 Object 類中的方法,預設實作是使用
==
比較兩個對象的位址,但是在一些子類,例如 String,Float,Integer 等,它們對 equals進行覆寫重寫,就不再是比較兩個對象的位址了。
hashCode() 也是 Object 類的一個方法。傳回一個離散的 int 型整數。在集合類操作中使用,為了提高查詢速度。
當覆寫 equals() 的時候,一定要同時覆寫 hashCode(),保證 x.equals(y) 傳回為 true 的時候,x,y 要有相同的 HashCode 。
回答完畢。
參考資料
Effective Java 中文版第二版
歡迎關注個人微信公衆号「淺淺同學的開發筆記」,最新的部落格,好玩的事情,都會在上面分享,期待與你共同成長。