天天看點

7. Map接口及其實作類Map接口及其實作類☆

文章目錄

  • Map接口及其實作類
    • 1. Map接口介紹
    • 2. Map接口中常用方法
      • 2.1 元素的添加 、 删除、修改操作
      • 2.2 元素查詢的操作
      • 2.3 周遊Map的三種方式☆
    • 3. Map接口的實作類
      • 3.1 HashMap集合
          • HashMap源碼中的重要常量☆
          • HashMap的儲存結構☆☆
            • JDK7
            • JDK8
            • 小結
          • HashMap存儲自定義類型鍵值
            • Student.java
            • 測試類HashMapTest
          • LinkedHashMap集合☆
            • LinkedHashMap的靜态内部類Entry
      • 3.2 TreeMap集合
      • 3.3 HashTable集合
          • Properties類

Map接口及其實作類

1. Map接口介紹

  1. 現實生活中,我們常會看到這樣的一種集合:

    IP

    位址與主機名,身份證号與個人,系統使用者名與系統使用者對象等,這種

    一一對應

    的關系,就叫做映射。

    Java

    提供了專門的集合類用來存放這種對象關系的對象,即

    java.util.Map

    接口。
  2. Map與Collection并列存在。用于儲存具有

    映射關系的資料:key-value。

  3. Map 中的 key 和 value 都可以是任何引用類型的資料
  4. Map 中的 key 用Set來存放, 不允許重複,即同一個 Map 對象所對應的類,須重寫hashCode()和equals()方法

    • 常用String類作為Map的“鍵”。

    • 7. Map接口及其實作類Map接口及其實作類☆
  5. key 和 value 之間存在單向一對一關系,即通過指定的 key 總能找到唯一的、确定的 value。
  6. Map接口中的集合都有兩個泛型變量,在使用時,要為兩個泛型變量賦予資料類型。兩個泛型變量的資料類型可以相同,也可以不同。
  7. Map接口的常用實作類:

    HashMap

    TreeMap

    LinkedHashMap

    Properties

    其中HashMap是 Map 接口使用頻率最高的實作類

7. Map接口及其實作類Map接口及其實作類☆

2. Map接口中常用方法

2.1 元素的添加 、 删除、修改操作

  1. Object put(Object key,Object value):

    将指定key-value

    添加到(或修改)

    目前map對象中。
  2. void putAll(Map m):

    将m中的所有key-value對存放到目前map中。
  3. Object remove(Object key):

    移除指定key的key-value對,并傳回value。
  4. void clear():

    清空目前map中的所有資料。

2.2 元素查詢的操作

  1. Object get(Object key):

    擷取指定key對應的value。
  2. boolean containsKey(Object key):

    是否包含指定的key。
  3. boolean containsValue(Object value):

    是否包含指定的value。
  4. int size():

    傳回map中key-value對的個數。
  5. boolean isEmpty():

    判斷目前map是否為空。
  6. boolean equals(Object obj):

    判斷目前mp和參數對象obj是否相等。

2.3 周遊Map的三種方式☆

public Set<K> keySet()

: 擷取Map集合中所有的鍵,存儲到Set集合中。
  • 擷取

    Map

    中所有的鍵,由于鍵是唯一的,是以傳回一個

    Set

    集合存儲所有的鍵。方法提示:

    keyset()

  • 周遊鍵的

    Set

    集合,得到每一個鍵。
  • 根據鍵,擷取鍵所對應的值。方法提示:

    get(K key)

Collection values():

傳回所有value構成的Collection集合

public Set<Map.Entry<K,V>> entrySet()

