簡體   English   中英

為什么我需要覆蓋 Java 中的 equals 和 hashCode 方法?

[英]Why do I need to override the equals and hashCode methods in Java?

最近我通讀了這個Developer Works 文檔

該文檔是關於有效且正確地定義hashCode()equals() ,但是我無法弄清楚為什么我們需要覆蓋這兩個方法。

我如何才能決定有效地實施這些方法?

Joshua Bloch 談到 Effective Java

您必須在每個覆蓋 equals() 的類中覆蓋 hashCode()。 不這樣做將導致違反 Object.hashCode() 的一般契約,這將阻止您的類與所有基於哈希的集合(包括 HashMap、HashSet 和 Hashtable)一起正常運行。

讓我們嘗試通過一個例子來理解它,如果我們覆蓋equals()而不覆蓋hashCode()並嘗試使用Map會發生什么。

假設我們有一個這樣的類,如果MyClass兩個對象的importantField相等,則它們相等(使用 eclipse 生成的hashCode()equals()

public class MyClass {
    private final String importantField;
    private final String anotherField;

    public MyClass(final String equalField, final String anotherField) {
        this.importantField = equalField;
        this.anotherField = anotherField;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result
                + ((importantField == null) ? 0 : importantField.hashCode());
        return result;
    }

    @Override
    public boolean equals(final Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final MyClass other = (MyClass) obj;
        if (importantField == null) {
            if (other.importantField != null)
                return false;
        } else if (!importantField.equals(other.importantField))
            return false;
        return true;
    }
}

想象一下你有這個

MyClass first = new MyClass("a","first");
MyClass second = new MyClass("a","second");

覆蓋僅equals

如果只覆蓋equals ,那么當您myMap.put(first,someValue)調用myMap.put(first,someValue)將散列到某個桶,當您調用myMap.put(second,someOtherValue) ,它將散列到另一個桶(因為它們具有不同的hashCode )。 因此,盡管它們相等,但由於它們沒有散列到同一個桶中,因此地圖無法意識到這一點,並且它們都留在地圖中。


雖然如果我們覆蓋hashCode()則沒有必要覆蓋equals() ,讓我們看看在這種特殊情況下會發生什么,我們知道MyClass兩個對象如果它們的importantField相等,但我們沒有覆蓋equals()

僅覆蓋hashCode

如果您只覆蓋hashCode那么當您調用myMap.put(first,someValue) ,它將首先計算其hashCode並將其存儲在給定的存儲桶中。 然后,當您調用myMap.put(second,someOtherValue)它應該根據地圖文檔將 first 替換為 second,因為它們是相等的(根據業務需求)。

但問題是 equals 沒有被重新定義,所以當映射散列second並遍歷存儲桶尋找是否有對象k使得second.equals(k)為真時,它不會找到任何作為second.equals(first)將是false

希望很清楚

諸如HashMapHashSet集合使用對象的哈希碼值來確定它應該如何存儲在集合中,並且再次使用哈希碼以便在其集合中定位該對象。

哈希檢索是一個兩步過程:

  1. 找到正確的存儲桶(使用hashCode()
  2. 在桶中搜索正確的元素(使用equals()

這是一個關於為什么我們應該覆蓋equals()hashcode()的小例子。

考慮一個Employee類,它有兩個字段:年齡和姓名。

public class Employee {

    String name;
    int age;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this)
            return true;
        if (!(obj instanceof Employee))
            return false;
        Employee employee = (Employee) obj;
        return employee.getAge() == this.getAge()
                && employee.getName() == this.getName();
    }

    // commented    
    /*  @Override
        public int hashCode() {
            int result=17;
            result=31*result+age;
            result=31*result+(name!=null ? name.hashCode():0);
            return result;
        }
     */
}

現在創建一個類,將Employee對象插入HashSet並測試該對象是否存在。

public class ClientTest {
    public static void main(String[] args) {
        Employee employee = new Employee("rajeev", 24);
        Employee employee1 = new Employee("rajeev", 25);
        Employee employee2 = new Employee("rajeev", 24);

        HashSet<Employee> employees = new HashSet<Employee>();
        employees.add(employee);
        System.out.println(employees.contains(employee2));
        System.out.println("employee.hashCode():  " + employee.hashCode()
        + "  employee2.hashCode():" + employee2.hashCode());
    }
}

它將打印以下內容:

false
employee.hashCode():  321755204  employee2.hashCode():375890482

現在取消注釋hashcode()方法,執行相同的輸出將是:

true
employee.hashCode():  -938387308  employee2.hashCode():-938387308

現在你能明白為什么如果兩個對象被認為是相等的,它們的hashcode也必須相等嗎? 否則,您將永遠無法找到該對象,因為 Object 類中的默認hashcode方法實際上總是為每個對象提供一個唯一編號,即使equals()方法以兩個或多個對象的方式被覆蓋被認為是平等的。 如果對象的hashcode沒有反映出來,則對象的相等程度無關緊要。 再說一遍:如果兩個對象相等,則它們的hashcode 也必須相等。

您必須在每個覆蓋 equals() 的類中覆蓋 hashCode()。 不這樣做將導致違反 Object.hashCode() 的一般契約,這將阻止您的類與所有基於哈希的集合(包括 HashMap、HashSet 和 Hashtable)一起正常運行。


來自Effective Java ,作者 Joshua Bloch

通過一致地定義equals()hashCode() ,您可以提高類作為基於哈希的集合中的鍵的可用性。 正如 hashCode 的 API 文檔所解釋的那樣:“為了哈希表(例如java.util.Hashtable提供的哈希表)的好處,支持此方法。”

關於如何有效地實現這些方法的問題的最佳答案是建議您閱讀Effective Java 的第 3 章。

身份不是平等。

  • 等於運算符==測試身份。
  • equals(Object obj)方法比較相等性測試(即我們需要通過覆蓋該方法來告訴相等性)

為什么我需要覆蓋 Java 中的 equals 和 hashCode 方法?

首先我們要了解equals方法的使用。

為了識別兩個對象之間的差異,我們需要覆蓋 equals 方法。

例如:

Customer customer1=new Customer("peter");
Customer customer2=customer1;
customer1.equals(customer2); // returns true by JVM. i.e. both are refering same Object
------------------------------
Customer customer1=new Customer("peter");
Customer customer2=new Customer("peter");
customer1.equals(customer2); //return false by JVM i.e. we have two different peter customers.

------------------------------
Now I have overriden Customer class equals method as follows:
 @Override
    public boolean equals(Object obj) {
        if (this == obj)   // it checks references
            return true;
        if (obj == null) // checks null
            return false;
        if (getClass() != obj.getClass()) // both object are instances of same class or not
            return false;
        Customer other = (Customer) obj;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name)) // it again using bulit in String object equals to identify the difference 
            return false;
        return true; 
    }
