Java集合之Map

Map接口概述

前邊對Java單列集合Collection有了基本的瞭解,現在開始學習下集合的Map接口。Map是一個非常有用的數據結構,該接口是一個雙列集合,所謂雙列就是Map是依照鍵(key)-值(value)對的序列來存儲元素,該元素是兩個對象,其中的鍵(key)是唯一的,不能重複,而每個鍵對應的值(value)則不同,value可以重複。

對於Map這種特點在生活中挺常見的,比如,現在每個人都有一個身份證號碼與自己的名字對應,當你在買車票時會直接根據你的身份證號碼來搜索具體的人也就是你的名字,而不是通過你的名字來確定身份證號碼,爲什麼不能用名字直接買票呢,相比大家肯定都能正確的回答,即就是生活中存在許許多多同名同姓的人,但是他們的身份證號碼卻是唯一的,就相當於Map的key,名字就相當於value。

常用的實現類和實現方法

實現類

接下來就看看Map接口的實現類,有好多種實現類,這裏主要說說比較常用的幾種:

  • 1、Hashtable: 
    底層是哈希表數據結構,線程是同步的,不可以存入null鍵,null值。 
    效率較低,被HashMap 替代。
  • 2、HashMap: 
    底層是哈希表數據結構,線程是不同步的,可以存入null鍵,null值。 
    要保證鍵的唯一性,需要覆蓋hashCode方法,和equals方法。 

    LinkedHashMap: 
    該子類基於哈希表又融入了鏈表。可以Map集合進行增刪提高效率。

  • 3、TreeMap: 
    底層是二叉樹數據結構。可以對map集合中的鍵進行排序。需要使用Comparable或者Comparator 進行比較排序。return 0,來判斷鍵的唯一性。

Map接口的所有實現類都會有兩個標準的構造方法:創建一個空映射的無參構造方法和創建一個與其參數具有相同鍵值映射關係的新映射

常用的方法

  • 1、添加元素: 
        V put(K key, V value): (可以相同的key值,但是添加的value值會覆蓋前面的,返回值是前一個,如果沒有就返回null) 
        putAll(Map m): 從指定映射中將所有映射關係複製到此映射中(可選操作)。
  • 2、刪除元素 
        remove() 刪除關聯對象,指定key對象 
        clear() 清空集合對象
  • 3、獲取元素 
        value get(key);可以用於判斷鍵是否存在的情況。當指定的鍵不存在的時候,返回的是null。
  • 4、對元素進行判斷: 
        boolean isEmpty() 長度爲0返回true否則false 
        boolean containsKey(Object key) 判斷集合中是否包含指定的key 
        boolean containsValue(Object value) 判斷集合中是否包含指定的value
  • 5、獲取集合長度: 
        Int size()

瞭解了Map接口的常用實現類和方法,接下來就可以做一些簡單的代碼測試了,創建HashMap實現類創建一個對象,使用以上五種常用方法進行測試:

  1. public class Test{
  2. public static void main(String[] args) {
  3. Map<Integer,String> mp = new HashMap<Integer,String>();
  4. mp.put(1,"aaa"); //添加成功返回null
  5. mp.put(2,"bbb");
  6. System.out.println("ccc添加返回值:"+mp.put(3,"ccc"));
  7. //mp.put(test, 3); //這種肯定是會報錯的,因爲創建mp對象時就已經使用泛型<>指定了鍵值對的類型爲Integer-String
  8. mp.remove(2); //根據鍵key刪除元素
  9. System.out.println(mp);
  10. String var = mp.get(3); //鍵對應的值存在則返回,不存在返回null
  11. System.out.println("獲取鍵值爲3元素的value:"+var);
  12. System.out.println("map集合的長度爲:"+mp.size());
  13. boolean bl = mp.isEmpty();
  14. System.out.println("mp集合是否爲空:"+bl);
  15. mp.clear();
  16. System.out.println("mp集合是否爲空:"+mp.isEmpty());
  17. }
  18. }

Map接口實現類的迭代方式

Map接口提供了三種collection視圖,允許以鍵集、值集或者鍵值對映射關係集來查看集合中的具體元素。對於這三種方式分別對應三個方法實現:

  • 1.keySet() 以鍵集的視圖呈現Map集合元素,返回Set集合類型
  • 2.values() 值集,返回Collection集合類型
  • 3.entrySet() 返回鍵值映射關係的Set視圖(三種方法更多信息請查看API文檔)