: 擷取到Map集合中所有的鍵值對對象的集合(Set集合)。
  • 我們已經知道,

    Map

    中存放的是兩種對象,一種稱為key(鍵),一種稱為

    value

    (值),它們在在

    Map

    中是一一對應關系,這一對對象又稱做

    Map

    中的一個

    Entry

    (項) 。

    Entry

    将鍵值對的對應關系封裝成了對象。即鍵值對對象,這樣我們在周遊

    Map

    集合時,就可以從每一個鍵值對(

    Entry

    )對象中擷取對應的鍵與對應的值。既然

    Entry

    表示了一對鍵和值,那麼也同樣提供了擷取對應鍵和對應值得方法:
  • Map集合不能直接使用疊代器或者foreach進行周遊。但是轉成Set之後就可以使用了。
    • public K getKey() :擷取Entry對象中的鍵。
    • public V getValue() :擷取Entry對象中的值。
/**
 * @Date 2020/11/13 17:29
 * @Version 10.21
 * @Author DuanChaojie
 */
public class TestMap {
    public static void main(String[] args) {
        // 集合初始化時,指定集合初始值大小
        // 說明:HashMap使用如下構造方法進行初始化,如果暫時無法确定集合大小,那麼指定預設值(16)即可
        Map map = new HashMap();
        //map.put(..,..)省略
        
        System.out.println("map的所有key:");
        // HashSet
        Set keys = map.keySet();
        for (Object key : keys) {
            System.out.println(key + "->" + map.get(key));
        }
        
        System.out.println("map的所有的value:");
        Collection values = map.values();
        Iterator iter = values.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
        
        System.out.println("map所有的映射關系:");
        // 映射關系的類型是Map.Entry類型,它是Map接口的内部接口
        Set mappings = map.entrySet();
        for (Object mapping : mappings) {
            Map.Entry entry = (Map.Entry) mapping;
            System.out.println("key是:" + entry.getKey() + ",value是:" + entry.getValue());
        }
    }
}
           

3. Map接口的實作類

3.1 HashMap集合

  1. HashMap是 Map 接口 使用頻率最高的實作類。

  2. 允許使用null鍵和null值,與HashSet一樣,不保證映射的順序。
  3. 所有的key構成的集合是Set:無序的、不可重複的。是以,key所在的類要重寫:equals()和hashCode()。

  4. 所有的value構成的集合是Collection:無序的、可以重複的。

    是以,value所在的類要重寫:equals()。

  5. 一個key-value構成一個entry
  6. 所有的entry構成的集合是Set:無序的、不可重複的
  7. HashMap

    判斷兩個 key 相等的标準是

    :兩個 key 通過 equals() 方法傳回 true,hashCode 值也相等。
    • 因為key不允許有重複。
  8. HashMap

    判斷兩個 value 相等的标準是

    :兩個 value 通過 equals() 方法傳回 true。
HashMap源碼中的重要常量☆
  1. DEFAULT_INITIAL_CAPACITY :

    HashMap的預設容量,16
  2. MAXIMUM_CAPACITY :

    HashMap的最大支援容量,2^30
  3. DEFAULT_LOAD_FACTOR :

    HashMap的預設加載因子0.75
  4. TREEIFY_THRESHOLD :

    Bucket中連結清單長度大于該預設值8,轉化為紅黑樹
  5. UNTREEIFY_THRESHOLD :

    Bucket中紅黑樹存儲的Node小于該預設值6,轉化為連結清單。
  6. MIN_TREEIFY_CAPACITY :

    桶中的Node被樹化時最小的hash表容量。(當桶中Node的數量大到需要變紅黑樹時,若hash表容量小于MIN_TREEIFY_CAPACITY時,此時應執行resize擴容操作這個MIN_TREEIFY_CAPACITY的值

    (預設為64)

    至少是TREEIFY_THRESHOLD的4倍。)
  7. table :

    存儲元素的數組,總是2的n次幂
  8. entrySet:

    存儲具體元素的集
  9. size :

    HashMap中存儲的鍵值對的數量
  10. modCount :

    HashMap擴容和結構改變的次數。
  11. threshold :

    擴容的臨界值(吞吐臨界值)12=容量*填充因子
  12. loadFactor:

    填充比(負載因子)。
    1. 負載因子值的大小,對HashMap有什麼影響?
      • 負載因子的大小決定了HashMap的資料密度。
      • 負載因子越大密度越大,發生碰撞的幾率越高,數組中的連結清單越容易長,造成查詢或插入時的比較次數增多,性能會下降。
      • 負載因子越小,就越容易觸發擴容,資料密度也越小,意味着發生碰撞的幾率越小,數組中的連結清單也就越短,查詢和插入時比較的次數也越小,性能會更高。但是會浪費一定的内容空間。而且經常擴容也會影響性能,建議初始化預設大一點的空間。
      • 按照其他語言的參考及研究經驗,會考慮将負載因子設定為0.7~0.75,此時平均檢索長度接近于常數。
