自定義類如何重寫hashCode()和equals()

在面試中經常遇到這樣一個問題,==和equals()方法有什麼區別?
很多時候,我們回答的都不算完全。這裏總結一下:
==分兩種情況:
1.基本數據類型,比較的是值相等
2.引用類型,比較的是對象的地址

equals分兩種情況:
若沒有對equals()進行重寫,則比較的是引用類型的對象的地址,和==效果一樣
若對equals()進行了重寫,如String、Date等類,那麼比較的是值是否相等。

int i=2;
int j=2;
i==j;  //true

Person p1 = new Person();
Person p2 = new Person();
System.out.println(p1==p2); //false
Person p3 = p2;
System.out.println(p2==p3); //true

若沒有重寫equals:

Person p1 = new Person();
p1.setName("張三");
Person p2 = new Person();
p2.setName("張三");
System.out.println(p1.equals(p2));  //false
Person p3 = p2;
System.out.println(p2.equals(p3));  //true

若重寫了equals:

String a = "123";
String b = "123";
System.out.println(a.equals(b));  //true

我們在重寫equals的時候,還得重寫hashCode()方法。那麼爲什麼要重寫hashCode()方法呢?因爲對象的比較有硬性規定:
1.應用執行期間,同一個對象內容不發生改變,經過多次調用,hashCode方法都必須始終返回同一個值。如果把對象重新copy到另外一個應用程序裏,hashcode可以和上一個的不一樣。
2.如果兩個對象根據equals(Object)方法比較是相等的,那麼所產生的的hashcode必須一樣。
3.如果兩個對象根據equals(Object)方法比較是不相等的,那麼產生的hashcode可能一樣,也可能不一樣。

若我們只重寫了equals()方法,沒有重寫hashCode()方法,那麼它會調用父類Object的hashCode()方法,那麼得到的hash值就不一樣:

public static void main(String[] args) {
        Person p1 = new Person();
        p1.setName("張三");
        p1.setAge(23);
        Person p2 = new Person();
        p2.setName("張三");
        p2.setAge(23);
        System.out.println(p1.hashCode()); //664740647
        System.out.println(p2.hashCode()); //804564176
        System.out.println(p1.equals(p2)); //true
}

那麼我們重寫的hashCode()如何自定義呢?
第一步:定義一個初始值,一般來說取17

int result = 17;

第二步:分別解析自定義類中與equals方法相關的字段(假如hashCode中考慮的字段在equals方法中沒有考慮,則兩個equals的對象就很可能具有不同的hashCode)
情況一:字段a類型爲boolean 則[hashCode] = a ? 1 : 0;
情況二:字段b類型爲byte/short/int/char, 則[hashCode] = (int)b;
情況三:字段c類型爲long, 則[hashCode] = (int) (c ^ c>>>32);
情況四:字段d類型爲float, 則[hashCode] = d.hashCode()(內部調用的是Float.hashCode(d), 而該靜態方法內部調用的另一個靜態方法是Float.floatToIntBits(d))
情況五:字段e類型爲double, 則[hashCode] = e.hashCode()(內部調用的是Double.hashCode(e), 而該靜態方法內部調用的另一個靜態方法是Double.doubleToLongBits(e),得到一個long類型的值之後,跟情況三進行類似的操作,得到一個int類型的值)
情況六:引用類型,若爲null則hashCode爲0,否則遞歸調用該引用類型的hashCode方法。
情況七:數組類型。(要獲取數組類型的hashCode,可採用如下方法:s[0]*31 ^ (n-1) + s[1] * 31 ^ (n-2) + … + s[n-1], 該方法正是String類的hashCode實現所採用的算法)

第三步:對於涉及到的各個字段,採用第二步中的方式,將其依次應用於下式:

result = result * 31 + [hashCode];

補充說明如果初始值result不取17而取0的話,則對於hashCode爲0的字段來說就沒有區分度了,這樣更容易產生衝突。

計算hashCode的注意事項
1、不能包含equals方法中沒有的字段,否則會導致相等的對象可能會有不同的哈希值。(即對類中每一個重要字段,也就是影響對象的值的字段,也就是equals方法裏有比較的字段,進行操作)
2、String對象和Bigdecimal對象已經重寫了hashcode方法,這些類型的值可以直接用於重寫hashcode方法;
3、result = 31 result + [hashCode];,這裏面爲啥用個31來計算,而且很多人都是這麼寫的,這是因爲31是個神奇的數字,任何數n31都可以被jvm優化爲(n<<5)-n,移位和減法的操作效率比乘法的操作效率高很多!
4、Google首席Java架構師Joshua Bloch在他的著作《Effective Java》中提出了一種簡單通用的hashCode算法:
1).初始化一個整形變量,爲此變量賦予一個非零的常數值,比如int result = 17;
2).如果是對象應用(例如有String類型的字段),如果equals方法中採取遞歸調用的比較方式,那麼hashCode中同樣採取遞歸調用hashCode的方式。否則需要爲這個域計算一個範式,比如當這個域的值爲null的時候(即 String a = null 時),那麼hashCode值爲0。

這裏可以看一下我們自定義的示例:

public class TestHash {

    public static void main(String[] args) {
        Person p1 = new Person();
        p1.setName("張三");
        p1.setAge(23);
        Person p2 = new Person();
        p2.setName("張三");
        p2.setAge(23);
        System.out.println(p1.equals(p2));
    }
}

class Person{

    private String name;
    private int 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 int hashCode() {
        int result = name.hashCode();
        result = 31*result + age;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }

        Person person = (Person) obj;
        if (!name.equals(person.getName())) {
            return false;
        }
        return true;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章