Customer customer1=new Customer("peter");
Customer customer2=new Customer("peter");
Insteady identify the Object equality by JVM, we can do it by overring equals method.
customer1.equals(customer2);  // returns true by our own logic

現在 hashCode 方法可以很容易理解了。

hashCode 生成整數,以便將對象存儲在HashMapHashSet等數據結構中。

假設我們已經覆蓋了上面的Customer方法,

customer1.equals(customer2);  // returns true by our own logic

當我們將對象存儲在存儲桶中時處理數據結構時(存儲桶是文件夾的奇特名稱)。 如果我們使用內置哈希技術,對於以上兩個客戶,它會生成兩個不同的哈希碼。 所以我們在兩個不同的地方存儲相同的對象。 為避免此類問題,我們還應根據以下原則覆蓋 hashCode 方法。

  • 不相等的實例可能具有相同的哈希碼。
  • 相等的實例應該返回相同的哈希碼。

簡單地說,Object 中的 equals 方法檢查引用是否相等,因為當屬性相等時,類的兩個實例在語義上仍然是相等的。 例如,當將對象放入使用 equals 和 hashcode 的容器(如HashMapSet )時,這一點很重要。 假設我們有一個類:

public class Foo {
    String id;
    String whatevs;

    Foo(String id, String whatevs) {
        this.id = id;
        this.whatevs = whatevs;
    }
}

我們創建兩個具有相同id 的實例:

Foo a = new Foo("id", "something");
Foo b = new Foo("id", "something else");

在不覆蓋 equals 的情況下,我們得到:

  • a.equals(b) 是假的,因為它們是兩個不同的實例
  • a.equals(a) 為真,因為它是同一個實例
  • b.equals(b) 為真,因為它是同一個實例

正確的? 好吧,如果這是你想要的。 但是假設我們希望具有相同 id 的對象是同一個對象,無論它是否是兩個不同的實例。 我們覆蓋等號(和哈希碼):

public class Foo {
    String id;
    String whatevs;

    Foo(String id, String whatevs) {
        this.id = id;
        this.whatevs = whatevs;
    }

    @Override
    public boolean equals(Object other) {
        if (other instanceof Foo) {
            return ((Foo)other).id.equals(this.id);   
        }
    }

    @Override
    public int hashCode() {
        return this.id.hashCode();
    }
}

至於實現 equals 和 hashcode,我可以推薦使用Guava 的輔助方法

為什么我們重寫equals()方法

在 Java 中,我們不能重載 ==、+=、-+ 等運算符的行為方式。 他們以某種方式行事。 因此,讓我們在這里關注我們的案例中的運算符 ==。

運算符 == 的工作原理。

它檢查我們比較的 2 個引用是否指向內存中的同一個實例。 僅當這 2 個引用代表內存中的同一實例時,運算符==才會解析為 true。

所以現在讓我們考慮下面的例子

public class Person {

      private Integer age;
      private String name;
    
      ..getters, setters, constructors
      }

因此,假設在您的程序中,您在不同的地方構建了 2 個 Person 對象,並且您希望對它們進行比較。

Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1 == person2 );  --> will print false!

從業務角度來看,這兩個對象看起來一樣嗎? 對於 JVM,它們是不一樣的。 由於它們都是使用new關鍵字創建的,因此這些實例位於內存中的不同段中。 因此運算符 == 將返回false

但是如果我們不能覆蓋 == 操作符,我們怎么能告訴 JVM 我們希望這兩個對象被視為相同。 .equals()方法在起作用。

