前言:
本來以為是個錯誤使用的問題掀抹,稍微那么深究一下型凳,發(fā)現(xiàn)腦海中,關(guān)于這個部分的知識庫存已經(jīng)告急了,可不能啊币厕。
removeAll() 失效重現(xiàn)
今天做一個批量刪除的功能列另,我使用了 List.removeAll()這個方法,但是該代碼執(zhí)行前后旦装,被操作的列表的 size 并沒由發(fā)生改變页衙。
排查了一下,是因?yàn)閮蓚€列表中存儲對象不同的原因阴绢。
為了更加清楚的理解店乐,我寫了簡單的小例子,浮現(xiàn)了錯誤的場景:
實(shí)體類:
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;
}
}
構(gòu)建場景:
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());
輸出結(jié)果是:
allStudents.size()------before-------------->10
remove result : false
allStudents.size()-------after-------------->10
但是呻袭,換 String 對象執(zhí)行 removeAll() 竟然可以成功眨八!
因?yàn)椴僮鲗ο蟛煌@是一個很簡單的原因左电,但是接下來要實(shí)驗(yàn)的另一個小例子廉侧,絕對讓你非常吃驚,我們講Bean 替換成 String 字符串試一下篓足。
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());
輸出結(jié)果是 :
allStudents.size()------before-------------->10
remove result : true
allStudents.size()-------after-------------->5
揭開這一切的面紗
從打印結(jié)果很明白的看到伏穆,removeAll() 成功執(zhí)行。String也是對象纷纫,為什么會這樣枕扫?代碼不會說謊,我們?nèi)ピ创a中去尋找答案辱魁。
從源碼中發(fā)現(xiàn)烟瞧,ArrayList 執(zhí)行 removeAll() 方法流程如下圖所示:
通過控制變量法分析,很容易就聚焦到 equals()
這個方法染簇,這個方法是 Object 的方法,默認(rèn)實(shí)現(xiàn)是比較對象在內(nèi)存的地址参滴。
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;
}
這樣的話青灼,引發(fā)了一個思考暴心,也就是說,如果自定義對象杂拨,重寫 equals() 中的實(shí)現(xiàn)专普,也是可以實(shí)現(xiàn)非相同對象的情況下,成功 removeAll()的弹沽。這里我用 上面例子的 Bean 實(shí)體類簡單實(shí)驗(yàn)一下:
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;
}
}
再次執(zhí)行第一個例子的程序檀夹,ArrayList<Bean> 成功 removeAll筋粗,打印信息如下:
allStudents.size()------before-------------->10
remove result : true
allStudents.size()-------after-------------->5
重寫 equals() 方法一定要符合規(guī)范!
但是這里我們要特別注意的是炸渡,當(dāng)我們重寫 equals() 方法的時候娜亿,一定要遵守它的規(guī)范,否則在程序使用中,使用錯誤實(shí)現(xiàn) equals() 的類時可能出現(xiàn)無法預(yù)料的問題蚌堵,而且很有可能暇唾,找了很久都定位不到問題在哪!辰斋,想想都后背發(fā)冷策州。
以下是 Object 中 equals()的方法說明注釋,絕對原汁原味宫仗!但是我相信肯定有人看了一遍還是不知道說的啥够挂,非常正常,我看兩遍也是藕夫,英語渣沒辦法孽糖,只能花更多時間去理解它,因?yàn)檎娴恼嬷匾?/p>
/**
* 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 方法實(shí)現(xiàn)的時候必須要滿足的特性:
1.(reflexive)自反性:
對于任何非 null 的引用值 x,x.equals(x) 必須為 true毅贮;
2.(symmetric)對稱性:
對于任何非 null 的引用值 x,y,當(dāng)且僅當(dāng) 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() 方法沒有修改的前提下瑰煎,多次調(diào)用 x.equals(y) 的返回結(jié)果一定是相同的铺然。
5.(non-nullity)非空性
對于任何非 null 的引用值 x,x.equals(null) 必須返回 false。
這些特性約束我們重寫 equals()的時候酒甸,寫條件判斷一定要謹(jǐn)慎魄健,下面是提供的一個模版:
@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 方法表現(xiàn)的也很好,調(diào)用 List.removeAll 也能成功執(zhí)行卤橄÷搪看似是沒有什么問題,但是當(dāng)我們試圖使用 hashMap 做存取操作的時候窟扑,就會出現(xiàn)問題喇颁。
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));
輸出結(jié)果:
Bean 中不正確覆蓋 hashCode(),取不到值:
allStudents.get(bean)----------------------->null
Bean 中正確覆蓋 hashCode()嚎货,能取到值:
allStudents.get(bean)----------------------->i :1
原因在于橘霎,HashMap 執(zhí)行 get() 操作的時候是通過散列碼,也就是對象的 HashCode 來搜索數(shù)據(jù)的殖属。所以讯蒲,當(dāng)不重寫 hashCode() 方法或者重寫的不規(guī)范的時候固耘,就會出現(xiàn)這樣的問題。
使用散列碼判斷對象的,有 HashMap 隙袁,HashSet,HashTable 等,因此婚脱,覆蓋 equals() 時雹仿,一定要同時覆蓋 hashCode()。
快速生成euqals() and hashCode()玄组!
看到上面的解釋滔驾,估計(jì)大家都感覺覆蓋這倆方法的時候都有點(diǎn)害怕了,但是告訴大家一個好消息俄讹,Android Studio 有這兩個方法的快速生成模版哆致,使用方法是 右鍵->generate->euqals() and hashCode(),也可以直接使用快捷鍵 command+N ->euqals() and hashCode()。
最后
到這里患膛,我想到摊阀,一個在面試的時候,經(jīng)常被問到的 java 基礎(chǔ)題:
java 中 == 和 equals 和 hashCode 的區(qū)別?
我想現(xiàn)在踪蹬,如果再被問到這個問題驹溃,我肯定可以比之前回答的要好一點(diǎn)了。
java 中有兩種類型延曙,值類型和引用類型豌鹤。其中,==
值類型和引用類型都可以運(yùn)算枝缔,equals 和 hashCode 是引用類型特有的方法布疙。
對于值類型,==
比較的是它們的值愿卸,對于引用類型灵临,==
比較的是兩者在內(nèi)存中的物理地址。
equals() 是 Object 類中的方法趴荸,默認(rèn)實(shí)現(xiàn)是使用 ==
比較兩個對象的地址儒溉,但是在一些子類,例如 String发钝,F(xiàn)loat,Integer 等顿涣,它們對 equals進(jìn)行覆蓋重寫波闹,就不再是比較兩個對象的地址了。
hashCode() 也是 Object 類的一個方法涛碑。返回一個離散的 int 型整數(shù)精堕。在集合類操作中使用,為了提高查詢速度蒲障。
當(dāng)覆蓋 equals() 的時候歹篓,一定要同時覆蓋 hashCode(),保證 x.equals(y) 返回為 true 的時候揉阎,x,y 要有相同的 HashCode 庄撮。
回答完畢。
參考資料
Effective Java 中文版第二版
最后
剛剛開通了個人微信公眾號毙籽,最新的博客洞斯,好玩的事情,都會在上面分享惧财,歡迎關(guān)注^ o ^巡扇。