• 《Java-SE-第十八章》之HashMap(jdk8)


    前言

    在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!”

    博客主页:KC老衲爱尼姑的博客主页

    博主的github,平常所写代码皆在于此

    共勉:talk is cheap, show me the code

    作者是爪哇岛的新手,水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!

    Map

    Map是一种以键值对(key-value)进行存储的集合,Map集中的每一个元素都包含一个 键(key) 对象 和 一个值(value)对象。其其特点都是由键来决定的,Map集合的键都是无序,不重复,无索引,Map集合后面重复的键对应的值会覆盖前的重复键的值,并且键和值都允许为空。

    HashMap

    HashMap概述

    HashMap是基于哈希表的Map接口实现的,该类存储的结构的键值对,并且k是唯一的,不能重复,而v作为值可以重复。

    数据结构

    在这里插入图片描述

    HashMap属性

    1.序列化ID

        private static final long serialVersionUID = 362498820763181265L;
    
    • 1

    2.HaahMap对象被创建时,初始的默认容量是16(1<<4)。

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    
    • 1

    3.HashMap的最大容量是2的30次方。

      static final int MAXIMUM_CAPACITY = 1 << 30;
    
    • 1

    4.HashMap默认的负载因子为0.75。

     static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    • 1

    5.当HashMap底层数组中某一位置的链表长度等于8,该链表便满足了转换成红黑树的条件。

     static final int TREEIFY_THRESHOLD = 8;
    
    • 1

    6.HashMap底层数组长度等于64时,链表长度等于8时,链表就会转换成红黑树。

    static final int MIN_TREEIFY_CAPACITY = 64;
    
    • 1

    7.当链表已经转换成红黑树时,当树节点少于6便会退化成链表。

     static final int UNTREEIFY_THRESHOLD = 6;
    
    • 1

    8.HashMap底层是一个链表数组

    transient Node<K,V>[] table;
    
    • 1

    9.该集合存储的就是Node节点,便于遍历使用

     transient Set<Map.Entry<K,V>> entrySet;
    
    • 1

    10.记录数组中节点的个数

     transient int size;
    
    • 1

    11.记录HashMap修改的次数

      transient int modCount;
    
    • 1

    12.threshold是阈值,为了减少哈希冲突,HashMap底层的数组不是等到存储空间都被利用完之后才扩容,而是根据当前的负载因子和数组长度计算出一个阈值,当超过该阈值就进行扩容。

     int threshold;
    
    • 1

    13.该属性也是负载因子,仅仅是一个成员变量,便于在方法中使用。

    final float loadFactor;
    
    • 1

    构造方法

    使用HashMap的无参构造方法构造HashMap时,其默认的初始化容量是16,负载因子为0.75。

    源码:

        public HashMap() {
            this.loadFactor = DEFAULT_LOAD_FACTOR; //负载因子为0.75
        }
    
    • 1
    • 2
    • 3

    HashMap还提供了三个带参构造方法,本质上可以视作两个。因为HashMap(int initialCapacity)方法体中实际调用的是HashMap(int initialCapacity, float loadFactor),使用带参构造指定初始化容量时,最终的初始化容量不一定是一开始指定的,初始化容量必须是2的次幂。tableSizeFor(initialCapacity)会对传入的容量进行调整,最终的调整的结果是等于或者大于指定容量的一个最接近2的次幂数。

      //initialCapacity和loadFactor分别为创建HashMap时指定的容量和负载因子
      public HashMap(int initialCapacity, float loadFactor) {
            //容量小于0抛出异常
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: " +  initialCapacity);
          	//容量大于数组最大容量时,将initialCapacity调整为MAXIMUM_CAPACITY
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
           //如果指定的负载因子小于等于0,或者负载因子是非数字则抛出异常
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: " +
                                                   loadFactor);
          	//进行负载因子初始化
            this.loadFactor = loadFactor;
           //
            this.threshold = tableSizeFor(initialCapacity);
        }
    
        //创建HashMap指定容量initialCapacity
        public HashMap(int initialCapacity) {
            this(initialCapacity, DEFAULT_LOAD_FACTOR);//调用上面的有参构造
        }
    
      
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    根其它Map集合构造hashMap

    如果传入的集合中有元素,在添加元素成功之前就会开辟好内存,如果该集合没有元素,就还是不会开辟内存。

    	//创建一个HashMap并将m中的元素存入其中 
        public HashMap(Map<? extends K, ? extends V> m) {
            this.loadFactor = DEFAULT_LOAD_FACTOR;
            putMapEntries(m, false);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    获得一个大于cap又是最接近cap的2的整数次幂数值

      static final int tableSizeFor(int cap) {
            int n = cap - 1;
            n |= n >>> 1;
            n |= n >>> 2;
            n |= n >>> 4;
            n |= n >>> 8;
            n |= n >>> 16;
          //判断n是否小于0,如果小于0则返回1,否则就继续判断是否大于最大容量,是的话就返回最大容量,不是则返回n+1。
            return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    综上所述,创建HashMap对象时,只是确定好了初始化容量以及负载因子,底层的数组并没有分配内存。只有当添加元素时才会给数组分配内存。

    核心方法

    HashMap真正的分配内配内存,是在添加元素时。

    1.put()

    源码:

     public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    //该方法根据key计算出该节点应该插入到数组的哪一个下标
     static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
     
     //插入元素
     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            //tab为哈希数组,p为节点,n为数组长度,i为数组下标
            Node<K,V>[] tab; Node<K,V> p; int n, i;
           //判断table的引用是否为空以及table数组的长度,为null或者为0说明需要扩容
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;//将扩容之后的数组长度赋值给n
         	//如果数组的i位置没有值,就将传入的key-value插入即可,如果插入成功则返回null。如果插入的节点已经存在则返回这个节点。
            if ((p = tab[i = (n - 1) & hash]) == null)//p为该索引位置的链表的第一个节点
                tab[i] = newNode(hash, key, value, null);//插入节点
            else {
                //发生哈希冲突的情况
                //定义临时变量
                Node<K,V> e; K k;
                //如果当前索引位置对应的链表第一个元素的hash与准备添加的key的hash相同
                //并且满足一下两个条件之一
                //1.准备加入的key与p.key相同
                //2.p指向的Node节点的key的equals()与准备加入的key比较后相同(如自定义类型作为key)
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                //判断是否是红黑树的节点
                else if (p instanceof TreeNode)
                    //添加到红黑树,如果该节点在红黑树中存在则返回该节点,不存在则返回null
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {
                    //hash不为链表首元素,不是红黑树的节点,就是链表中的节点,遍历链表,依次把该元素与链表中的每个元素比较后,都不相同则加到末尾
                    for (int binCount = 0; ; ++binCount) {
                        if ((e = p.next) == null) {//如果遍历到链表末尾说明已经没有重复的节点,此时直接添加到链表的末尾
                            p.next = newNode(hash, key, value, null);
    					   //如果链表长度已经达到了8个节点,就会进行树化
                            if (binCount >= TREEIFY_THRESHOLD - 1) 
                                treeifyBin(tab, hash);
                            break;
                        }
                        //在遍历的过程中找到了重复的节点,直接break,e为重复的节点
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                //有重复的key()旧节点和插入的节点
                if (e != null) { 
                    V oldValue = e.value;//拿到旧节点的value
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;//将待插入节点的value更新到旧节点
                    afterNodeAccess(e);
                    return oldValue;//返回
                }
            }
            ++modCount;
            if (++size > threshold)//添加成功size首先++,再和阈值进行判断,大于就要扩容
                resize();
            afterNodeInsertion(evict);
            return null;//成功添加
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    注意

    链表树化需要满足连个添加,第一是链表长度已经到了8,并且数组长度要大于等于64。如果仅仅是链表长度满足了添加,调用 treeifyBin(tab, hash)去树化,实际还是对数组进行了扩容。

    源码如下:

    在这里插入图片描述

    添加逻辑
    1. 首先根据key计算出哈希值
    2. 判断table是否为null或者为长度为0,满足就扩容
    3. 计算出数组的下标,并判断该位置是否为null
    4. 如果table[i]位置为null插入即可
    5. 不为空说明发生了哈希冲突,再判断插入的节点与当前位置节点的key是否相同
    6. 如果相同直接覆盖元素
    7. 如果不是,在判断是否是树节点
    8. 如果是树节点,红黑树插入
    9. 如果不是,直接遍历链表寻找是否存在相同的key
    10. 如果存在直接覆盖节点
    11. 不存在将该节点添加到链表末尾
    12. 接着判断链表长度是否达到了8,达到了尝试树化
    13. 最后对容量判断是否大于了阈值,大于了进行扩容。

    2.resize()

    final Node<K,V>[] resize() {
            //将数组引用赋值该oldTab
            Node<K,V>[] oldTab = table;
            //得到得到当前数组长度
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
           //旧阈值
            int oldThr = threshold;
            //newCap为数组新的容量,newThr为新的数组长度,newThr为新的阈值
            int newCap, newThr = 0;
        	//判断是需要初始化数组还是扩容,如果oldCap小于或者等于0则表示需要初始化数组,大于0 则需要扩容
            if (oldCap > 0) {
                //oldCap大于数组的最大容量则需要重置大小
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                //将数组扩容到到原来2倍,阈值也扩大原来的2倍
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
            //说明调用的是有参构造方法,理由无参构造没有对threshold进行初始化
            else if (oldThr > 0) 
                newCap = oldThr;
            else { 
                //无参构造初始化数组
                newCap = DEFAULT_INITIAL_CAPACITY;
                //计算阈值
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
        	//如果阈值为空需要重新计算阈值
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
        	//将计算出来的阈值赋值给成员变量 threshold
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
             //扩容成功后,需要将旧数组的元素搬运到新数组去
             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;//更新数组引用
            //判断旧数组是否有元素,有的话就开始搬运
            if (oldTab != null) {
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;
                        if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        else { // preserve order
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;//返回新数组的引用
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    扩容逻辑
    1. 首先为初始化和扩容准备好所需的变量
    2. 根据当前数组的长度(oldCap)进行判断数组是需要扩容还是初始化
    3. 如果oldCap大于0说明是需要扩容,将数组长度以及阈值都扩充到原来的2倍
    4. 如果oldCap小于等于0则说明数组需要初始化,
    5. 根据oldThr判读是调用那个构造方法进行初始化,然后为其分配初始容量以及负载因子
    6. 然后对新计算出来的阈值进行检查,如果为0则需要重新计算
    7. 最后如果是扩容,则将源数组的元素搬运到目标数组去。

    3.get()

     public V get(Object key) {
            Node<K,V> e;
           //调用getNode()方法来完成
            return (e = getNode(hash(key), key)) == null ? null : e.value;
        }
        
        final Node<K,V> getNode(int hash, Object key) {
    		//table为数组,first头节点,n是数组长度
            Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
            //判断数组是否null并且已经开辟空间了,同时得到计算出索引位置的节点(first)
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
                //如果该索引位置的头节点就是要找到的节点直接返回
                if (first.hash == hash && 
                    ((k = first.key) == key || (key != null && key.equals(k))))
                    return first;
                //不是头节点
                if ((e = first.next) != null) {
                    //判断第一个节点是否是红黑树的节点
                    if (first instanceof TreeNode)
                        //进入红黑树茶查找
                        return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                    do {
                        //不是红黑树节点,就是链表节点遍历链表查找
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            return e;
                    } while ((e = e.next) != null);
                }
            }
            //找不到该节点
            return null;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    查找逻辑

    1. 根据key计算出哈希值
    2. 判断数组是否为null,长度是否为0,如果是就直接返回null
    3. 如果不是,计算出数组的下标,判断该位置的节点的key是否和要查找的key相同,一致直接返回
    4. 不相同,判断第一个节点是否是红黑树节点,如果是则去红黑树里去查找
    5. 如果不是树节点,说明是链表节点,遍历链表即可。找到就返回,还没找到返回null。

    遍历

    1.键找值的方式的遍历

    代码示例

    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;
    
    /**
     * @author 929KC
     * @date 2022/11/5 15:57
     * @description:
     */
    public class Demo {
        public static void main(String[] args) {
            HashMap<String,Integer> map = new HashMap<>();
            map.put("a",1);
            map.put("b",2);
            map.put("c",3);
            // 1、键找值:第一步:先拿到集合的全部键。
            Set<String> key = map.keySet();
            // 2、第二步:遍历每个键,根据键提取值
            for (String s : key) {
                int  value = map.get(s);
                System.out.println(s+" "+value);
            }
        }
    }
    
    //a 1
    //b 2
    //c 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    2.键值对的方式遍历

    代码示例

    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;
    
    /**
     * @author 929KC
     * @date 2022/11/5 15:57
     * @description:
     */
    public class Demo {
        public static void main(String[] args) {
            HashMap<String,Integer> map = new HashMap<>();
            map.put("a",1);
            map.put("b",2);
            map.put("c",3);
            Set<Map.Entry<String, Integer>> entries = map.entrySet();
            for (Map.Entry<String, Integer> entry : entries) {
                Integer value = entry.getValue();
                String key = entry.getKey();
                System.out.println(key+" "+value);
            }
        }
    }
    //a 1
    //b 2
    //c 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    3.forEach遍历

    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;
    import java.util.function.BiConsumer;
    
    /**
     * @author 929KC
     * @date 2022/11/5 15:57
     * @description:
     */
    public class Demo {
        public static void main(String[] args) {
            HashMap<String,Integer> map = new HashMap<>();
            map.put("a",1);
            map.put("b",2);
            map.put("c",3);
            map.forEach(new BiConsumer<String, Integer>() {
                @Override
                public void accept(String key, Integer value) {
                    System.out.println(key+" "+value);
                }
            });
        }
    }
    //a 1
    //b 2
    //c 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27


    在这里插入图片描述

  • 相关阅读:
    add_new _device_point添加权限
    基于 Vagrant 手动部署多个 Redis Server
    云呐|固定资产管理系统功能包括哪些?
    nodejs项目实例拼车租车平台
    Android NfcManager 之NFC接入
    【Git】git stash报错 Too many revisions specified: ‘stash@‘ ‘MAA=‘ ‘xml‘ ‘text‘
    Snagit 2024:让你的屏幕活动瞬间变得生动有力 mac/win版
    Python-序列、集合和字典
    Qtday1
    导航专业入门,高考/考研假期预习指南
  • 原文地址:https://blog.csdn.net/qq_60308100/article/details/127729149