您可以覆蓋equals()以檢查某些對象是否具有相同的特定字段的值被視為相等。

您可以選擇要比較的字段。 如果我們說 2 Person 對象是相同的當且僅當它們具有相同的年齡和相同的名稱,那么 IDE 將創建類似以下內容以自動生成 equals()

@Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                name.equals(person.name);
    }

讓我們回到我們之前的例子

    Person person1 = new Person("Mike", 34);
    Person person2 = new Person("Mike", 34);
    System.out.println ( person1 == person2 );   --> will print false!
    System.out.println ( person1.equals(person2) );  --> will print true!

所以我們不能重載 == 運算符以我們想要的方式比較對象,但 Java 給了我們另一種方式, equals()方法,我們可以根據需要覆蓋它。

但是請記住,如果我們不在類中提供自定義版本的.equals() (又名覆蓋),那么來自 Object 類的預定義.equals()==運算符的行為將完全相同。

從 Object 繼承的默認equals()方法將檢查兩個比較的實例在內存中是否相同!

為什么我們重寫hashCode()方法

java中的一些數據結構如HashSet、HashMap基於應用於這些元素的散列函數來存儲它們的元素。 散列函數是hashCode()

如果我們可以選擇覆蓋.equals()方法,那么我們也必須選擇覆蓋hashCode()方法。 這是有原因的。

從 Object 繼承的hashCode()默認實現認為內存中的所有對象都是唯一的!

讓我們回到那些散列數據結構。 這些數據結構有一個規則。

HashSet 不能包含重復值,HashMap 不能包含重復鍵

HashSet 在幕后使用 HashMap 實現,其中 HashSet 的每個值都作為鍵存儲在 HashMap 中。

所以我們必須了解 HashMap 是如何工作的。

簡單來說,HashMap 是一個具有一些桶的本機數組。 每個桶都有一個鏈表。 在那個鏈表中,我們的密鑰被存儲。 HashMap 通過應用hashCode()方法為每個鍵定位正確的鏈表,然后它遍歷該鏈表的所有元素並對這些元素中的每一個應用equals()方法以檢查該元素是否已經包含在那里。 不允許重復鍵。

在此處輸入圖片說明

當我們在 HashMap 中放入一些東西時,鍵存儲在這些鏈表之一中。 該鍵將存儲在哪個鏈表中,由該鍵上的hashCode()方法的結果顯示。 因此,如果key1.hashCode()結果為 4,則該 key1 將存儲在數組的第 4 個存儲桶中,即存在於那里的鏈表中。

默認情況下, hashCode()方法為每個不同的實例返回不同的結果。 如果我們有默認的equals() ,它的行為類似於 == ,它將內存中的所有實例視為不同的對象,我們沒有任何問題。

但是在我們之前的示例中,我們說過如果他們的年齡和姓名匹配,我們希望 Person 實例被認為是平等的。

    Person person1 = new Person("Mike", 34);
    Person person2 = new Person("Mike", 34);
    System.out.println ( person1.equals(person2) );  --> will print true!

現在讓我們創建一個映射來存儲這些實例作為鍵,一些字符串作為對值

Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");

在 Person 類中,我們沒有覆蓋hashCode方法,但我們覆蓋了equals方法。 由於默認的hashCode為不同的 java 實例提供了不同的結果, person1.hashCode()person2.hashCode()有很大的機會得到不同的結果。

我們的地圖可能以不同鏈表中的那些人結束。

在此處輸入圖片說明

這違反了 HashMap 的邏輯

一個 HashMap 不允許有多個相等的鍵!

但是我們現在有了,原因是從對象類繼承的默認hashCode()是不夠的。 不是在我們覆蓋 Person 類的equals()方法之后。

這就是為什么我們必須在重寫equals方法后重寫hashCode()方法的原因。

