天天看點

一個由List.removeAll()失效引發的思考前言: removeAll() 失效重制 但是,換 String 對象執行 removeAll() 竟然可以成功!揭開這一切的面紗重寫 equals() 方法一定要符合規範!快速生成euqals() and hashCode()!最後 參考資料

前言:

本來以為是個錯誤使用的問題,稍微那麼深究一下,發現腦海中,關于這個部分的知識庫存已經告急了,可不能啊。

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() 方法流程如下圖所示:

一個由List.removeAll()失效引發的思考前言: removeAll() 失效重制 但是,換 String 對象執行 removeAll() 竟然可以成功!揭開這一切的面紗重寫 equals() 方法一定要符合規範!快速生成euqals() and hashCode()!最後 參考資料

通過控制變量法分析,很容易就聚焦到

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()。

一個由List.removeAll()失效引發的思考前言: removeAll() 失效重制 但是,換 String 對象執行 removeAll() 竟然可以成功!揭開這一切的面紗重寫 equals() 方法一定要符合規範!快速生成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 中文版第二版

歡迎關注個人微信公衆号「淺淺同學的開發筆記」,最新的部落格,好玩的事情,都會在上面分享,期待與你共同成長。

一個由List.removeAll()失效引發的思考前言: removeAll() 失效重制 但是,換 String 對象執行 removeAll() 竟然可以成功!揭開這一切的面紗重寫 equals() 方法一定要符合規範!快速生成euqals() and hashCode()!最後 參考資料