1. Set和存儲順序
2. 理解Map
再次強調默認的Object.equal()只是比較對象的地址,所以如果要用自己的類作爲HashMap的鍵,必須同時重載hashCode()和equal()方法。
3. 理解hashCode()
使用散列的目的在於:想要使用一個對象來查找另一個對象。散列的價值在於速度(爲什麼使用散列):散列使得查詢得以快速進行。
我們知道存儲一組元素最快的數據結構是數組,所以使用數組來表示鍵的信息(並非鍵本身)。通過鍵對象生成一個數字(稱爲散列碼,即hashCode()方法生成的數字),將其作爲數組的下標。而這個數組中存放的是值的List,因爲相同散列碼的鍵所對應的值都存放在數組的這個下標中。不同的鍵可以產生相同的下標(散列碼)。於是查詢一個值的過程就是,使用hashCode方法計算出此鍵的散列碼,那麼就知道值所在的數組下標,然後到此下標中的List中使用equals()方法進行線性查詢。
HashMap其實就是數組+List實現的。
那麼設計hashCode()時最重要的元素是:無論何時,對同一個對象調用hashCode()方法都應生成同樣的值。另外一個,好的hashCode()應該產生均勻分佈的散列碼。
4. 爲什麼HashMap是線程不安全的?
因爲HashMap底層維護了一個數組,當多線程時對這個數組的操作是不安全的。
5. 不同接口的選擇
5.1 對List的選擇
將ArrayList作爲默認首選,當程序需要經常從表中間進行插入和刪除時才選用LinkedList。
需要進行隨機訪問是,例如put()和set()操作時,和查詢時,一般ArrayList的速度快,需要頻繁插入和刪除時,LinkedList的速度快。原因:ArrayList底層是數組支持,LinkedList是由雙向鏈表實現的。ArrayList在插入時,必須創建空間並將移動它的引用,所以代價高。而LinkedList只需鏈接新的元素,而不必修改列表中的剩餘元素,所以無論尺寸如何變化,其代價大致相同。
5.2 對Set的選擇
HashSet的性能基本上總是比TreeSet好,特別是在添加和查詢元素時,而這兩個操作也是最重要的操作。TreeSet存在的唯一原因是它可以維持元素的排序狀態。
HashSet的實現原理總結如下:
①是基於HashMap實現的,默認構造函數是構建一個初始容量爲16,負載因子爲0.75 的HashMap。封裝了一個 HashMap 對象來存儲所有的集合元素,所有放入 HashSet 中的集合元素實際上由 HashMap 的 key 來保存,而 HashMap 的 value 則存儲了一個 PRESENT,它是一個靜態的 Object 對象。
②當我們試圖把某個類的對象當成 HashMap的 key,或試圖將這個類的對象放入 HashSet 中保存時,重寫該類的equals(Object obj)方法和 hashCode() 方法很重要,而且這兩個方法的返回值必須保持一致:當該類的兩個的 hashCode() 返回值相同時,它們通過 equals() 方法比較也應該返回 true。通常來說,所有參與計算 hashCode() 返回值的關鍵屬性,都應該用於作爲 equals() 比較的標準。
③HashSet的其他操作都是基於HashMap的。
LinkedHashSet
是基於鏈表實現的。對於插入操作,LinkedHashSet比HashSet的代價更高,這是由於維護鏈表所帶來的額外開銷造成的。
HashSet導致的內存泄漏
把一個對象存儲進hashSet集合後,修改這個對象中參與計算hash的變量的值,這時這個對象的hash值也會隨之改變,那麼這麼對象可以正常地被刪除嗎?下面用代碼試一下:
public class MPoint {
private int x;
private int y;
public MPoint() {
}
public MPoint( int x, int y) {
this. x = x;
this. y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this. x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this. y = y;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x; //x參與計算hash值
return result;
}
@Override
public boolean equals(Object obj) {
if ( this == obj)
return true;
if ( obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MPoint other = (MPoint) obj;
if ( x != other. x)
return false;
if ( y != other. y)
return false;
return true;
}
}
主函數類測試,在一個HashSet集合中添加三個元素,mp1、mp2、mp3,並進行其中屬性的修改和輸出測試
public class HashSetTest {
public static void main(String[] args) {
HashSet<MPoint> set = new HashSet<MPoint>();
MPoint mp1 = new MPoint(1, 6);
MPoint mp2 = new MPoint(2, 7);
MPoint mp3 = new MPoint(1, 6);
set.add( mp1);
set.add( mp2);
set.add( mp3);
set.add( mp1);
System. out.println( set.size()); // 結果爲2
mp1.setX(3);
set.remove( mp1);
System. out.println( set.size()); // 結果還是爲2,說明沒有刪除成功
System. out.println( set.contains( mp1)); // 結果爲false,修改了參與計算hash值的變量,其對象不能再被找到
System. out.println( set.remove( mp1)); // 結果爲false,修改了參與計算hash值的變量,其對象不能被刪除
mp2.setY(2);
System. out.println( set.contains( mp2)); // 結果爲true,沒有修改關鍵屬性的對象可以被找到
System. out.println( set.remove( mp2)); // 結果爲true,沒有修改關鍵屬性的對象可以被刪除
System. out.println( set.size()); // 結果還是爲1
}
}
輸出:
2
2
false
false
true
true
1
可以看出已經發生了內存泄漏了,mp1對象不能被正常刪除。
5.3 對Map的選擇
除了IdentityHashMap,所有的Map實現的插入操作都會隨着Map尺寸的變大而明顯變慢。但是,查找的代價通常比插入要小得多,這是個好消息,因爲我們執行查找元素的操作要比執行插入元素的操作多得多。
HashMap與Hashtable的性能大體相當。因爲HashMap是用來替代Hashtable的,他們使用相同的底層存儲和查找機制。
TreeMap通常比HashMap慢,但它能根據key來排序。
LinkedHashMap在插入時比HashMap慢一點,因爲它維護散列數據結構的同時還要維護鏈表(以保證插入順序)。正是由於這個列表,使得其迭代速度更快。LinkedHashMap是HashMap的一個子類,它保留插入的順序。LinkedHashMap是Map接口的哈希表和鏈接列表實現,具有可預知的迭代順序。 LinkedHashMap實現與HashMap的不同之處在於,後者維護着一個運行於所有條目的雙重鏈接列表。此鏈接列表定義了迭代順序,該迭代順序可以是插入順序或者是訪問順序。
HashMap的性能因子:
負載因子:尺寸/容量。負載輕的表產生衝突的可能性小。當達到負載因子的水平時,容器自動增加其容量,並重新將現有對象分佈到新的桶位集中,這被稱爲“再散列”。
桶:一個數組的某一項就是一個桶,如a[0]、a[1]都是桶