現在讓我們解決這個問題。 讓我們重寫我們的hashCode()方法來考慮equals()考慮的相同字段,即age, name

 public class Person {

      private Integer age;
      private String name;
    
      ..getters, setters, constructors

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                name.equals(person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

      }

現在讓我們再次嘗試將這些鍵保存在我們的 HashMap 中

Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");

person1.hashCode()person2.hashCode()肯定是一樣的。 假設它是0。

HashMap 將轉到存儲桶 0,在該 LinkedList 中,將 person1 保存為值為“1”的鍵。 對於第二個 put HashMap 足夠智能,當它再次進入存儲桶 0 以保存值為“2”的 person2 鍵時,它將看到另一個相等的鍵已經存在於那里。 所以它會覆蓋以前的密鑰。 所以最終只有 person2 鍵存在於我們的 HashMap 中。

在此處輸入圖片說明

現在我們符合 Hash Map 的規則,即不允許多個相等的鍵!

hashCode()

如果你只覆蓋 hash-code 方法什么也不會發生,因為它總是為每個對象返回一個新的hashCode作為 Object 類。

equals()

如果您只覆蓋 equals 方法,如果a.equals(b)為真,則意味着 a 和 b 的hashCode必須相同,但由於您沒有覆蓋hashCode方法,因此不會發生這種情況。

注意: Object 類的hashCode()方法總是為每個對象返回一個新的hashCode

因此,當您需要在基於散列的集合中使用您的對象時,您必須同時覆蓋equals()hashCode()

讓我用非常簡單的詞來解釋這個概念。

首先從更廣泛的角度來看,我們有集合,而 hashmap 是集合中的數據結構之一。

要理解為什么我們必須覆蓋 equals 和 hashcode 方法,首先需要了解什么是 hashmap 和什么是。

哈希圖是一種以數組方式存儲數據的鍵值對的數據結構。 假設 a[],其中 'a' 中的每個元素都是一個鍵值對。

此外,上述數組中的每個索引都可以是鏈表,從而在一個索引處具有多個值。

現在為什么要使用哈希圖?

如果我們必須在一個大數組中搜索,那么搜索每個數組將不會有效,那么散列技術告訴我們,讓我們用一些邏輯預處理數組並根據該邏輯對元素進行分組,即散列

EG:我們有數組 1,2,3,4,5,6,7,8,9,10,11 並且我們應用了一個哈希函數 mod 10 所以 1,11 將被分組在一起。 因此,如果我們必須在前一個數組中搜索 11,那么我們將不得不迭代整個數組,但是當我們對其進行分組時,我們限制了迭代范圍,從而提高了速度。 為簡單起見,用於存儲所有上述信息的數據結構可以被認為是一個二維數組

現在除了上面的 hashmap 還告訴它不會在其中添加任何重復項。 這就是我們必須覆蓋 equals 和 hashcode 的主要原因

所以當它說解釋hashmap的內部工作時,我們需要找到hashmap有哪些方法以及它如何遵循我上面解釋的上述規則

所以 hashmap 有稱為 put(K,V) 的方法,並且根據 hashmap 它應該遵循有效分布數組的上述規則,並且不添加任何重復項

所以 put 的作用是首先為給定的鍵生成哈希碼來決定該值應該放在哪個索引中。那么新值應該添加到該索引的鏈表末尾之后。 但請記住,不應根據哈希圖的所需行為添加重復項。 所以假設你有兩個整數對象 aa=11,bb=11。

作為從對象類派生的每個對象,比較兩個對象的默認實現是比較對象內部的引用而不是值。 因此,在上述情況下,盡管語義上相等,但相等性測試將失敗,並且存在具有相同哈希碼和相同值的兩個對象從而創建重復項的可能性。 如果我們覆蓋,那么我們可以避免添加重復項。 您也可以參考 詳細工作

import java.util.HashMap;


public class Employee {
    String name;
    String mobile;

    public Employee(String name,String mobile) {
        this.name = name;
        this.mobile = mobile;
    }
    
    @Override
    public int hashCode() {
        System.out.println("calling hascode method of Employee");
        String str = this.name;
        int sum = 0;
        for (int i = 0; i < str.length(); i++) {
            sum = sum + str.charAt(i);
        }
        return sum;
    }

    @Override
    public boolean equals(Object obj) {
        // TODO Auto-generated method stub
        System.out.println("calling equals method of Employee");
        Employee emp = (Employee) obj;
        if (this.mobile.equalsIgnoreCase(emp.mobile)) {
            System.out.println("returning true");
            return true;
        } else {
            System.out.println("returning false");
            return false;
        }
    }

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

        Employee emp = new Employee("abc", "hhh");
        Employee emp2 = new Employee("abc", "hhh");
        HashMap<Employee, Employee> h = new HashMap<>();
        //for (int i = 0; i < 5; i++) {
            h.put(emp, emp);
            h.put(emp2, emp2);
        //}
        
        System.out.println("----------------");
        System.out.println("size of hashmap: "+h.size());
    }
}

Java 提出了一個規則,即

“如果兩個對象使用 Object class equals 方法相等,那么 hashcode 方法應該為這兩個對象提供相同的值。”

所以,如果在我們的類中我們覆蓋了equals()我們也應該覆蓋hashcode()方法來遵循這個規則。 例如,在Hashtable中使用equals()hashcode()兩種方法將值存儲為鍵值對。 如果我們覆蓋一個而不是另一個,如果我們使用這樣的對象作為鍵, Hashtable可能無法按我們想要的方式工作。

因為如果您不覆蓋它們,您將使用 Object 中的默認實現。

鑒於實例相等和 hascode 值通常需要了解組成對象的內容,它們通常需要在您的類中重新定義以具有任何有形的含義。

為了使用我們自己的類對象作為 HashMap、Hashtable 等集合中的鍵,我們應該通過了解集合的內部工作來覆蓋這兩個方法( hashCode() 和 equals() )。 否則,它會導致我們意想不到的錯誤結果。

添加到@Lombo 的答案

什么時候需要覆蓋 equals() ?

Object 的 equals() 的默認實現是

public boolean equals(Object obj) {
        return (this == obj);
}

這意味着兩個對象只有在它們具有相同的內存地址時才會被認為是相等的,只有當您將一個對象與其自身進行比較時才為真。

但是,如果兩個對象的一個​​或多個屬性具有相同的值,您可能希望將它們視為相同的對象(請參閱@Lombo 的回答中給出的示例)。

因此,在這些情況下,您將覆蓋equals()並給出自己的平等條件。

