Thinking in java:17 容器深入研究

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的代價更高,這是由於維護鏈表所帶來的額外開銷造成的。


TreeSet

TreeSet採用的數據結構是紅黑樹,我們可以讓它按指定規則對其中的元素進行排序。它又是如何判斷兩個元素是否相同呢?除了用equals方法檢查兩個元素是否相同外,還要檢查compareTo方法是否返回爲0。
所以如果對象要存放到Tree集合裏,需要在重寫compareTo時,把相同的對象的比較值定爲0,防止相同的元素被重複添加進集合中。

面試分享:
HashSet在源代碼內是如何實現不add相同的元素的?

理論上我們都知道,它的內部是根據equals()返回值和hashCode()的值是否相同兩個方法來判斷兩個對象是否相同的。而源代碼上,HashSet內部用了一個HashMap作存儲,add()方法內是調用了map的put()方法,map的put()方法會檢查這個鍵是否已存在,若是則返回該鍵之前的值,並更新成新值,若不是則返回null,但這個鍵是不會發生改變的。所以,在源代碼裏,HashSet的add()方法是根據map的put返回值來判斷添加元素是否成功的。

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]都是桶




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章