深入理解HashMap和TreeMap的區別


深入理解HashMap和TreeMap的區別

簡介

HashMap和TreeMap是Map家族中非常常用的兩個類,兩個類在使用上和本質上有什麼區別呢?本文將從這兩個方面進行深入的探討,希望能揭露其本質。

HashMap和TreeMap本質區別

先看HashMap的定義:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

再看TreeMap的定義:

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable

從類的定義來看,HashMap和TreeMap都繼承自AbstractMap,不同的是HashMap實現的是Map接口,而TreeMap實現的是NavigableMap接口。NavigableMap是SortedMap的一種,實現了對Map中key的排序。

這樣兩者的第一個區別就出來了,TreeMap是排序的而HashMap不是。

再看看HashMap和TreeMap的構造函數的區別。

public HashMap(int initialCapacity, float loadFactor) 

HashMap除了默認的無參構造函數之外,還可以接受兩個參數initialCapacity和loadFactor。

HashMap的底層結構是Node的數組:

transient Node<K,V>[] table

initialCapacity就是這個table的初始容量。如果大家不傳initialCapacity,HashMap提供了一個默認的值:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

當HashMap中存儲的數據過多的時候,table數組就會被裝滿,這時候就需要擴容,HashMap的擴容是以2的倍數來進行的。而loadFactor就指定了什麼時候需要進行擴容操作。默認的loadFactor是0.75。

static final float DEFAULT_LOAD_FACTOR = 0.75f;

再來看幾個非常有趣的變量:

static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;

上面的三個變量有什麼用呢?在java 8之前,HashMap解決hashcode衝突的方法是採用鏈表的形式,爲了提升效率,java 8將其轉成了TreeNode。什麼時候會發送這個轉換呢?

這時候就要看這兩個變量TREEIFY_THRESHOLD和UNTREEIFY_THRESHOLD。

有的同學可能發現了,TREEIFY_THRESHOLD爲什麼比UNTREEIFY_THRESHOLD大2呢?其實這個問題我也不知道,但是你看源代碼的話,用到UNTREEIFY_THRESHOLD時候,都用的是<=,而用到TREEIFY_THRESHOLD的時候,都用的是>= TREEIFY_THRESHOLD - 1,所以這兩個變量在本質上是一樣的。

MIN_TREEIFY_CAPACITY表示的是如果table轉換TreeNode的最小容量,只有capacity >= MIN_TREEIFY_CAPACITY的時候才允許TreeNode的轉換。

TreeMap和HashMap不同的是,TreeMap的底層是一個Entry:

private transient Entry<K,V> root

他的實現是一個紅黑樹,方便用來遍歷和搜索。

TreeMap的構造函數可以傳入一個Comparator,實現自定義的比較方法。

public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

如果不提供自定義的比較方法,則使用的是key的natural order。

排序區別

我們講完兩者的本質之後,現在舉例說明,先看下兩者對排序的區別:

    @Test
    public void withOrder(){
        Map<String, String> books = new HashMap<>();
        books.put("bob", "books");
        books.put("c", "concurrent");
        books.put("a", "a lock");
        log.info("{}",books);
    }
    @Test
    public void withOrder(){
        Map<String, String> books = new TreeMap<>();
        books.put("bob", "books");
        books.put("c", "concurrent");
        books.put("a", "a lock");
        log.info("{}",books);
    }

同樣的代碼,一個使用了HashMap,一個使用了TreeMap,我們會發現TreeMap輸出的結果是排好序的,而HashMap的輸出結果是不定的。

Null值的區別

HashMap可以允許一個null key和多個null value。而TreeMap不允許null key,但是可以允許多個null value。

    @Test
    public void withNull() {
        Map<String, String> hashmap = new HashMap<>();
        hashmap.put(null, null);
        log.info("{}",hashmap);
    }
    @Test
    public void withNull() {
        Map<String, String> hashmap = new TreeMap<>();
        hashmap.put(null, null);
        log.info("{}",hashmap);
    }

HashMap會報出: NullPointerException。

性能區別

HashMap的底層是Array,所以HashMap在添加,查找,刪除等方法上面速度會非常快。而TreeMap的底層是一個Tree結構,所以速度會比較慢。

另外HashMap因爲要保存一個Array,所以會造成空間的浪費,而TreeMap只保存要保持的節點,所以佔用的空間比較小。

HashMap如果出現hash衝突的話,效率會變差,不過在java 8進行TreeNode轉換之後,效率有很大的提升。

TreeMap在添加和刪除節點的時候會進行重排序,會對性能有所影響。

共同點

兩者都不允許duplicate key,兩者都不是線程安全的。

本文的例子https://github.com/ddean2009/learn-java-collections

歡迎關注我的公衆號:程序那些事,更多精彩等着您!
更多內容請訪問 www.flydean.com

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