HashMap之分析(二)

70 阅读2分钟

一、HashMap的类定义

public class HashMap<K,V>
         extends AbstractMap<K,V> 
         implements Map<K,V>, Cloneable, Serializable

1.继承了AbstractMap抽象类。实现了Map接口 (拥有Map的基本操作)
2.实现了Cloneable接口(表示可以进行拷贝,浅拷贝。即引用数据类型的修改会影响源数据)
3.实现了Serializable接口(表示可以进行本地化存储和读取)

二、HashMap的特点

1.允许key和value为null
2.非线程安全
3.不保证有序(如插入顺序)

三、HashMap的数据结构

JDK1.8对HashMap进行了比较大的优化,底层数据结构实现由之前的“数组+链表”改为“数组+单项链表+红黑树”。
当链表节点较少时仍然是以链表存在,当节点大于较多时(大于8)会转为红黑树。

image.png

关于红黑树与链表之间的转换条件:

链表越来越长时会考虑二叉树中的红黑树

当链表>8时,会转为红黑树;

当红黑树的节点个数 <= 6 时红黑树转为链表结构

死循环问题:

JDK1.7之前是头插法, 有弊端:性能不好,容易形成死循环
JDK1.8是尾查法       避免死循环
JDK 1.8 以前,Java 语言在并发情况下使用 HashMap 造成 Race Condition,从而导致死循环。程序经常占了 100% 的 CPU,查看堆栈,卡在了 “HashMap.get()” 这个方法上了,重启程序后问题消失。
JDK 1.8 以前,导致死循环的主要原因是扩容后,节点的顺序会反掉.如下图:扩容前节点 A 在节点 C 前面,而扩容后节点 C 在节点 A 前面。

hashmap扩容.png

四、HashMap的存储流程

hashMap存储流程.png

4.1 数组元素 & 链表节点的 实现类

  • HashMap中的数组元素 & 链表节点 采用 Node类 实现 ,与 JDK 1.7 的对比(Entry类),只是换了名字

源码如下

/** 
  * HashMap的内部类,实现了Map.Entry接口,本质是一个映射(键值对)
  * 实现了getKey()、getValue()、equals()、hashCode()等方法
  **/  
  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 V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

      /** 
        * 作用:判断2个Entry是否相等,必须key和value都相等,才返回true  
        */
        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.2 红黑树节点 实现类

  • HashMap中的红黑树节点 采用 TreeNode 类 实现
/**
 * 红黑树节点 实现类:继承自LinkedHashMap.Entry<K,V>类
 */
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {  

    TreeNode<K,V> parent;  // 父节点
    TreeNode<K,V> left;    // 左子树
    TreeNode<K,V> right;   // 右子树
    TreeNode<K,V> prev;    // 删除辅助节点 
    boolean red;   

    TreeNode(int hash, K key, V val, Node<K,V> next) {  
        super(hash, key, val, next);  
    }  
  
    // 返回当前节点的根节点  
    final TreeNode<K,V> root() {  
        for (TreeNode<K,V> r = this, p;;) {  
            if ((p = r.parent) == null)  
                return r;  
            r = p;  
        }  
    } 

五、HashMap的使用

  1. 遍历HashMap
  • 注:对于遍历方式,推荐使用针对 key-value对(Entry)的方式:效率高
  • 原因:
      1. 对于遍历keySet 、valueSet,实质上遍历了2次:第一次转为 iterator 迭代器遍历、第二次从 HashMap 中取出 key 的 value 操作(通过 key 值 hashCode 和 equals 索引)
      1. 对于遍历 entrySet ,实质遍历了1次,获取存储实体Entry(存储了key 和 value )
      Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
      for(Map.Entry<String, Integer> entry : entrySet){
              System.out.print(entry.getKey());
              System.out.println(entry.getValue());
      }