HashMap的儲存結構☆☆
  1. JDK 7及以前版本:HashMap是數組+連結清單結構(即為鍊位址法)。
  2. JDK 8版本釋出以後:HashMap是數組+連結清單+紅黑樹實作。

JDK7

7. Map接口及其實作類Map接口及其實作類☆
  1. HashMap的内部存儲結構其實是 數組和連結清單的結合。當執行個體化一個HashMap時,系統會建立一個長度為Capacity的Entry數組,這個長度在哈希表中被稱為容量(Capacity),

    在這個數組中可以存放元素的位置我們稱之為“桶”(bucket),

    每個bucket都有自己的索引,系統可以根據索引快速的查找bucket中的元素。
  2. 每個bucket中存儲一個元素,即一個Entry對象

    ,但每一個Entry對象可以帶一個引用變量,用于指向下一個元素,是以,在一個桶中,就有可能生成一個Entry鍊。而且新添加的元素作為連結清單的head。
  3. HashMap靜态内部類Entry

    static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            final int hash;
            Entry<K,V> next;
         	// ....
     }
               
  4. 添加元素的過程:
    1. 向HashMap中添加

      entry1(key,value)

      ,需要首先計算entry1中key的哈希值(

      根據key所在類的hashCode()計算得到

      ),此哈希值經過處理以後,得到在底層Entry[]數組中要

      存儲的位置i

    2. 如果位置i上沒有元素,則entry1直接添加成功。如果位置i上已經存在entry2(或還有連結清單存在的entry3,entry4),則需要通過循環的方法,依次比較entry1中key和其他的entry。如果彼此hash值不同,則直接添加成功。
    3. 如果hash值不同,繼續比較二者是否equals。如果傳回值為true,則使用entry1的value

      去替換equals為true的entry的value。

    4. 如果周遊一遍以後,發現所有的equals傳回都為false,則entry1仍可添加成功。entry1指向原有的entry元素。
  5. HashMap 的擴容:
    1. 當HashMap中的元素越來越多的時候,hash沖突的幾率也就越來越高,因為數組的長度是固定的。是以為了提高查詢的效率,就要對HashMap的數組進行擴容,

      而在HashMap數組擴容之後,最消耗性能的點就出現了:原數組中的資料必須重新計算其在新數組中的位置,并放進去,這就是resize。

    2. 那麼HashMap 什麼時候進行擴容呢 ?
      1. 當HashMap中的元素個數超過數組大小(數組總大小length,不是數組中個數size)loadFactor 時 , 就 會 進 行 數 組 擴 容 , loadFactor 的 默 認 值(DEFAULT_LOAD_FACTOR)為0.75,這是一個折中的取值。
      2. 也就是說,預設情況下,數組大小(DEFAULT_INITIAL_CAPACITY)為16,那麼當HashMap中元素個數超過

        16*0.75=12

        (這個值就是代碼中的threshold值,也叫做臨界值)的時候,就把數組的大小擴充為 2*16=32,即擴大一倍,然後重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,

        是以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的性能。

JDK8

