【当年笔记】集合之Map

Map 常用的实现类如下:

  • Hashtable :Java 早期hash类,线程安全,不支持 null 键和值,因为它的性能不如 ConcurrentHashMap,所以基本不用。
  • HashMap :最常用的hash实现类,线程不安全,支持 null 键和值,多线程中可用 ConcurrentHashMap 替代。
  • TreeMap :基于红黑树的有序 Map,自身实现了按 key 排序,也可以指定 Comparator 来自定义排序。
  • LinkedHashMap :HashMap 的一个子类,按插入顺序排序

HashMap 数据结构

数组+链表+树的方式存储数据,HashMap 底层的数据是数组被成为哈希桶,每个桶存放的是链表,链表中的每个节点,就是 HashMap 中的每个元素。在 JDK 8 当链表长度大于等于8 时,就会转成红黑树的数据结构,以提升查询和插入的效率。

HashMap put方法源码分析

        // 对 key 进行 hash()
        return putVal(hash(key), key, value, false, true);
    }
    static final int hash(Object key) {
        int h;
      // 对 key 进行 hash() 的具体实现
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // tab为空则创建
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 计算 index,并对 null 做处理
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 节点存在 对比key的hash值并调用equals
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 判断是否已树化后的链表
            else if (p instanceof TreeNode)
                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))))
                        break;
                    p = e;
                }
            }
            // 写入
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 超过load factor*current capacity, 调用扩容方法resize
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

image

get方法


    public V get(Object key) {
      Node<K,V> e;
      return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

/**
    * @param hash 通过hash(key)得到的 hash 值,再根据 hash 值在节点数组中寻址
    * @param key key 对象,当存在 hash 碰撞时,要逐个比对链表的值是否与传入的值相等
    * @return 查找到则返回键值对节点对象,否则返回 null
    */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 声明节点数组对象、链表的第一个节点对象、循环遍历时遍历到的节点对象、数组长度、节点的键对象
        // 获取节点数组、数组长度、通过位运算得到求模结果确定链表的首节点
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // 首先比对首节点,如果首节点的 hash 值和 key 的 hash 值相同,并且首节点的键对象和 key 相同(同一内存地址或 equals 结果为true),则返回该节点
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first; // 返回首节点
            // 如果首节点比对不相同、那么看看是否存在下一个节点,如果存在的话,可以继续比对,如果不存在就意味着 key 无对应的值 
            if ((e = first.next) != null) {
                // 如果存在下一个节点 e,那么先看首节点是否是个树节点
                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)))) // 比对时还是先看 hash 值是否相同、再看地址或 equals
                        return e; // 如果当前节点e的键对象和key相同,那么返回 e
                } while ((e = e.next) != null); // 看看是否还有下一个节点,如果有,继续下一轮比对,否则跳出循环
            }
        }
        return null; // 在比对完所有节点后 无匹配的 key,则返回null

HashMap 在 JDK 7 多线程中使用会导致什么问题?

答:HashMap 在 JDK 7 中会导致死循环的问题。因为在 JDK 7 中,多线程进行 HashMap 扩容时会导致链表的循环引用,这个时候使用
get() 获取元素时就会导致死循环,造成 CPU 100% 的情况。

HashMap 在 JDK 7 和 JDK 8 中有哪些不同?

  • 存储结构:JDK 7 使用的是数组 + 链表;JDK 8 使用的是数组 + 链表 + 红黑树。
  • 存放数据的规则:JDK 7 无冲突时,存放数组;冲突时,存放链表;JDK 8 在没有冲突的情况下直接存放数组,有冲突时,当链表长度小于 8 时,存放在单链表结构中,当链表长度大于 8 时,树化并存放至红黑树的数据结构中。
  • 插入数据方式:JDK 7 使用的是头插法(先将原位置的数据移到后 1 位,再插入数据到该位置);JDK 8 使用的是尾插法(直接插入到链表尾部/红黑树)。
posted @ 2022-06-09 17:52  正文儿  阅读(39)  评论(0编辑  收藏  举报