我已經成功實現了 equals() 並且效果很好。那么為什么他們還要求覆蓋 hashCode() 呢?

好吧。只要您不在用戶定義的類上使用基於“哈希”的集合,就可以了。 但是在未來的某個時候,您可能想要使用HashMapHashSet並且如果您不override“正確實現” hashCode() ,這些基於 Hash 的集合將無法按預期工作。

覆蓋僅等於(除了@Lombo 的答案)

myMap.put(first,someValue)
myMap.contains(second); --> But it should be the same since the key are the same.But returns false!!! How?

首先,HashMap 檢查second的 hashCode 是否與first相同。 只有當值相同時,它才會繼續檢查同一個桶中的相等性。

但是這里這兩個對象的 hashCode 是不同的(因為它們的內存地址與默認實現不同)。 因此它甚至不會關心檢查是否相等。

如果您在重寫的 equals() 方法中有一個斷點,如果它們具有不同的 hashCode,它就不會介入。 contains()檢查hashCode()並且只有當它們相同時才會調用您的equals()方法。

為什么我們不能讓 HashMap 檢查所有桶中的相等性? 所以我沒有必要覆蓋 hashCode() !!

那么你就錯過了基於哈希的集合的重點。 考慮以下 :

Your hashCode() implementation : intObject%9.

以下是以桶的形式存儲的密鑰。

Bucket 1 : 1,10,19,... (in thousands)
Bucket 2 : 2,20,29...
Bucket 3 : 3,21,30,...
...

假設,您想知道地圖是否包含鍵 10。您想搜索所有桶嗎? 或者您只想搜索一個存儲桶?

根據 hashCode,您會發現如果 10 存在,則它必須存在於 Bucket 1 中。因此只會搜索 Bucket 1 !!

它在使用值對象時很有用。 以下是Portland Pattern Repository的摘錄:

值對象的示例是數字、日期、貨幣和字符串等。 通常,它們是使用相當廣泛的小物體。 他們的身份基於他們的狀態而不是他們的對象身份。 這樣,您可以擁有同一個概念值對象的多個副本。

因此,我可以擁有代表 1998 年 1 月 16 日的對象的多個副本。這些副本中的任何一個都將彼此相等。 對於像這樣的小對象,通常更容易創建新對象並移動它們,而不是依賴單個對象來表示日期。

值對象應始終覆蓋 Java 中的 .equals()(或 Smalltalk 中的 =)。 (記住也要覆蓋 .hashCode() 。)

class A {
    int i;
    // Hashing Algorithm
    if even number return 0 else return 1
    // Equals Algorithm,
    if i = this.i return true else false
}
  • put('key','value') 將使用hashCode()計算哈希值以確定存儲桶並使用equals()方法查找該值是否已存在於存儲桶中。 如果不是它會添加否則它將被替換為當前值
  • get('key') 將首先使用hashCode()查找 Entry (bucket) 並使用equals()查找 Entry 中的值

如果兩者都被覆蓋,

地圖<A>

Map.Entry 1 --> 1,3,5,...
Map.Entry 2 --> 2,4,6,...

如果 equals 沒有被覆蓋

地圖<A>

Map.Entry 1 --> 1,3,5,...,1,3,5,... // Duplicate values as equals not overridden
Map.Entry 2 --> 2,4,6,...,2,4,..

如果 hashCode 未被覆蓋

地圖<A>

Map.Entry 1 --> 1
Map.Entry 2 --> 2
Map.Entry 3 --> 3
Map.Entry 4 --> 1
Map.Entry 5 --> 2
Map.Entry 6 --> 3 // Same values are Stored in different hasCodes violates Contract 1
So on...

HashCode 等價合約

  1. 根據equal方法相等的兩個鍵應該生成相同的hashCode
  2. 生成相同 hashCode 的兩個 Key 不必相等(在上面的例子中所有偶數生成相同的 hash Code)

1)常見錯誤如下例所示。

public class Car {

    private String color;

    public Car(String color) {
        this.color = color;
    }

    public boolean equals(Object obj) {
        if(obj==null) return false;
        if (!(obj instanceof Car))
            return false;   
        if (obj == this)
            return true;
        return this.color.equals(((Car) obj).color);
    }

    public static void main(String[] args) {
        Car a1 = new Car("green");
        Car a2 = new Car("red");

        //hashMap stores Car type and its quantity
        HashMap<Car, Integer> m = new HashMap<Car, Integer>();
        m.put(a1, 10);
        m.put(a2, 20);
        System.out.println(m.get(new Car("green")));
    }
}

未找到綠色汽車

2. hashCode()引起的問題

該問題是由未覆蓋的方法hashCode() equals()hashCode()之間的契約是:

  1. 如果兩個對象相等,則它們必須具有相同的哈希碼。
  2. 如果兩個對象具有相同的哈希碼,則它們可能相等,也可能不相等。

     public int hashCode(){ return this.color.hashCode(); }

假設您有類 (A) 聚合了另外兩個 (B) (C),並且您需要將 (A) 的實例存儲在哈希表中。 默認實現只允許區分實例,而不是通過 (B) 和 (C)。 所以 A 的兩個實例可能相等,但默認情況下不允許您以正確的方式比較它們。