接下來,通過實例來了解這三種迭代方法;

  1. //第一種遍歷:keySet,鍵集
  2. Set<Integer> keys = mp.keySet();
  3. Iterator<Integer> sitr = keys.iterator();
  4. while(sitr.hasNext()){
  5. Integer key = sitr.next();
  6. System.out.println("mp集合的鍵爲:"+key);
  7. //雖然keySet方法只能獲取鍵集,但還是可以使用get(key)方法獲取對應的值
  8. System.out.println("mp集合的值爲:"+mp.get(key));
  9. }
  10. //第二種遍歷value,這種方法只能迭代集合的值集不能獲取到鍵
  11. Collection<String> values = mp.values();
  12. Iterator<String> citr = values.iterator();
  13. while(citr.hasNext()){
  14. String str = citr.next();
  15. System.out.println("集合的值爲:"+str);
  16. }
  17. //第三種遍歷方法:entrySet,鍵-值對
  18. Set<Map.Entry<Integer,String>> entry = mp.entrySet();
  19. Iterator<Map.Entry<Integer,String>> eitr = entry.iterator();
  20. while(eitr.hasNext()){
  21. Entry<Integer,String> ee = eitr.next();
  22. System.out.println("集合的鍵爲:"+ee.getKey());
  23. System.out.println("集合的值爲:"+ee.getValue());
  24. }
  25. //以上幾種在JDK 5後還可以採用for-each循環遍歷形式
  26. Map<String, Integer> hashmap = new HashMap<String, Integer>();
  27. for(Map.Entry<String, Integer> map : hashmap.entrySet()){
  28. System.out.println("key="+map.getKey()+" value="+map.getValue());
  29. }

Map實現類具體分析

HashMap實現類

HashMap實現類內部是基於數據結構哈希表實現,出現於JDK1.2版本,並且可以允許null的鍵值,是非線程同步的。想要深入學習該實現類,就得先了解哈希表的基本原理。

hash表

大家都知道數據結構數組和鏈表,數組是查找容易、插入和刪除困難;鏈表則是插入、刪除容易,查找困難。而這個哈希表就是集數組和鏈表結構的優點於一身的一種數據結構。Hash表採用一個映射函數f:key->value將關鍵字映射到該記錄在表中的位置,從而在想要查找該條記錄時,可以直接根據關鍵字和映射關係(Hash函數)計算出其在表中的存儲位置(也就是Hash地址)。 
常用的Hash函數構造方法有以下幾種: 
1.直接定址法、2.平方取中法、3.摺疊法、4.除留取餘法 
對於上邊四種方法,很容易產生衝突,即就是不同的關鍵字經過Hash函數處理得到的Hash地址可能一樣,造成混亂,因而爲了應對這種衝突情況自然有相應的解決方法,比如有:a.開放定址法;b.鏈地址法 
哈希表有多種這種解決衝突的方法,這裏說一種常用的鏈地址法(拉鍊法) 

 
如圖所示,鏈地址法可以理解爲一個鏈表的數組形式,左邊是數組形式,每個數組的元素是一個鏈表。具體原理可以這樣理解,就是將關鍵字採用相應的Hash函數(比如除留取餘法)處理得到相應的Hash地址,然後在存儲空間中尋址若不存在則直接依據數組結構進行存儲存儲,倘若該Hash地址已存在,則在數據相應Hash地址位開闢一個鏈表結構一次存儲相同的Hash地址。

哈希表的內容還有很多,這裏主要就說這麼多,明白了這些對於HashMap實現類的原理理解也就容易多了。

HashMap存儲原理

先來看看HashMap實現類的默認構造方法,根據參數(容量長度和增長因子)的不通分爲以下四種:

  1. public HashMap() //採用默認容量(16)和增長因子(0.75)
  2. public HashMap(int initialCapacity) //指定集合長度
  3. public HashMap(int initialCapacity, float loadFactor) //指定集合長度和增長因子
  4. public HashMap(Map<? extends K, ? extends V> m) // 構造一個映射關係與指定 Map 相同的新 HashMap,容量和增長因子均默認值