7. Map接口及其實作類Map接口及其實作類☆
  1. HashMap的内部存儲結構其實是

    數組+ 連結清單+ 樹

    的結合。當執行個體化一個HashMap時,會初始化initialCapacity和loadFactor,在put第一對映射關系時,系統會建立一個長度為initialCapacity的

    Node數組

    ,這個長度在哈希表中被稱為容量(Capacity),在這個數組中可以存放元素的位置我們稱之為

    “桶”(bucket),每個bucket都有自己的索引,系統可以根據索引快速的查找bucket中的元素。

  2. 每個bucket中存儲一個元素,即一個Node對象,

    但每一個Node對象可以帶一個引用變量next,用于指向下一個元素,

    是以,在一個桶中,就有可能生成一個Node鍊。也可能是一個一個TreeNode對象,

    每一個TreeNode對象可以有兩個葉子結點left和right,是以,在一個桶中,就有可能生成一個TreeNode樹。

    而新添加的元素作為連結清單的last,或樹的葉子結點。

  3. HashMap的靜态内部類Node
    static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            V value;
            Node<K,V> next;
    
            Node(int hash, K key, V value, Node<K,V> next) {
                this.hash = hash;
                this.key = key;
                this.value = value;
                this.next = next;
            }
    
            public final K getKey()        { return key; }
            public final V getValue()      { return value; }
            public final String toString() { return key + "=" + value; }
    
            public final int hashCode() {
                return Objects.hashCode(key) ^ Objects.hashCode(value);
            }
    
            public final V setValue(V newValue) {
                V oldValue = value;
                value = newValue;
                return oldValue;
            }
    
            public final boolean equals(Object o) {
                if (o == this)
                    return true;
                if (o instanceof Map.Entry) {
                    Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                    if (Objects.equals(key, e.getKey()) &&
                        Objects.equals(value, e.getValue()))
                        return true;
                }
                return false;
            }
        }
               
  4. 那麼HashMap 什麼時候進行擴容和樹形化呢 ?
    1. 當HashMap中的元素個數超過數組大小(數組總大小length,不是數組中個數size)loadFactor 時 , 就會 進 行 數 組 擴 容 , loadFactor 的 默 認 值(DEFAULT_LOAD_FACTOR)為0.75,這是一個折中的取值。也就是說,預設情況下,數組大小(DEFAULT_INITIAL_CAPACITY)為16,那麼當HashMap中元素個數超過

      16*0.75=12

      (這個值就是代碼中的threshold值,也叫做臨界值)的時候,就把數組的大小擴充為 2*16=32,即擴大一倍,然後重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,是以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的性能。
    2. 當HashMap中的其中一個鍊的對象個數如果達到了8個,此時如果capacity沒有達到64,那麼HashMap會先擴容解決,如果已經達到了64,那麼這個鍊會變成樹,結點類型由Node變成TreeNode類型。當然,如果當映射關系被移除後,下次resize方法時判斷樹的結點個數低于6個,也會把樹再轉為連結清單。

      • 如果不了解這段,可以看一下HashMap源碼中的重要常量

        MIN_TREEIFY_CAPACITY

  5. 關于映射關系的key 是否可以修改 ?
    • 答案是:不要修改!
    • 映射關系存儲到HashMap中會存儲key的hash值,這樣就不用在每次查找時重新計算每一個Entry或Node(TreeNode)的hash值了,是以如果已經put到Map中的映射關系,再修改key的屬性,而這個屬性又參與hashcode值的計算,那麼會導緻比對不上。

小結

JDK1.8相較于之前的變化:

  1. HashMap map = new HashMap();//預設情況下,先不建立長度為16的數組。
  2. 當首次調用map.put()時,再建立長度為16的數組。

  3. 數組為Node類型,在jdk7中稱為Entry類型。

  4. 形成連結清單結構時,新添加的key-value對在連結清單的尾部(七上八下)。

  5. 當數組指定索引位置的連結清單長度>8時,且map中的數組的長度> 64時,此索引位置上的所有key-value對使用紅黑樹進行存儲。

HashMap存儲自定義類型鍵值
  1. 每位學生(姓名,年齡)都有自己的家庭住址。那麼,既然有對應關系,則将學生對象和家庭住址存儲到

    map

    集合中。學生作為鍵, 家庭住址作為值。
  2. 注意,學生姓名相同并且年齡相同視為同一名學生。

Student.java