考慮在桶中收集所有黑色的球。 你的工作是為這些球着色如下,並將其用於適當的游戲,

網球 - 黃色、紅色。 板球 - 白色

現在桶有黃、紅、白三種顏色的球。 現在你做了着色只有你知道哪種顏色適合哪個游戲。

給球上色 - 散列。 選擇比賽用球 - 等於。

如果您進行了着色並且有人為板球或網球選擇了球,他們不會介意顏色!!!

我正在研究解釋“如果你只覆蓋 hashCode 那么當你調用myMap.put(first,someValue)它首先計算它的 hashCode 並將其存儲在給定的存儲桶中。然后當你調用myMap.put(first,someOtherValue)它應該根據地圖文檔將第一個替換為第二個,因為它們是相等的(根據我們的定義)。”

我認為第二次添加myMap它應該是“第二個”對象,如myMap.put(second,someOtherValue)

方法equals 和hashcode 在對象類中定義。 默認情況下,如果 equals 方法返回 true,則系統將進一步檢查哈希碼的值。 如果兩個對象的哈希碼也相同,則僅將對象視為相同。 因此,如果您只覆蓋 equals 方法,那么即使覆蓋的 equals 方法指示 2 個對象相等,系統定義的哈希碼也可能不指示這 2 個對象相等。 所以我們也需要覆蓋哈希碼。

Java 中的 Equals 和 Hashcode 方法

它們是 java.lang.Object 類的方法,它是所有類(自定義類以及在 java API 中定義的其他類)的超類。

執行:

公共布爾等於(對象 obj)

公共 int hashCode()

在此處輸入圖片說明

公共布爾等於(對象 obj)

此方法僅檢查兩個對象引用 x 和 y 是否引用同一個對象。 即它檢查是否 x == y。

它是自反的:對於任何參考值 x,x.equals(x) 應該返回 true。

它是對稱的:對於任何參考值 x 和 y,當且僅當 y.equals(x) 返回 true 時,x.equals(y) 應該返回 true。

它是可傳遞的:對於任何參考值 x、y 和 z,如果 x.equals(y) 返回 true 並且 y.equals(z) 返回 true,那么 x.equals(z) 應該返回 true。

它是一致的:對於任何引用值 x 和 y,x.equals(y) 的多次調用始終返回 true 或始終返回 false,前提是沒有修改對象的 equals 比較中使用的信息。

對於任何非空引用值 x,x.equals(null) 應返回 false。

公共 int hashCode()

此方法返回調用此方法的對象的哈希碼值。 此方法以整數形式返回哈希碼值,並且支持基於哈希的集合類,例如 Hashtable、HashMap、HashSet 等。必須在每個覆蓋 equals 方法的類中覆蓋此方法。

hashCode 的總合約為:

每當在 Java 應用程序執行期間在同一對象上多次調用它時,hashCode 方法必須始終返回相同的整數,前提是對象上的 equals 比較中使用的信息沒有被修改。

該整數不需要從應用程序的一次執行到同一應用程序的另一次執行保持一致。

如果根據 equals(Object) 方法兩個對象相等,則對兩個對象中的每一個調用 hashCode 方法必須產生相同的整數結果。

如果根據 equals(java.lang.Object) 方法兩個對象不相等,則不需要對兩個對象中的每一個調用 hashCode 方法必須產生不同的整數結果。 但是,程序員應該意識到為不相等的對象生成不同的整數結果可能會提高哈希表的性能。

只要它們相等,相等的對象就必須產生相同的散列碼,但不相等的對象不需要產生不同的散列碼。

資源:

Java牧場

圖片

如果您覆蓋equals()而不是hashcode() ,則不會發現任何問題,除非您或其他人在HashSet等散列集合中使用該類類型。 在我之前的人已經多次清楚地解釋了文檔化的理論,我在這里只是提供一個非常簡單的例子。

考慮一個類,它的equals()需要表示一些定制的東西:-

    public class Rishav {

        private String rshv;

        public Rishav(String rshv) {
            this.rshv = rshv;
        }

        /**
        * @return the rshv
        */
        public String getRshv() {
            return rshv;
        }

        /**
        * @param rshv the rshv to set
        */
        public void setRshv(String rshv) {
            this.rshv = rshv;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof Rishav) {
                obj = (Rishav) obj;
                if (this.rshv.equals(((Rishav) obj).getRshv())) {
                    return true;
                } else {
                    return false;
                }
            } else {
                return false;
            }
        }

        @Override
        public int hashCode() {
            return rshv.hashCode();
        }

    }

現在考慮這個主類:-

    import java.util.HashSet;
    import java.util.Set;

    public class TestRishav {

        public static void main(String[] args) {
            Rishav rA = new Rishav("rishav");
            Rishav rB = new Rishav("rishav");
            System.out.println(rA.equals(rB));
            System.out.println("-----------------------------------");

            Set<Rishav> hashed = new HashSet<>();
            hashed.add(rA);
            System.out.println(hashed.contains(rB));
            System.out.println("-----------------------------------");

            hashed.add(rB);
            System.out.println(hashed.size());
        }

    }

這將產生以下輸出:-

    true
    -----------------------------------
    true
    -----------------------------------
    1

