概述
在我們使用類集架構(比方使用hashMap、hashSet)的時候,常常會涉及到重寫equals()和hashCode()這兩個方法。
這兩個方法的聯系是:
1. 假設兩個對象不同,那麼他們的hashCode肯定不相等;
2. 假設兩個對象的hashCode同樣。那麼他們也未必相等。
是以說。假設想在hashMap裡面讓兩個不相等的對象相應同一個值,首先須要讓他們的hashCode同樣。其次還要讓他們的equals()方法傳回true,是以為了達到這個目的,我們就僅僅能重寫hashCode()和equals()這兩個方法了。
引用一篇文章的講解:The idea behind a Map is to be able to find an object faster than a linear search. Using hashed keys to locate objects is a two-step process. Internally the Map stores objects as an array of arrays. The index for the first array is the hashcode() value of the key. This locates the second array which is searched linearly by using equals() to determine if the object is found.
大緻意思就是:使用Map比線性搜尋要快。Map存儲對象是使用數組的數組(能夠了解為二維數組。這并不準确,隻是能夠依照這個了解。之前hashMap用的是數組。每一個數組節點相應一個連結清單。如今Java 8已經把連結清單改成了treeMap。相當于一個二叉樹。這樣檢索的時候比連結清單更快,尤其是在最壞情況下,由原來連結清單的O(n)變成了二叉樹的O(logn),詳見https://dzone.com/articles/hashmap-performance)。是以搜尋大緻是分為兩步的,第一步是依據hashCode尋找第一維數組的下标,然後依據equals的傳回值推斷對象是第二維數組中的哪一個。
例證
舉個栗子——
import java.util.HashMap;
public class Apple {
private String color;
public Apple(String color) {
this.color = color;
}
public static void main(String[] args) {
Apple a1 = new Apple("green");
Apple a2 = new Apple("red");
//hashMap stores apple type and its quantity
HashMap<Apple, Integer> m = new HashMap<Apple, Integer>();
m.put(a1, 10);
m.put(a2, 20);
System.out.println(m.get(new Apple("green")));
}
}
程式的執行結果為:null
此時,我們已經向hashMap裡面存儲兩個對象了,且a1就是green Apple,那麼為什麼我們通過”green”去查找卻傳回null呢?
顯然。後來我們新new出來一個對象,這和之前加入的a1綠蘋果那個對象絕對不是同一個對象,依據終極父類Object中的hashCode()的計算結果,其傳回值絕對是不一樣的。
是以——
第一步:重寫hashCode()
我們須要先讓“凡是color屬性同樣的對象,其hashCode都一樣”,是以我們能夠這樣重寫hashCode():
public int hashCode(){
return this.color.length();
}
這裡我們使用color屬性的内容的長度作為hashCode的大小,那麼凡是green的蘋果,hashCode肯定都是5(green字元串長度為5),這樣一來,屬性同樣的對象的hashCode肯定都同樣了。這僅僅是保證了一維數組的下标找到了(姑且這樣了解),還須要找第二維的下标呢,這個須要在第二步中解決。在解決第二步之前,你可能會有問題——這樣一來的話。假設有black蘋果(假設有black),那麼它的hashCode也變成了5了啊,和green一樣了。這個同樣靠第二步解決。
第二步:重寫equals()
我們已經知道。假設想讓兩個對象一樣,除了讓他們的hashCode值一樣外,還要讓他們的equals()函數傳回true,兩個都符合才算一樣。
是以第二步我們要重寫equals()函數,使“僅僅要color一樣,兩個蘋果就是同樣的”:
public boolean equals(Object obj) {
if (!(obj instanceof Apple)) //首先類型要同樣才有可能傳回true
return false;
if (obj == this) //假設是自己跟自己比。顯然是同一個對象,傳回true
return true;
return this.color.equals(((Apple) obj).color); //假設顔色同樣,也傳回true
}
這樣一來,依據最後一句話。凡是顔色同樣的蘋果。第二維也映射到同一個位置了(姑且這麼了解)。這樣一來,就能夠依據顔色在hashMap裡尋找蘋果了。
把我們重寫過的hashCode()和equals()加入到之前的代碼中,便會輸出結果:10,即鍵a1所相應的值。
結語
感覺這篇文章涉及的内容還是相當基礎和重要的。
文章到此也差點兒相同能夠結束了。另附上曾經學習時記的筆記,感覺還是挺實用的,我自己的筆記自己看起來自然是毫無障礙的,隻是實在不想整理了,就直接貼上來吧,大家将就将就看看吧,能夠作為上面内容的唠叨和補充。
附錄1
HashSet在存儲的時候(比方存的是字元串)。則存進去之後依照哈希值排序(也就意味着周遊的時候得到的順序不是我們加入的順序,即亂序),假設第二個對象和第一個Hash值一樣可是對象不一樣,則第二個會鍊在第一個後面。在加入對象的時候,add()傳回boolean型,假設加入的對象同樣(比方兩個同樣的字元串),則傳回false。加入失敗。
HashSet怎樣保證元素的唯一性?
通過元素的方法——hashCode()和equals()來實作。
假設兩個元素的hashCode不同,直接就存了;
假設兩個元素的hashCode同樣,則調用equals推斷是否為true,true則證明是同一個對象,就不存了,false的話證明是不同的對象,存之。
一旦自己定義了對象。想要存進HashSet,則一定要覆寫hashCode()和equals()方法——
比方我們定義Person類,僅含有name,age兩個參數。規定:僅僅要姓名和年齡同樣,就斷定為“同一個人”,則不能存入HashSet,否則的話能夠。
對于這種情況。假設我們new出來幾個人,當中存在名字和年齡同樣的,則均會存入HashSet。原因就是這些對象是不同的對象,所占記憶體不一樣,則通過hashCode()傳回的哈希值也都不一樣。是以理所當然的存入了HashSet。為了避免把“同一個人”存進HashSet,我們首先須要讓hashCode()針對“同一個人”傳回同樣的哈希值,即覆寫hashCode()方法!
public int hashCode(){
return 110;
}
這樣自然也能夠,隻是沒有必要讓全部的對象傳回的哈希值都一樣,僅僅要“同一個人”的哈希值一樣即可了,是以寫成這樣更好:
public int hashCode(){
return name.hashCode() + age;
}
這種話“同一個人”傳回的哈希值就是同樣的。隻是這樣還是不夠完美。由于覆寫的這個hashCode()盡管會讓“同樣的人”傳回同樣的哈希值,但也可能會讓“不同的人”傳回同樣的哈希值。比方兩個人name不同,age不同。但name的哈希值加上age恰恰同樣,這種話就坑爹了。為了避免這種現象,讓這種哈希值恰巧撞上的機率進一步減小,我們寫成這樣會更好:
public int hashCode(){
return name.hashCode() + age * 19;
}
最好是乘上一個素數之類的,能夠大大減少“不同的人”的哈希值撞上的機率。
通過以上hashCode()函數的覆寫。我們讓“同樣的人”的哈希值同樣了,那麼接下來就要覆寫equals()函數。由于Java碰到哈希值同樣的情況之後,接下來要依據equals()函數推斷兩個對象是否同樣,同樣則不再存入HashSet,不同的話就存進去,且是鍊在和它哈希值同樣的對象上的(鍊成一串兒)。
附錄2:下面是TreeSet内容。和上面關系不大了
TreeSet能夠對裡面的元素進行排序,比方假設對象是字元串,則依照字典序排序。
假設要存自己定義對象,須要讓自己定義的對象具有比較性。這種話TreeSet才幹将其依照一定的順序去排序。
否則會報出異常。為了讓自己定義對象能夠具有比較性。對象須要實作Comparable接口。
比方,有一個Person類。我們要new出來一些人放到TreeSet裡面,為了使對象具有可比性進而能夠存入TreeSet。我們規定依照對象的年齡排序。
首先,讓Person類實作Comparable接口,覆寫接口的compareTo()方法。其傳回值和c語言的strcmp()同樣:
這種話。假設某兩個對象的年齡同樣,則後來者将不會被存入TreeSet,由于後來者被覺得和之前的那個是同一個對象。
是以我們對主關鍵字排序之後一定要對次關鍵字進行排序。僅僅有全部的關鍵字都比較完成還是傳回0,我們才幹覺得兩個對象同樣。
是以覆寫的compareTo()應該是這種:
public int compareTo(Object obj){ //傳進來的須要是Object類型,這一點要注意
if(!(obj instanceof Person)){ //傳進來的對象不正确,直接抛出異常
throw new RuntimeException("Not The Same Kind Of Object!");
}
Person p = (Person)obj; //将對象向下轉型
if(this.age < p.age)
return -1;
if(this.age > p.age)
return 1;
if(this.age == p.age)
{
return this.name.compareTo(p.name); //String類中覆寫過Comparable接口的空方法compareTo()。依照字典序對字元串進行排序
}
}
是以,假設你想讓TreeSet依照輸入順序存資料,而不是自己主動排序。能夠這樣覆寫compareTo()方法:
public int compareTo(Object obj){
return 1;
}
非常easy,隻是缺陷就是“同樣的人”也會被存進去。這個函數是全然依照輸入内容的順序不加以不論什麼删改原模原樣原順序存進TreeSet的。
同理,假設return -1就是輸入順序的逆序;假設return 0則僅僅能存入第一個輸入的對象。
以上是TreeSet排序的第一種方法——讓元素自身具有比較性(讓類實作Comparable接口,覆寫compareTo()方法)。
隻是這個方案有缺陷,比方我突然不想依照年齡排,想依照姓名排序。這就須要又一次改動代碼了。
是以TreeSet排序的另外一種方法——讓集合自身具有比較性(将自己定義的比較器傳入Treeset的構造函數)。
比方我們如今要依照人名字典序排序:
首先建造一個比較器。實作Comparator接口,覆寫compare()方法(傳回值也和strcmp()一樣):
public PersonCompareMethod implements Comparator{
public int compare(Object obj1, Object obj2){ //傳入兩個Object對象
Person p1 = (Person)obj1; //向下轉型
Person p2 = (Person)obj2;
int num = p1.getName().compareTo(p2.getName()); //直接比較兩個字元串的字典序,由于String類已經覆寫過Comparable接口的空方法compareTo()。依照字典序對字元串進行排序
if(num == 0){
return new Integer(p1.getAge()).compareTo(new Integer(p2.getAge()));
//這一句話比較巧妙!!!本來我們是能夠直接依照數字大小比較年齡這個次要關鍵字的,隻是在比較的時候,我們換了一種比較高端的方法:建立了兩個Integer對象,由于Integer類裡面也有compareTo()方法。将數字依照字典序進行比較。
當然是實作了Comparable接口并覆寫compareTo()方法才具有這種功能 }
return num;
}
}
之後,将我們的構造器傳入TreeSet建立對象時調用的構造函數即可:
TreeSet ts = new TreeSet(new MyCompareMethod());
使用另外一種方式的情況:
對象不具有比較性。或者是比較性并非我們所須要的。