容量長度即就是集合的長度空間大小;增長因子是用於在集合空間不夠用時增大集合容量的增長率。

  1. static final int DEFAULT_INITIAL_CAPACITY = 16;
  2. static final float DEFAULT_LOAD_FACTOR = 0.75f;
  3. /**
  4. * Constructs an empty <tt>HashMap</tt> with the default initial capacity (16) and the default load factor (0.75).
  5. */
  6. public HashMap() {
  7. this.loadFactor = DEFAULT_LOAD_FACTOR;
  8. threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
  9. table = new Entry[DEFAULT_INITIAL_CAPACITY];
  10. init();
  11. }

HashMap類的元素存儲主要是使用put方法實現,想要搞清楚其存儲原理,就可以從put方法的實現代碼中去研究學習。接下來就看看put的實現代碼:

  1. public V put(K key, V value) {
  2. if (key == null)
  3. return putForNullKey(value);
  4. int hash = hash(key.hashCode());
  5. int i = indexFor(hash, table.length);
  6. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  7. Object k;
  8. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  9. V oldValue = e.value;
  10. e.value = value;
  11. e.recordAccess(this);
  12. return oldValue;
  13. }
  14. }
  15. modCount++;
  16. addEntry(hash, key, value, i);
  17. return null;
  18. }

如代碼所示,往HashMap添加元素的時候,首先會判斷key值是否爲null,若爲空則調用putForNullKey方法:

  1. private V putForNullKey(V value) {
  2. for (Entry<K,V> e = table[0]; e != null; e = e.next) {
  3. if (e.key == null) {
  4. V oldValue = e.value;
  5. e.value = value;
  6. e.recordAccess(this);
  7. return oldValue;
  8. }
  9. }
  10. modCount++;
  11. addEntry(0, null, value, 0);
  12. return null;
  13. }

將其放置在數組的第一個鏈表中(for循環table[0]),若存在null則使用新value更新原有value,否則調用addEntry方法;回到上一步,key不爲null,調用hash計算鍵key的哈希表碼值,根據hash碼值和哈希表table長度計算將其放入數組的第幾個鏈表(也就是數組的索引),此時存在兩種情況: 

       情況1:如果算出的位置目前已經存在其他的鍵key,那麼還會調用該鍵的equals方法與這個位置上的鍵進行比較,如果equals方法返回的是false,那麼該鍵允許被存儲,如果equals方法返回的是true,那麼該鍵被視爲重複,不允存儲,但是會將該鍵對應的值value存入,更新舊的value。 

       情況2: 如果算出的位置目前沒有任何元素存儲,那麼該鍵key可以直接添加到哈希表中,調用addEntry方法:

rehash操作

  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2. if ((size >= threshold) && (null != table[bucketIndex])) {
  3. resize(2 * table.length);
  4. hash = (null != key) ? hash(key) : 0;
  5. bucketIndex = indexFor(hash, table.length);
  6. }
  7. createEntry(hash, key, value, bucketIndex);
  8. }