我對結果很滿意。 但是,如果我沒有覆蓋hashCode() ,它會導致噩夢,因為具有相同成員內容的Rishav對象將不再被視為唯一的,因為hashCode將不同,這是默認行為生成的,這是輸出:-

    true
    -----------------------------------
    false
    -----------------------------------
    2

hashCode()方法用於獲取給定對象的唯一整數。 這個整數用於確定桶的位置,當這個對象需要存儲在一些HashTableHashMap類的數據結構中時。 默認情況下,對象的hashCode()方法返回存儲對象的內存地址的整數表示。

當我們將對象插入HashTableHashMapHashSet時,將使用對象的hashCode()方法。 有關 Wikipedia.org 上的HashTables更多信息,以供參考。

要在地圖數據結構中插入任何條目,我們需要鍵和值。 如果鍵和值都是用戶定義的數據類型,鍵的hashCode()將決定內部存儲對象的位置。 當還需要從地圖中查找對象時,鍵的哈希碼將決定在哪里搜索對象。

哈希碼僅在內部指向某個“區域”(或列表、存儲桶等)。 由於不同的密鑰對象可能具有相同的哈希碼,因此哈希碼本身並不能保證找到正確的密鑰。 HashTable然后迭代這個區域(所有鍵都具有相同的哈希碼)並使用鍵的equals()方法來找到正確的鍵。 一旦找到正確的鍵,就返回為該鍵存儲的對象。

因此,如我們所見,在HashTable存儲和查找對象時,使用了hashCode()equals()方法的組合。

筆記:

  1. 始終使用對象的相同屬性來生成hashCode()equals()兩者。 在我們的例子中,我們使用了員工 ID。

  2. equals()必須一致(如果對象沒有被修改,那么它必須保持返回相同的值)。

  3. 每當a.equals(b) ,那么a.hashCode()必須與b.hashCode()相同。

  4. 如果您覆蓋一個,那么您應該覆蓋另一個。

http://parameshk.blogspot.in/2014/10/examples-of-comparable-comporator.html

在下面的示例中,如果您在 Person 類中注釋掉對 equals 或 hashcode 的覆蓋,此代碼將無法查找 Tom 的訂單。 使用哈希碼的默認實現會導致哈希表查找失敗。

我在下面是一個簡化的代碼,它按人拉出人們的訂單。 Person 被用作哈希表中的鍵。

public class Person {
    String name;
    int age;
    String socialSecurityNumber;

    public Person(String name, int age, String socialSecurityNumber) {
        this.name = name;
        this.age = age;
        this.socialSecurityNumber = socialSecurityNumber;
    }

    @Override
    public boolean equals(Object p) {
        //Person is same if social security number is same

        if ((p instanceof Person) && this.socialSecurityNumber.equals(((Person) p).socialSecurityNumber)) {
            return true;
        } else {
            return false;
        }

    }

    @Override
    public int hashCode() {        //I am using a hashing function in String.java instead of writing my own.
        return socialSecurityNumber.hashCode();
    }
}


public class Order {
    String[]  items;

    public void insertOrder(String[]  items)
    {
        this.items=items;
    }

}



import java.util.Hashtable;

public class Main {

    public static void main(String[] args) {

       Person p1=new Person("Tom",32,"548-56-4412");
        Person p2=new Person("Jerry",60,"456-74-4125");
        Person p3=new Person("Sherry",38,"418-55-1235");

        Order order1=new Order();
        order1.insertOrder(new String[]{"mouse","car charger"});

        Order order2=new Order();
        order2.insertOrder(new String[]{"Multi vitamin"});

        Order order3=new Order();
        order3.insertOrder(new String[]{"handbag", "iPod"});

        Hashtable<Person,Order> hashtable=new Hashtable<Person,Order>();
        hashtable.put(p1,order1);
        hashtable.put(p2,order2);
        hashtable.put(p3,order3);

       //The line below will fail if Person class does not override hashCode()
       Order tomOrder= hashtable.get(new Person("Tom", 32, "548-56-4412"));
        for(String item:tomOrder.items)
        {
            System.out.println(item);
        }
    }
}

String 類和包裝類具有與 Object 類不同的equals()hashCode()方法實現。 Object 類的 equals() 方法比較對象的引用,而不是內容。 Object 類的 hashCode() 方法為每個對象返回不同的哈希碼,無論內容是否相同。

當您使用 Map 集合並且鍵是 Persistent 類型,StringBuffer/builder 類型時,它會導致問題。 由於與 String 類不同,它們不會覆蓋 equals() 和 hashCode(),因此當您比較兩個不同的對象時,即使它們具有相同的內容,equals() 也會返回 false。 它將使 hashMap 存儲相同的內容鍵。 存儲相同的內容鍵意味着它違反了 Map 規則,因為 Map 根本不允許重復鍵。 因此,您覆蓋類中的 equals() 和 hashCode() 方法並提供實現(IDE 可以生成這些方法),以便它們與 String 的 equals() 和 hashCode() 工作相同並防止相同的內容鍵。

您必須將 hashCode() 方法與 equals() 一起覆蓋,因為 equals() 根據哈希碼工作。

