美文网首页
Java集合-HashMap 详解

Java集合-HashMap 详解

作者: 栖风渡 | 来源:发表于2021-12-30 21:18 被阅读0次

    Map

    Map类图.png

    java中的Map是一种可以存放键值对的数据集合,Map中的Key是不可重复的,同时一个Key只能对应一个 Value.

    Map是用来替换Java中的Dictionary,

    Map可以提供三个视图:

    1. 将所有的Key返回为一个Set keySet()
    1. 将所有的Value返回为一个Set valueSet()
    1. 或者将Key-value返回为一个Set
    

    像TreeMap这一类,可以保证元素的存放和获取顺序,但是HashMap并不能保证。

    1. HashMap

    HashMapMap的实现类,同时HashMap 允许使用null作为key,或者valueHashMap大致上跟HashTable是相同的(Hashtable是同步的,并且不允许null

    HashMap是基于哈希表结构,其在没有hash冲突的情况下,进行添加,删除,查找等操作性能是很高的,只需要对指定位置进行一次从操作即可,其时间复杂度为 O(1),

    在HashMap中,其主要的数据存储方式就是数组。 我们通过Hash算法,将当前元(Entry)的关键字通过某一个函数直接映射到数组中的某个位置,通过数组下标一次定位就可以完成操作。

    在HashMap中,我们将上面题导的映射函数称之为 哈希函数,哈希函数的设计,决定了Hash冲突的次数,也就决定了当前HashMap的性能。

    HashMap的基本操作例如 get,put 所需要的时间是固定的,HashMap的Iterator方法跟当前HashMap的容量成正比。 因此如果你想保证迭代器的性能,那么就不能将HashMap的初始容量设置的太大。

    影响HashMap的关键因素:

    1. **initial capacity**     
    1.   **loadFactor**    (初始值和**loadfactory**共同决定了当前**hashMap**的扩容次数)
    1. **key**的**hash**算法      (如果**Key**的**hash**值重复较多,那么也可以直接降低当前**hashmap**的性能)
    

    1.1 HashMap基本原理

    假设我们需要存入两个 <Key ->Value> 元素

    A: <Chen -> henan>

    B: <Wang -> shandong>

    固定哈希算法为 函数f(x), indexA = f(Chen), indexB = f(Wang)

    这样我们得到了A,B两个元素的数组角标,这样就把相应的Entry放入对应数组位置就可以,用图表示可以为:

    Map-hash.png

    1.2hash冲突

    上面说到的hash函数,仅仅是指 将元素的Key转换成 index的算法,有时候我们并不能保证我们使用的hash算法能够保证 不同的键值对元素对应不同的 数组index,这样就有可能出现 hash(Chen) == hash(Wang)的情况,这就是我们说的hash冲突。

    通常情况下解决hash冲突的方法有很多种,例如:开放定址算法(发生冲突,继续寻找下一块未被使用的地址),再散列算法,链地址法,在HashMap中,设计者使用了链地址法,也就是对于冲突的元素,使用链表进行存储

    2 HashMap的实现

    对于HashMap 如何存储键值对数据的呢?

    HashMap在内存中是基于数组形式实现的:

        transient Node<K,V>[] table;   // 内部使用一个数组存储键值对元素
    

    键值对元素的存储格式, 使用Node对键值对进行包装:

     static class Node<K,V> implements Map.Entry<K,V> {
            final int hash; // Node包含当前键值对的hash值
            final K key; //key值
            V value; //value值
            Node<K,V> next; //下一个节点的Node, 当出现hash冲突时使用
    
            Node(int hash, K key, V value, Node<K,V> next) {
                this.hash = hash;
                this.key = key;
                this.value = value;
                this.next = next;
            }
     }
    

    可以推出,一个HashMap的基本形式如下:

    HashMap-结构.png

    当链表数量超过8:

    HashMap-树结构.png

    对于 index0、index3、index7出现了hash碰撞,所以,这个节点存储的node就形成了一个单链表的形式。

    如果通过hash算法定位到的数组位置 没有链表,那么 删除,替换,添加等操作的时间复杂度都是 O(1)

    如果定位到的数组位置有hash冲突,那么这些操作的时间复杂度就为 O(n), n = 链表长度

    3 HashMap源码分析

    下面我们就从HashMap的一些基本操作代码入手,来探究下 HashMap的实现原理。

    3.1 构造方法

    HashMap的两个关键构造因子:

    initial compacity 初始化容量, 这个参数 决定了当前HashMap可以拥有多少个key-value 实体

    loadFactor: 这个值 决定了当前HashMap的 装填程度, 如果当前 容量超过 capacity loadFactor,那么就表示当前HashMap需要进行一次重新扩容,同时需要重新hash*。

    因此,如果想要保证当前HashMap的性能, 适当的Map大小以及加载因子是关键。

    另一个影响HashMap性能的关键就是 Keyhash值,如果有大量Key的hash值是重复的,那么当前HashMap的性能也会降低。

        public HashMap(int initialCapacity, float loadFactor) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: " +
                                                   initialCapacity);
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: " +
                                                   loadFactor);
            this.loadFactor = loadFactor; // hashMap的加载因子,简单的说就是 hashMap可以进行扩容时的容量占比
            this.threshold = tableSizeFor(initialCapacity); //对于给定的容量,hashTable都转换为 相应的2^n.
        }
    

    3.2 HashMap.put

      public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    
    

    关于 hash(key)算法解释参见:

    HashMap的hash() - Black_Knight - 博客园 (cnblogs.com)

    HashMap中的hash函数 - 淡腾的枫 - 博客园 (cnblogs.com)

    可以看到 HashMap.hash确实在兼容性能的基础上做到了尽量减少hash碰撞。


    3.2.1 putVal方法

      final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length; // 参见 3.2.2  创建一个长度为16的数组
            if ((p = tab[i = (n - 1) & hash]) == null) //如果 通过hash值找到的位置没有存放,那么直接创建新的node,并将值放入。
                tab[i] = newNode(hash, key, value, null);
            else { //以下就是处理hash冲突的步骤了。
                Node<K,V> e; K k;
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p; //如果指定hash位置已经存放了Node,并且key的值 相等,那么就直接进行替换
                else if (p instanceof TreeNode) //如果指定结点已经变成了 树,说明这里冲突太多,执行树图的存放操作,数的操作参见 # 4.1.1
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {
                    for (int binCount = 0; ; ++binCount) { //这里开始遍历链表形式。
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash); //链表接入新的结点之后,长度超出阈值,那么就需要将此链表变成树
                            break;
                        }
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k)))) //编辑寻找当前Key的Node,找到就跳出。
                            break;
                        p = e;
                    }
                }
                if (e != null) { // existing mapping for key 只有当链表中有一个已经存在相同Key的node时,走这里,
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e); //这里暂时是空实现
                    return oldValue;
                }
            }
            ++modCount;
            if (++size > threshold) 
                resize(); //检查当前数组的长度,看是否需要进行扩容
            afterNodeInsertion(evict);
            return null;
        }
    

    3.2.2 数组的初始化方法:

        final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length; //第一次调用的话, table为null,
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY; //第一次初始化,默认的容量就是16
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //第一次初始化,扩容阈值就是 16*0.75
            }
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //第一次初始化(不设置容量的情况下),这里就创建一个长度为16的数组
            table = newTab;
            if (oldTab != null) { //第一次,这里不会走
               。。。
            }
            return newTab;
        }
    

    3.3.3 链表长度太长,链表将会变成树

        static final int TREEIFY_THRESHOLD = 8; // 默认链表最长的长度为8
    

    判断是否满足将当前链表变成树的条件:

        final void treeifyBin(Node<K,V>[] tab, int hash) {
            int n, index; Node<K,V> e;
            if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)// 如果数组为空,或者当前数组长度小于 默认长度64,那么就直接进行扩容
                resize();
            else if ((e = tab[index = (n - 1) & hash]) != null) { //指定 hash位置的结点存在
                TreeNode<K,V> hd = null, tl = null;
                do {
                    TreeNode<K,V> p = replacementTreeNode(e, null);
                    if (tl == null)
                        hd = p;
                    else {
                        p.prev = tl;
                        tl.next = p;
                    }
                    tl = p;
                } while ((e = e.next) != null); //到这里做完,还是类似于一个双向链表的形式。
                if ((tab[index] = hd) != null)
                    hd.treeify(tab);//这里的操作就是关键一部了, 将链表变成标准的树结构
            }
        }
    

    将链表变成树结构:

    有关红黑树的介绍:

    【老实李】JDK1.8中HashMap的红黑树 - 简书 (jianshu.com) //这个只是说明白了一小部分

    解读HashMap中的红黑树操作 - 知乎 (zhihu.com) // 这个讲的比较深入。

             final void treeify(Node<K,V>[] tab) {
                TreeNode<K,V> root = null;
                for (TreeNode<K,V> x = this, next; x != null; x = next) { //开始遍历并且格式化之前创建的 树结构
                    next = (TreeNode<K,V>)x.next;
                    x.left = x.right = null; //首先将当前树的左右二叉树置为空
                    if (root == null) { //第一次进行的时候,这里就将第一个作为当前树的跟。
                        x.parent = null;
                        x.red = false; //红黑树根节点必须是黑的
                        root = x;
                    }
                    else {
                        K k = x.key;
                        int h = x.hash;
                        Class<?> kc = null;
                        for (TreeNode<K,V> p = root;;) {
                            int dir, ph;
                            K pk = p.key;
                            if ((ph = p.hash) > h)
                                dir = -1;
                            else if (ph < h)
                                dir = 1;
                            else if ((kc == null &&
                                      (kc = comparableClassFor(k)) == null) ||
                                     (dir = compareComparables(kc, k, pk)) == 0)
                                dir = tieBreakOrder(k, pk);
    
                            TreeNode<K,V> xp = p;
                            if ((p = (dir <= 0) ? p.left : p.right) == null) {
                                x.parent = xp;
                                if (dir <= 0)
                                    xp.left = x;
                                else
                                    xp.right = x;
                                root = balanceInsertion(root, x);
                                break;
                            }
                        }
                    }
                }
                moveRootToFront(tab, root);
            }
    

    以上,分析了HashMap的插入方法,

    1. 第一次存放数据的时候,首先创建一个数组,(默认数组长度为16, 默认加载因子为0.75)
    2. HashMap通过特殊的hash算法尽可能的减少Hash碰撞。 // keyhash值得前16位和16位异或,然后取与当前容量,就是当前节点得index值
    3. 如果出现hash碰撞,那么就将相同 index位置变成一条链表
    4. 如果链表长度较长(>=8),并且当前hashMap得容量超过 64,那么就需要将当前链表变成一个红黑树结构,同时又由于红黑树得自平衡性,可以保证查找删除等操作得时间复杂度在 O(logn)

    3.3 HashMap.remove()

        final Node<K,V> removeNode(int hash, Object key, Object value,
                                   boolean matchValue, boolean movable) {
            Node<K,V>[] tab; Node<K,V> p; int n, index;
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) { //tab不为空并且数组长度>0,
                Node<K,V> node = null, e; K k; V v;
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    node = p;//找到节点
                else if ((e = p.next) != null) {
                    if (p instanceof TreeNode)
                        node = ((TreeNode<K,V>)p).getTreeNode(hash, key);//找到树结构得节点
                    else {
                        do {
                            if (e.hash == hash &&
                                ((k = e.key) == key ||
                                 (key != null && key.equals(k)))) {
                                node = e;//找到链表结构的节点
                                break;
                            }
                            p = e;
                        } while ((e = e.next) != null);
                    }
                }
                if (node != null && (!matchValue || (v = node.value) == value ||
                                     (value != null && value.equals(v)))) {
                    if (node instanceof TreeNode)
                        ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); //树节点需要特殊处理
                    else if (node == p)
                        tab[index] = node.next;//如果是链表的第一个,那么就直接移除
                    else
                        p.next = node.next;//如果是链表中间的一个,那么就删除中间的
                    ++modCount;
                    --size;
                    afterNodeRemoval(node);
                    return node;
                }
            }
            return null;
        }
    
    

    总结

    HashMap在进行数据存储的时候使用了尽可能减少碰撞的hash算法,同时 使用了 数组、链表、红黑树的数据结构,尽可能的将性能和空间进行平衡,这也体现了源码工程师的智慧

    相关文章

      网友评论

          本文标题:Java集合-HashMap 详解

          本文链接:https://www.haomeiwen.com/subject/yedwqrtx.html