HashMap原理剖析一

213 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情

Map继承结构剖析

Map集合的顶级接口是java.util.Map而并没有继承java.util.Collection。Map集合结构属于映射型的结构,一个key键对应一个value值。其主要实现类包括TreeMap,HashMap,LinkedHashMap;我们此次主要讨论我们平时工作中使用最多的HashMap;

java.util.Map接口简介

java.util.Map接口给出了Map体系中的基本操作功能,如下:

  1. 添加一个K-V节点
V put(K key,V value);
  1. 根据K-V键值对中的Key信息,返回Value,如果当前Map集合中没有Key信息,那么将返回null;
V get(Object key);
  1. 获取当前Map集合中的存储的K-V键值对数量
int size();
  1. 判断Map集合中是否至少存在一个K-V键值对节点
boolean isEmpty();
  1. 清空Map中所有的键值对数量
void clear();

java.util.AbstractMap抽象类

java.util.AbstractMap抽象类的作用是为了减少具体Map集合编码工作,降低具体Map集合的实现难度;包括其提供了一些基本的方法,但是这些方法在逻辑上并不能运行,需要依靠子类的实现才可以;这是一种模板方法设计模式的体现,也是适配器设计模式的体现;

HashMap的实现原理

HashMap之所以叫做HashMap,是因为它是对Key的某个属性比如说内存起始位置属性进行hash计算,然后根据计算结构,将当前K-V键值对节点添加到HashMap集合中的某个位置;所以说Hash值的计算是关键;HashMap集合的主要结构包括数组结构,链表结构,红黑树结构;

HashMap中使用HashMap.Node类的对象构建单向链表,以HashMap集合中的数组中的每一个索引位上的数据对象为基础,构建出一个独立的单向链表;当某个索引位上的链表长度达到指定的阈值(默认8),单向链表会转化为红黑树;当红黑树中的节点减少到一定程度(默认6),红黑树会转换为单向链表。

HashMap中的关键属性以及常量

public class HashMap<K,V> extends AbstractMap<K,V>

    implements Map<K,V>, Cloneable, Serializable {

    private static final long serialVersionUID = 362498820763181265L;

 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

 static final int MAXIMUM_CAPACITY = 1 << 30;

 static final float DEFAULT_LOAD_FACTOR = 0.75f;

 static final int TREEIFY_THRESHOLD = 8;

 static final int UNTREEIFY_THRESHOLD = 6;

 static final int MIN_TREEIFY_CAPACITY = 64;

    transient Node<K,V>[] table;

    transient Set<Map.Entry<K,V>> entrySet;
    
    transient int size;

    transient int modCount;

    int threshold;

    final float loadFactor;
}

常量:

DEFAULT_INITIAL_CAPACITY : 代表的是数组默认初始化长度16,容量只能2倍扩容 MAXIMUM_CAPACITY :数组的最大容量 DEFAULT_LOAD_FACTOR :默认的负载因子 TREEIFY_THRESHOLD :树化阈值 UNTREEIFY_THRESHOLD :反树化阈值 MIN_TREEIFY_CAPACITY :只有当集合中的K-V键值对多于这个值,并且单链表大于树化阈值才会进行树化操作

属性方面我们重点讲一下负载因子loadFactor,它维护着集合内K-V键值对的数量和集合中数组大小的平衡,将当前集合容量值与负载因子相乘得到数组下一次扩容操作的K-V键值对节点数量;

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;
    this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

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;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

HashMap的无参构造制定了默认的负载因子loadFactor;从tableSizeFor这个方法我们可以看出,如果我们指定了initialCapacity,那么其只能是比入参cap大且接近2的幂数的值;

链表情况下向HashMap中添加节点

在链表情况下向HashMap中添加节点主要分为下面几个步骤:

  1. 如果当前HashMap集合使用的数组为null或者长度为0,那么需要进行扩容操作
  2. 使用(n-1)& hash 的方式获取数组索引位,如果当前数组索引位没有节点则直接在这个索引位上添加一个新的K-V键值对节点
  3. 如果当前索引位上已经有了K-V键值对,那么可能是链表的头节点,也可能是红黑树的根节点;
  4. 首先判断要添加的K-V键值对的key与当前桶中第一个节点的Key是不是hash相等,并且物理地址相等或者equals相等,如果相等的话则需要进行更新操作;
  5. 如果不相等的话,则判断是不是TreeNode,如果是TreeNode,则说明是红黑树节点
  6. 如果是链表节点,则通过for循环依次遍历当前桶中的单向链表中的每一个节点;在遍历的过程中同样判断要添加的K-V键值对的key与当前桶中第一个节点的Key是不是hash相等,并且物理地址相等或者equals相等,如果相等的话则需要进行更新操作;如果遍历完成没有满足条件的key,那么则需要创建一个新的节点,进行添加操作;并且需要判断是否需要进行树化;

红黑树情况下向HashMap中添加节点

  1. 首先判断要添加的K-V键值对的key与当前桶中第一个节点的Key是不是hash相等,并且物理地址相等或者equals相等,如果相等的话则需要进行更新操作;
  2. 如果没有步骤一的条件的话,就在满足添加位置要求的某个缺失左儿子节点后者右儿子节点的节点处添加新的K-V键值对节点
  3. 红黑树的平衡性如果遭到破坏,那么需要使用红黑树的再平衡算法,重新恢复红黑树的平衡;