public class Student {
    private String name;
    private int age;
    public Student() {
    }
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
           

測試類HashMapTest

public class HashMapTest {
    
    public static void main(String[] args) {
        //1,建立Hashmap集合對象。
        Map<Student,String>map = new HashMap<Student,String>();
        
        //2,添加元素。
        map.put(newStudent("lisi",28), "上海");
        map.put(newStudent("wangwu",22), "北京");
        map.put(newStudent("zhaoliu",24), "成都");
        map.put(newStudent("zhouqi",25),"廣州");
        map.put(newStudent("wangwu",22), "南京");
        
        //3,取出元素。鍵找值方式
        Set<Student>keySet = map.keySet();
        for(Student key: keySet){
            Stringvalue = map.get(key);
            System.out.println(key.toString()+"....."+value);
        }
    }
}
           
  1. 當給

    HashMap

    中存放自定義對象時,如果自定義對象作為

    key

    存在,這時要保證對象唯一,必須複寫對象的

    hashCode

    equals

    方法(如果忘記,請回顧

    HashSet

    存放自定義對象)。
  2. 如果要保證

    map

    中存放的

    key

    和取出的順序一緻,可以使用

    java.util.LinkedHashMap

    集合來存放。
LinkedHashMap集合☆
  1. 我們知道HashMap保證成對元素唯一,并且查詢速度很快,可是成對元素存放進去是沒有順序的,那麼我們要保證有序,還要速度快怎麼辦呢?
  2. LinkedHashMap 是 HashMap 的子類,它是連結清單和哈希表組合的一個資料存儲結構。
public class LinkedHashMapDemo {
    public static void main(String[] args) {
        LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
        map.put("鄧超", "孫俪");
        map.put("李晨", "範冰冰");
        map.put("劉德華", "朱麗倩");
        Set<Entry<String, String>> entrySet = map.entrySet();
        
        for (Entry<String, String> entry : entrySet) {
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }
}
           
結果:
鄧超 孫俪
李晨 範冰冰
劉德華 朱麗倩
           

LinkedHashMap的靜态内部類Entry

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}
           

3.2 TreeMap集合

  1. TreeMap存儲 Key-Value 對時,需要根據 key-value 對進行排序。TreeMap 可以保證所有的 Key-Value 對處于有序狀态。
  2. TreeSet底層使用 紅黑樹 結構存儲資料。
  3. TreeMap 的 Key 的排序:
    • 自然排序:

      TreeMap 的所有的 Key 必須實作 Comparable 接口,而且所有的 Key 應該是同一個類的對象,否則将會抛出 ClasssCastException。
    • 定制排序:

      建立 TreeMap 時,傳入一個 Comparator 對象,該對象負責對TreeMap 中的所有 key 進行排序。此時不需要 Map 的 Key 實作Comparable 接口
  4. TreeMap判斷兩個key 相等的标準:兩個key通過compareTo()方法或者compare()方法傳回0。

3.3 HashTable集合

  1. HashTable是個古老的 Map 實作類,JDK1.0就提供了。

    不同于HashMap,Hashtable是線程安全的。

  2. Hashtable實作原理和HashMap相同,功能相同。底層都使用哈希表結構,查詢速度快,很多情況下可以互用。
  3. 與HashMap不同,Hashtable 不允許使用 null 作為 key 和 value

  4. 與HashMap一樣,Hashtable 也不能保證其中 Key-Value 對的順序
  5. Hashtable判斷兩個key相等、兩個value相等的标準,與HashMap一緻。
Properties類
  1. Properties 類是 Hashtable 的子類,該對象用于處理屬性檔案由于屬性檔案裡的 key、value 都是字元串類型,是以 Properties 裡的 key和 value 都是字元串類型
  2. 存取資料時,建議使用

    setProperty(String key,String value)方法

    getProperty(String key)方法。

  3. Properties pros = new Properties();
    pros.load(new FileInputStream("jdbc.properties"));
    String user = pros.getProperty("user");
    System.out.println(user);