當HashMap集合的大小size大於等於閾值(默認容量16和加載因子0.75的乘機),並且table[bucketIndex]不爲null時,就會發生ReHash操作,也就是達到了容量上限需要擴容了,主要發生在resize方法中:

  1. void resize(int newCapacity) {
  2. Entry[] oldTable = table;
  3. int oldCapacity = oldTable.length;
  4. if (oldCapacity == MAXIMUM_CAPACITY) {
  5. threshold = Integer.MAX_VALUE;
  6. return;
  7. }
  8. Entry[] newTable = new Entry[newCapacity];
  9. boolean oldAltHashing = useAltHashing;
  10. useAltHashing |= sun.misc.VM.isBooted() &&
  11. (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
  12. boolean rehash = oldAltHashing ^ useAltHashing;
  13. transfer(newTable, rehash);
  14. table = newTable;
  15. threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  16. }

若原來的容量大小已經是MAXIMUM_CAPACITY(2^30),則將閾值threshold設置爲整數的最大值Integer.MAX_VALUE((2^31)-1);否則創建一張新表,調用transfer方法將當前表中的所有entries拷貝到新表中。

以上就是HashMap集合的一些基本原理。接下來看看和HashMap類似的HashTable實現類。

HashTable實現類

HashTable實現類是在JDK1.0版本出現的,底層實現同HashMap一樣都是基於哈希表的。HashTable不允許null鍵和值,是線程同步的,在JDK1.2版本時被HashMap取代,兩者主要區別就是:線程安全性,同步(synchronization),以及速度。

在單線程時,由於HashTable是線程安全的,性能肯定是不如HashMap的。而且HashMap的迭代器是快速失敗的(fail-fast),當在迭代時有其他線程改變了HashMap的結構(增加或移除元素,除過迭代器本身的remove方法),就會拋出ConsurrentModificationException異常。

對於HashMap不是線程安全的,在JDK 5時加入了ConcurrentHashMap,可以支持多個併發線程。

TreeMap實現類

HashMap實現類底層是基於哈希表數據結構實現(可以理解爲數組和鏈表的合體)。而TreeMap實現類底層是基於紅黑樹(二叉樹)數據結構實現的,往TreeMap添加元素的時候,如果元素的鍵具備自然順序或者創建映射時提供了Comparator接口,那麼就會按照鍵的這兩種特性進行排序存儲。 
基於樹實現必然會牽扯到左右子樹節點的定義,下來先看看TreeMap類的節點是如何定義的:

  1. static final class Entry<K,V> implements Map.Entry<K,V> {
  2. K key;
  3. V value;
  4. Entry<K,V> left = null; //左子樹
  5. Entry<K,V> right = null; //右子樹
  6. Entry<K,V> parent; //父節點
  7. boolean color = BLACK;
  8. Entry(K key, V value, Entry<K,V> parent) {
  9. this.key = key;
  10. this.value = value;
  11. this.parent = parent;
  12. }
  13. public K getKey() {
  14. return key;
  15. }
  16. /**
  17. * Returns the value associated with the key
  18. * @return the value associated with the key
  19. */
  20. public V getValue() {
  21. return value;
  22. }
  23. /**
  24. * 該方法功能是當新插入的節點的key和當前節點的key相等時,會
  25. * 以新插入key對應的value1更新當前節點key對應的value2,然後將該value2返回
  26. */
  27. public V setValue(V value) {
  28. V oldValue = this.value;
  29. this.value = value;
  30. return oldValue;
  31. }
  32. public boolean equals(Object o) {
  33. if (!(o instanceof Map.Entry))
  34. return false;
  35. Map.Entry<?,?> e = (Map.Entry<?,?>)o;
  36. return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
  37. }
  38. public int hashCode() {
  39. int keyHash = (key==null ? 0 : key.hashCode());
  40. int valueHash = (value==null ? 0 : value.hashCode());
  41. return keyHash ^ valueHash;
  42. }
  43. public String toString() {
  44. return key + "=" + value;
  45. }
  46. }

TreeMap實現的對象中添加節點信息,如果插入的節點的鍵key已存在,則會更新舊鍵的value值,並返回被替換的value,否則put方法返回null

    1. public V put(K key, V value) {
    2. Entry<K,V> t = root;
    3. if (t == null) { //當前節點爲空則直接以插入鍵值對新建節點
    4. compare(key, key); // type (and possibly null) check
    5. root = new Entry<>(key, value, null);
    6. size = 1;
    7. modCount++;
    8. return null;
    9. }
    10. int cmp;
    11. Entry<K,V> parent;
    12. // split comparator and comparable paths
    13. Comparator<? super K> cpr = comparator;
    14. if (cpr != null) { //Comparator比較器不爲空
    15. do {
    16. parent = t;
    17. cmp = cpr.compare(key, t.key);
    18. if (cmp < 0) //當前節點鍵大於插入節點的鍵,繼續遍歷當前節點的左孩子
    19. t = t.left;
    20. else if (cmp > 0) //小於,遍歷右孩子
    21. t = t.right;
    22. else
    23. return t.setValue(value); //鍵已存在,更新value值
    24. } while (t != null);
    25. }
    26. else {//比較器爲空,採用Comparable接口的comparaTo方法比較
    27. if (key == null)
    28. throw new NullPointerException();
    29. Comparable<? super K> k = (Comparable<? super K>) key;
    30. do {
    31. parent = t;
    32. cmp = k.compareTo(t.key);
    33. if (cmp < 0) //當前key大於插入key,繼續遍歷左子樹
    34. t = t.left;
    35. else if (cmp > 0) //當前key小於插入key,遍歷右子樹
    36. t = t.right;
    37. else
    38. return t.setValue(value);
    39. } while (t != null);
    40. }
    41. //將所有子樹全部遍歷爲空後(即就是到葉子節點),則將插入節點作爲parent的子節點
    42. Entry<K,V> e = new Entry<>(key, value, parent);
    43. if (cmp < 0)
    44. parent.left = e;
    45. else
    46. parent.right = e;
    47. fixAfterInsertion(e); //插入新節點,調用fixAfterInsertion方法調整紅黑樹
    48. size++;
    49. modCount++;
    50. return null;
    51. }

fixAfterInsertion方法調整紅黑樹主要由三個方法setColor(設置顏色)rotateLeft(左旋)、rotateRight(右旋)實現,這也是紅黑樹的核心操作,對於這部分更爲詳細的介紹可以參看文章http://www.cnblogs.com/chenssy/p/3746600.html 

TreeMap添加不具備自然順序的元素和自定義Comparator接口 
接下來看看測試代碼,繼續深入瞭解下,鍵值對象不具有自然順序時,鍵所屬的類沒有實現Comparable接口,也沒有在創建TreeMap對象的時候傳入比較器時,運行代碼時就會報錯:

  1. class staff{
  2. int id;
  3. String name;
  4. staff(int id,String name) {
  5. super();
  6. this.id = id;
  7. this.name = name;
  8. }
  9. @Override
  10. public String toString() {
  11. return "["+this.id+","+this.name+"]";
  12. }
  13. }
  14. public class TreeMap_test {
  15. public static void main(final String[] args) {
  16. Map<staff,String> map = new TreeMap<staff,String>();
  17. map.put(new staff(2,"張三"), "語文");
  18. map.put(new staff(1,"李四"), "數學");
  19. map.put(new staff(5,"王五"), "英語");
  20. System.out.println(map);
  21. }
  22. }

程序中因爲鍵值是自定義staff的類型,並沒有自然順序,在輸入鍵值時調用默認的compare方法就無法進行比較,因而運行程序時會報錯如下:

  1. Exception in thread "main" java.lang.ClassCastException: staff cannot be cast to java.lang.Comparable
  2. at java.util.TreeMap.compare(Unknown Source)
  3. at java.util.TreeMap.put(Unknown Source)
  4. at TreeMap_test.main(TreeMap_test.java:25)

因而在使用TreeMap添加元素時應該注意如下兩點:

  • 1.往TreeMap添加元素的時候,如果元素的鍵不具備自然順序特性, 那麼鍵所屬的類必須要實現Comparable接口,把鍵的比較規則定義在CompareTo方法上;
  • 2.往TreeMap添加元素的時候,如果元素的鍵不具備自然順序特性,而且鍵所屬的類也沒有實現Comparable接口,那麼就必須在創建TreeMap對象的時候傳入比較器。
  1. //1.鍵所屬的類實現Comparable接口
  2. class staff implements Comparable<staff>{
  3. @Override
  4. public int compareTo(staff o) {
  5. return this.id-o.id;
  6. }
  7. }
  8. //2.自定義Comparator接口,在創建TreeMap對象時傳入比較器
  9. class Mycomparetor implements Comparator<staff> {
  10. @Override
  11. public int compare(staff o1,staff o2) {
  12. return o1.id - o2.id;
  13. }
  14. }
  15. public class TreeMap_test {
  16. public static void main(final String[] args) {
  17. Mycomparetor comparetor = new Mycomparetor();
  18. Map<staff,String> map = new TreeMap<staff,String>(comparetor);
  19. }
  20. }

運行程序就會按鍵中的id排序:

  1. {[1,李四]=數學, [2,張三]=語文, [5,王五]=英語}

以上就是自己學習Java集合之Map接口的一些總結,雖說不是很深入,但是從不懂到編寫基本的集合代碼肯定沒有問題,後邊還得繼續加油。。。

發佈了45 篇原創文章 · 獲贊 17 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章