此外,覆蓋 hashCode() 方法和 equals() 有助於完善 equals()-hashCode() 約定:“如果兩個對象相等,則它們必須具有相同的哈希碼。”

什么時候需要為 hashCode() 編寫自定義實現?

眾所周知,HashMap 的內部工作是基於 Hashing 的原理。 有一些存儲條目集的存儲桶。 您可以根據自己的要求自定義 hashCode() 實現,以便可以將相同的類別對象存儲到相同的索引中。 當您使用put(k,v)方法將值存儲到 Map 集合中時, put(k,v)的內部實現是:

put(k, v){
hash(k);
index=hash & (n-1);
}

意思是,它生成索引,索引是基於特定鍵對象的哈希碼生成的。 因此,讓此方法根據您的要求生成哈希碼,因為相同的哈希碼條目集將存儲到相同的存儲桶或索引中。

就是這樣!

恕我直言,這是按照規則說的 - 如果兩個對象相等,那么它們應該具有相同的哈希值,即相等的對象應該產生相等的哈希值。

上面給出,對象中的默認 equals() 是 == 對地址進行比較,hashCode() 以整數形式返回地址(實際地址上的散列),這對於不同的對象也是不同的。

如果需要在基於Hash的集合中使用自定義對象,則需要同時覆蓋equals()和hashCode(),例如如果我想維護Employee對象的HashSet,如果我不使用更強的hashCode和equals我可能最終會覆蓋兩個不同的員工對象,當我使用年齡作為 hashCode() 時會發生這種情況,但是我應該使用可以是員工 ID 的唯一值。

為了幫助您檢查重復的對象,我們需要一個自定義的 equals 和 hashCode。

由於哈希碼總是返回一個數字,因此使用數字而不是字母鍵檢索對象總是很快。 它會怎么做? 假設我們通過傳遞一些其他對象中已經可用的值來創建一個新對象。 現在新對象將返回與另一個對象相同的哈希值,因為傳遞的值相同。 一旦返回相同的散列值,JVM 將每次都轉到相同的內存地址,如果存在多個對象用於相同的散列值,它將使用 equals() 方法來識別正確的對象。

當您想在 Map 中存儲和檢索自定義對象作為鍵時,您應該始終覆蓋自定義 Object 中的 equals 和 hashCode。 例如:

Person p1 = new Person("A",23);
Person p2 = new Person("A",23);
HashMap map = new HashMap();
map.put(p1,"value 1");
map.put(p2,"value 2");

這里 p1 和 p2 將僅視為一個對象,並且map大小將僅為 1,因為它們相等。

public class Employee {

    private int empId;
    private String empName;

    public Employee(int empId, String empName) {
        super();
        this.empId = empId;
        this.empName = empName;
    }

    public int getEmpId() {
        return empId;
    }

    public void setEmpId(int empId) {
        this.empId = empId;
    }

    public String getEmpName() {
        return empName;
    }

    public void setEmpName(String empName) {
        this.empName = empName;
    }

    @Override
    public String toString() {
        return "Employee [empId=" + empId + ", empName=" + empName + "]";
    }

    @Override
    public int hashCode() {
        return empId + empName.hashCode();
    }

    @Override
    public boolean equals(Object obj) {

        if (this == obj) {
            return true;
        }
        if (!(this instanceof Employee)) {
            return false;
        }
        Employee emp = (Employee) obj;
        return this.getEmpId() == emp.getEmpId() && this.getEmpName().equals(emp.getEmpName());
    }

}

測試班

public class Test {

    public static void main(String[] args) {
        Employee emp1 = new Employee(101,"Manash");
        Employee emp2 = new Employee(101,"Manash");
        Employee emp3 = new Employee(103,"Ranjan");
        System.out.println(emp1.hashCode());
        System.out.println(emp2.hashCode());
        System.out.println(emp1.equals(emp2));
        System.out.println(emp1.equals(emp3));
    }

}

在對象類中,equals(Object obj) 用於比較地址比較,這就是為什么在測試類中,如果您比較兩個對象,則 equals 方法給出 false 但是當我們覆蓋 hashcode() 時,它可以比較內容並給出正確的結果。

這兩種方法都在 Object 類中定義。 兩者都是最簡單的實現。 因此,當您需要為這些方法添加更多實現時,您可以在類中進行覆蓋。

例如:對象中的 equals() 方法僅檢查其在引用上的相等性。 因此,如果您還需要比較它的狀態,那么您可以像在 String 類中那樣覆蓋它。

這個答案中沒有提到測試 equals/hashcode 合同。

我發現EqualsVerifier庫非常有用和全面。 它也很容易使用。

此外,從頭開始構建equals()hashCode()方法涉及大量樣板代碼。 Apache Commons Lang庫提供了EqualsBuilderHashCodeBuilder類。 這些類極大地簡化了復雜類的equals()hashCode()方法的實現。

順便說一句,值得考慮覆蓋toString()方法以幫助調試。 Apache Commons Lang庫提供了ToStringBuilder類來幫助解決這個問題。

最近,我通讀了這份Developer Works文檔

該文檔旨在有效,正確地定義hashCode()equals() ,但是我無法弄清為什么我們需要重寫這兩種方法。

我該如何決定有效地實施這些方法?

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM