1. 程式人生 > >HashSet與HashMap的分析, HashCode與equals的分析

HashSet與HashMap的分析, HashCode與equals的分析

        以下為個人見解,如有謬誤,歡迎指出。

       首先HashSet是set集合的子類,無序,不可重複,底層是雜湊表實現。雜湊表是陣列+連結串列+紅黑樹組成(jdk1.8之後),我們如何判斷往HashSet集合中新增元素。這個時候就不得不討論HashCode和equals兩個方法了。

      HashCode與equals都是object父類的成員方法,所以任何物件都有HashCode()和 equals()方法,

第1部分 equals() 的作用

equals() 的作用是 用來判斷兩個物件是否相等

equals() 定義在JDK的Object.java中。通過判斷兩個物件的地址是否相等(即,是否是同一個物件)來區分它們是否相等。原始碼如下:

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

既然Object.java中定義了equals()方法,這就意味著所有的Java類都實現了equals()方法,所有的類都可以通過equals()去比較兩個物件是否相等。 但是,我們已經說過,使用預設的“equals()”方法,等價於“==”方法。因此,我們通常會重寫equals()方法:若兩個物件的內容相等,則equals()方法返回true;否則,返回fasle。  

下面根據“類是否覆蓋equals()方法”,將它分為2類。
(01) 若某個類沒有覆蓋equals()方法,當它的通過equals()比較兩個物件時,實際上是比較兩個物件是不是同一個物件(比較兩個物件的地址)。這時,等價於通過“==”去比較這兩個物件(==在比較基本型別時,直接比例變數值,在比較引用型別時,比較物件的地址)。
(02) 我們可以覆蓋類的equals()方法,來讓equals()通過其它方式比較兩個物件是否相等。通常的做法是:若兩個物件的內容相等,則equals()方法返回true;否則,返回fasle。


下面,舉例對上面的2種情況進行說明。

1.  “沒有覆蓋equals()方法”的情況

import java.util.*;
import java.lang.Comparable;

/**
 * @desc equals()的測試程式。
 *
 * @author skywang
 * @emai [email protected]
 */
public class EqualsTest1{

    public static void main(String[] args) {
        // 新建2個相同內容的Person物件,
        // 再用equals比較它們是否相等
        Person p1 = new Person("eee", 100);
        Person p2 = new Person("eee", 100);
        System.out.printf("%s\n", p1.equals(p2));
    }

    /**
     * @desc Person類。
     */
    private static class Person {
        int age;
        String name;

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

        public String toString() {
            return name + " - " +age;
        }
    }
}

 

執行結果

false

結果分析

       我們通過 p1.equals(p2) 來“比較p1和p2是否相等時”。實際上,呼叫的Object.java的equals()方法,即呼叫的 (p1==p2) 。它是比較“p1和p2是否是同一個物件”。
       而由 p1 和 p2 的定義可知,它們雖然內容相同;但它們是兩個不同的物件!因此,返回結果是false。

 

2. "覆蓋equals()方法"的情況

我們修改上面的EqualsTest1.java覆蓋equals()方法

程式碼如下 (EqualsTest2.java)

import java.util.*;
import java.lang.Comparable;

/**
 * @desc equals()的測試程式。
 *
 * @author skywang
 * @emai [email protected]
 */
public class EqualsTest2{

    public static void main(String[] args) {
        // 新建2個相同內容的Person物件,
        // 再用equals比較它們是否相等
        Person p1 = new Person("eee", 100);
        Person p2 = new Person("eee", 100);
        System.out.printf("%s\n", p1.equals(p2));
    }

    /**
     * @desc Person類。
     */
    private static class Person {
        int age;
        String name;

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

        public String toString() {
            return name + " - " +age;
        }

        /** 
         * @desc 覆蓋equals方法 
         */  
        @Override
        public boolean equals(Object obj){  
            if(obj == null){  
                return false;  
            }  
              
            //如果是同一個物件返回true,反之返回false  
            if(this == obj){  
                return true;  
            }  
              
            //判斷是否型別相同  
            if(this.getClass() != obj.getClass()){  
                return false;  
            }  
              
            Person person = (Person)obj;  
            return name.equals(person.name) && age==person.age;  
        } 
    }
}

執行結果

true

結果分析

我們在EqualsTest2.java 中重寫了Person的equals()函式:當兩個Person物件的 name 和 age 都相等,則返回true。
因此,執行結果返回true。

 

講到這裡,順便說一下java對equals()的要求。有以下幾點:

1. 對稱性:如果x.equals(y)返回是"true",那麼y.equals(x)也應該返回是"true"。
2. 反射性:x.equals(x)必須返回是"true"。
3. 類推性:如果x.equals(y)返回是"true",而且y.equals(z)返回是"true",那麼z.equals(x)也應該返回是"true"。
4. 一致性:如果x.equals(y)返回是"true",只要x和y內容一直不變,不管你重複x.equals(y)多少次,返回都是"true"。
5. 非空性,x.equals(null),永遠返回是"false";x.equals(和x不同型別的物件)永遠返回是"false"。

 

 


第2部分 equals() 與 == 的區別是什麼?

== : 它的作用是判斷兩個物件的地址是不是相等。即,判斷兩個物件是不試同一個物件。

equals() : 它的作用也是判斷兩個物件是否相等。但它一般有兩種使用情況(前面第1部分已詳細介紹過):
                 情況1,類沒有覆蓋equals()方法。則通過equals()比較該類的兩個物件時,等價於通過“==”比較這兩個物件。
                 情況2,類覆蓋了equals()方法。一般,我們都覆蓋equals()方法來兩個物件的內容相等;若它們的內容相等,則返回true(即,認為這兩個物件相等)。

 

下面,通過示例比較它們的區別。

程式碼如下: 

import java.util.*;
import java.lang.Comparable;

/**
 * @desc equals()的測試程式。
 *
 * @author skywang
 * @emai [email protected]
 */
public class EqualsTest2{

    public static void main(String[] args) {
        // 新建2個相同內容的Person物件,
        // 再用equals比較它們是否相等
        Person p1 = new Person("eee", 100);
        Person p2 = new Person("eee", 100);
        System.out.printf("%s\n", p1.equals(p2));
    }

    /**
     * @desc Person類。
     */
    private static class Person {
        int age;
        String name;

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

        public String toString() {
            return name + " - " +age;
        }

        /** 
         * @desc 覆蓋equals方法 
         */  
        @Override
        public boolean equals(Object obj){  
            if(obj == null){  
                return false;  
            }  
              
            //如果是同一個物件返回true,反之返回false  
            if(this == obj){  
                return true;  
            }  
              
            //判斷是否型別相同  
            if(this.getClass() != obj.getClass()){  
                return false;  
            }  
              
            Person person = (Person)obj;  
            return name.equals(person.name) && age==person.age;  
        } 
    }
}

執行結果

p1.equals(p2) : true
p1==p2 : false

結果分析

在EqualsTest3.java 中:
(01) p1.equals(p2) 
        這是判斷p1和p2的內容是否相等。因為Person覆蓋equals()方法,而這個equals()是用來判斷p1和p2的內容是否相等,恰恰p1和p2的內容又相等;因此,返回true。

(02) p1==p2
       這是判斷p1和p2是否是同一個物件。由於它們是各自新建的兩個Person物件;因此,返回false。

 


第3部分 hashCode() 的作用

hashCode() 的作用是獲取雜湊碼,也稱為雜湊碼;它實際上是返回一個int整數。這個雜湊碼的作用是確定該物件在雜湊表中的索引位置。

hashCode() 定義在JDK的Object.java中,這就意味著Java中的任何類都包含有hashCode() 函式。但是如何子類不重寫hashCode()方法,那麼子類物件的hashCode值是一個不定值,總在變換,所以在JAVA中,基本型別,和String 都重寫了hashCode 方法和equals方法,這樣保證了每個物件都有唯一的雜湊值, equals方法比較的是兩個物件的本身內容。

 

   所以往HashSet集合中新增元素的時候,我們會先判斷元素的雜湊值,在雜湊表中是否存在,如果存在,再用equals比較兩個元素是否相等,如果相同,就不新增,如果不相同,就在連結串列頭新增新的元素。
      HashMap的底層是用hash陣列和單向連結串列實現的 ,當呼叫put方法是,首先計算key的hashcode,定位到合適的陣列索引,然後再在該索引上的單向連結串列進行迴圈遍歷用equals比較key是否存在,如果存在則用新的value覆蓋原值,如果不存在,則插入單向連結串列的頭部(而不是向後追加)。HashMap的兩個重要屬性是容量capacity和載入因子loadfactor,預設值分佈為16和0.75,當容器中的元素個數大於 capacity*loadfactor時,容器會進行擴容resize 為2n,在初始化Hashmap時可以對著兩個值進行修改,負載因子0.75被證明為是效能比較好的取值,通常不會修改,那麼只有初始容量capacity會導致頻繁的擴容行為,這是非常耗費資源的操作,所以,如果事先能估算出容器所要儲存的元素數量,最好在初始化時修改預設容量capacity,以防止頻繁的resize操作影響效能。