JDK源码分析hashmap

 

HashMap是什么?

  1.  
    /**
  2.  
    * 基于Map接口实现,允许null值和null键。
  3.  
    * HashMap和HashTable很相似,只是HashTable是同步的,以及不能为null的键
  4.  
    * HashMap有两个重要参数,capacity和load factor 默认的load factor大小为0.75
  5.  
    * iterator是fail-fast的。
  6.  
    *
  7.  
    */
  8.  
    public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>,
  9.  
    Cloneable, Serializable {

如上,基本的特性在代码里面注释了,HashMap实现了Map接口,是一个基于散列表的Map类,Map接口的特性就是存储键值对。散列表是一种存储结构,它可以通过散列函数直接访问到目标数据值,所以在定位下标方面可认为为o(1)。

1  HashMap继承自AbstractMap类同时实现了Cloneable,Serializable这两个接口,其中前一个接口Cloneable是为了实现clonet()机制,Serializable接口是为了实现序列化机制,关于这两种机制的相关知识再此不做赘述。

2   HashMap用到了泛型来实现参数化类型,其实java中的全部集合框架都使用到了泛型。


HashMap重要的字段

  1.  
    /**
  2.  
    * Hash的默认大小
  3.  
    */
  4.  
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  5.  
     
  6.  
    /**
  7.  
    * HashMap最大存储容量
  8.  
    */
  9.  
    static final int MAXIMUM_CAPACITY = 1 << 30;
  10.  
     
  11.  
    /**
  12.  
    * 增长因子,意思就是当table已经用到table.length*0.75时,就需要扩容
  13.  
    */
  14.  
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
  15.  
     
  16.  
    /**
  17.  
    * 由链表存储转变为由树存储的门限,最少是8
  18.  
    */
  19.  
    static final int TREEIFY_THRESHOLD = 8;
  20.  
     
  21.  
    /**
  22.  
    * 由树存储节点转化为树的节点,默认是6,即从8到6时,重新转化为链表存储
  23.  
    */
  24.  
    static final int UNTREEIFY_THRESHOLD = 6;
  25.  
     
  26.  
    /**
  27.  
    * 当由链表转为树时候,此时Hash表的最小容量。 也就是如果没有到64的话,就会进行resize的扩容操作。
  28.  
    * 这个值最小要是TREEIFY_THRESHOLD的4倍。
  29.  
    */
  30.  
    static final int MIN_TREEIFY_CAPACITY = 64;

上述代码中解释了HashMap中重要字段的意思,相信大家一看就会有大概理解了。

由于在Java8的实现中,当经过hash函数计算得出的下标地址冲突到一定范围时,就会 把冲突的数据用链表的形式连起来,而当用链表数据大于一定范围时,就会将链表转化为红黑树存储。 

 

HashMap结构概括

首先HashMap会有一个基准数组table:

    1.  
      /**
    2.  
      * 存储数据的table集合,长度一定为2的倍数
    3.  
      */
    4.  
      transient Node<K, V>[] table; //用来存储数据的数组,每个元素都是Node即链表

其中transient关键字是用来让该域在整个类被序列化的时候不包含该内容,即该域不被序列化

重点说一下static final float DEFAULT_LOAD_FACTOR = 0.75f;这个属性,该属性即为装载因子,本质上就是我们学习数据结构中解决Hash冲突的填充因子的意思,它的默认值是0.75,如果实际元素所占容量占分配容量的75%时就需要扩容。

第一步,table是一个数组,所以会有下标,HashMap首先会根据传入每个节点的(key,value)中的key,算出应该放到哪一个下标的数组中。 

第二步,如果此下标数组为null,那么就直接放入,不为null,就走到第三步。 
第三步,如果不为null,就说明冲突了,检查key的equals方法,看是否和原节点的key相同,相同就直接替换,否则进入第四步。 
第四步,很明显冲突了,而且是不相等的冲突,这是检查是否需要将此下标的存储结构换为红黑树,不需要就是链表直接在末尾插入节点,否则进入第五步。 
第五步,原有的链表结构不足以支撑存储了,所以换为红黑树存储了,此时就是往红黑树中插入该节点。 
上述步骤省略了链表与红黑树之间转换。 
整个存储结构图如下(没有放入红黑树存储结构)(省略了value值) 

这里写图片描述

 

HashMap的内部实现原理:

我们来看一下HashMap的构造器

  1.  
    public HashMap(int initialCapacity, float loadFactor) {
  2.  
    if (initialCapacity < 0)
  3.  
    throw new IllegalArgumentException("Illegal initial capacity: " +
  4.  
    initialCapacity);
  5.  
    if (initialCapacity > MAXIMUM_CAPACITY)
  6.  
    initialCapacity = MAXIMUM_CAPACITY;
  7.  
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
  8.  
    throw new IllegalArgumentException("Illegal load factor: " +
  9.  
    loadFactor);
  10.  
    this.loadFactor = loadFactor;
  11.  
    this.threshold = tableSizeFor(initialCapacity);
  12.  
    }
  13.  
     
  14.  
     
  15.  
    public HashMap(int initialCapacity) {
  16.  
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
  17.  
    }
  18.  
     
  19.  
     
  20.  
    public HashMap() {
  21.  
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
  22.  
    }
  23.  
     
  24.  
     
  25.  
    public HashMap(Map<? extends K, ? extends V> m) {
  26.  
    this.loadFactor = DEFAULT_LOAD_FACTOR;
  27.  
    putMapEntries(m, false);
  28.  
    }

 

可以看到java设计者们重载了4个HashMap的构造器,重点关注一下tableSizeFor(),putMapEntries这两个函数,我们来看一下tableSizeFor的源码:

 

Table的容量只能是2的倍数

table容量为2的倍数时,有利于下一步计算table的下标,

另一个方面,虽然在HashMap中,提供了一个构造方法:

public HashMap(int initialCapacity, float loadFactor) 

看似提供了初始容量的方法,但是这个方法最后一行代码中调用了另一个方法tableSizeFor来确定table的容量:

tableSizeFor方法保证  函数返回值是>=给定参数initialCapacity最小的2的幂次方的数值。

  1.  
    /**
  2.  
    * hashMap大小只能为map的倍数。 最终会返回一个最适合cap的2的倍数
  3.  
    * capacity.
  4.  
    */
  5.  
    static final int tableSizeFor(int cap) {
  6.  
    int n = cap - 1;
  7.  
    n |= n >>> 1;
  8.  
    n |= n >>> 2;
  9.  
    n |= n >>> 4;
  10.  
    n |= n >>> 8;
  11.  
    n |= n >>> 16;
  12.  
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  13.  
    }
解释:a|=b------a=a|b   二进制位或

n |= n >>> 16

  • n继续无符号右移16位。
  • n | (n >>> 16),导致n二进制表示高17~32位经过运算值均为1

    目前n的高1~32位均为1

无论给定cap(cap < MAXIMUM_CAPACITY )的值是多少,经过以上运算,其值的二进制所有位都会是1。再将其加1,这时候这个值一定是2的幂次方。当然如果经过运算值大于MAXIMUM_CAPACITY,直接选用MAXIMUM_CAPACITY
 

所以最终table的length  即后来会提到的n   只能是2的倍数。



hash值的计算方法

在HashMap中要注意区分hashCode和hash两个方法

  1.  
    static final int hash(Object key) { //高低16位进行异或操作
  2.  
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  3.  
    }

hashCode返回的是一个32位的2进制数值。

 

 

table下标计算方法    

HashMap中存储数据tableindex是由keyHash值决定的。在HashMap存储数据的时候,我们期望数据能够均匀分布,以避免哈希冲突。自然而然我们就会想到去用%取余的操作来实现我们这一构想。

取余(%)操作中 如果除数是2的幂次方则等同于与其除数减一的与(&)操作。

 

tab[index] =tab[(n - 1) & e.hash]     //即等同于index=e.hash % n; 

其中hash=hash(key),n=table.length

由于n2的幂次方,那么n- 1的高位应该全部为0。如果e.hash值只用自身的hashcode的话,那么index只会和e.hash低位做&操作。这样一来,index的值就只有低位参与运算,高位毫无存在感,从而会带来哈希冲突的风险。
所以在计算key的哈希值的时候,用其自身hashcode值与其低16位做异或操作。这也就让高位参与到index的计算中来了,即降低了哈希冲突的风险又不会带来太大的性能问题。

最终会截 取到hash的后log(n)-1位,会得到一个范围在0~table.length的值,这个值,就是数组的下标。是不是很有艺术。

由于hash是由key的hashCode的高16位与低16位经过异或而得,混合了原始哈希码的高低位,大大的提升了随机性,也让碰撞机率大大降低。

综上可以看出为什么Table容量只能是2的倍数~~~

n为2的整数幂保证了n-1最后一位(当然是二进制表示)为1,从而保证了取索引操作 h&(n-1)的最后一位同时有为0和为1的可能性,保证了散列的均匀性。反过来讲,当Hash表长度n为奇数时,n-1最后一位为0,这样与h按位与的最后一位肯定为0,即索引位置肯定是偶数,这样数组的奇数位置全部没有放置元素,浪费了大量空间。

总之:n为2的幂保证了按位与 最后一位的有效性,使哈希表散列更均匀。

 

 

接着我们来看一下:putMapEntries这个函数:

  1.  
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
  2.  
    int s = m.size();
  3.  
    if (s > 0) {
  4.  
    if (table == null) { // pre-size
  5.  
    float ft = ((float)s / loadFactor) + 1.0F;
  6.  
    int t = ((ft < (float)MAXIMUM_CAPACITY) ?
  7.  
    (int)ft : MAXIMUM_CAPACITY);
  8.  
    if (t > threshold)
  9.  
    threshold = tableSizeFor(t); //确定table容量
  10.  
    }
  11.  
    else if (s > threshold)
  12.  
    resize(); //当容量超过阈值时候需要扩容
  13.  
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
  14.  
    K key = e.getKey();
  15.  
    V value = e.getValue();
  16.  
    putVal(hash(key), key, value, false, evict); //建表存链表节点
  17.  
    }
  18.  
    }
  19.  
    }

首先获得传入的map实例的大小s,然后存在一个将大小s与临界值比较的过程

如果map实例的 > threshold  ,则调用resize()方法,即扩容,我们来看一下resize()的源码:

  1.  
    final Node<K,V>[] resize() {
  2.  
          Node<K,V>[] oldTab = table;                    //定义了一个临时数组oldTab来保存table 
  3.  
    int oldCap = (oldTab == null) ? 0 : oldTab.length; //获取该table的大小oldCap 
  4.  
    int oldThr = threshold;
  5.  
    int newCap, newThr = 0;
  6.  
    if (oldCap > 0) {
  7.  
    if (oldCap >= MAXIMUM_CAPACITY) {         //如果oldCap的值> MAXIMUM_CAPACITY(即HashMap所允许的最大容量1>>30),则无法扩容
  8.  
    threshold = Integer.MAX_VALUE;      //只能更改 threshold(即扩容的临界值)的值
  9.  
    return oldTab;
  10.  
    }                                         //否则 newCap = oldCap << 1,即令新的容量为原来的2倍
  1.  
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 且oldCap >=DEFAULT_INITIAL_CAPACITY(从上面HashMap中重要成员属性这块可以看到值为16)
  2.  
    newThr = oldThr << 1;                            // 则将新的临界值也更改为原来的2倍,即newThr = oldThr << 1;
  1.  
    }
  2.  
    else if (oldThr > 0) // initial capacity was placed in threshold
  3.  
    newCap = oldThr;
  4.  
    else { // zero initial threshold signifies using defaults
  5.  
    newCap = DEFAULT_INITIAL_CAPACITY;
  6.  
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  7.  
    }
  8.  
    if (newThr == 0) {
  9.  
    float ft = (float)newCap * loadFactor;
  10.  
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  11.  
    (int)ft : Integer.MAX_VALUE);
  12.  
    }
  13.  
    threshold = newThr;
  14.  
    @SuppressWarnings({"rawtypes","unchecked"})
  1.  
            /*下面开始构造新表,初始化表中的数据*/
  2.  
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //构建新表
  3.  
    table = newTab;                                    //把新表赋值给table
  1.  
    if (oldTab != null) {
  2.  
    // oldTab 复制到 newTab
  3.  
    for (int j = 0; j < oldCap; ++j) {
  4.  
    Node<K,V> e;
  5.  
    if ((e = oldTab[j]) != null) {
  6.  
    oldTab[j] = null;
  7.  
    if (e.next == null)
  8.  
    // 链表 只有一个节点,直接赋值
  9.  
    newTab[e.hash & (newCap - 1)] = e;
  10.  
    else if (e instanceof TreeNode)
  11.  
    // e 为红黑树的情况
  12.  
    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  13.  
    else { /*如果e后边有链表,到这里表示e后面带着个单链表,需要遍历单链表*/ // preserve order
  14.  
    Node<K,V> loHead = null, loTail = null; //新表是旧表的两倍容量,实例上就把单链表拆分为两队,
  15.  
    Node<K,V> hiHead = null, hiTail = null; //e.hash&oldCap为偶数一队,e.hash&oldCap为奇数一对
  16.  
    Node<K,V> next;
  17.  
    do {
  18.  
    next = e.next;
  19.  
    if ((e.hash & oldCap) == 0) {
  20.  
    if (loTail == null)
  21.  
    loHead = e;
  22.  
    else
  23.  
    loTail.next = e;
  24.  
    loTail = e;
  25.  
    }
  26.  
    else {
  27.  
    if (hiTail == null)
  28.  
    hiHead = e;
  29.  
    else
  30.  
    hiTail.next = e;
  31.  
    hiTail = e;
  32.  
    }
  33.  
    } while ((e = next) != null);
  34.  
    if (loTail != null) {
  35.  
    loTail.next = null;
  36.  
    newTab[j] = loHead; // lo队不为null,放在新表原位置
  37.  
    }
  38.  
    if (hiTail != null) {
  39.  
    hiTail.next = null;
  40.  
    newTab[j + oldCap] = hiHead; //hi队不为null,放在新表j+oldCap位置
  41.  
    }
  42.  
    }
  43.  
    }
  44.  
    }
  45.  
    }
  46.  
    return newTab;
  47.  
    }

即扩容机制包括两部分:1  HashMap中table数组的容量的扩容

                                     2  成员属性threshold(即扩容的临界值)的更改。

 

Note:

加载因子(默认0.75):为什么需要使用加载因子,为什么需要扩容呢?

因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多),扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率

HashMap本来是以空间换时间,所以填充比没必要太大。但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。

 

HashMap内部扩容机制

 

构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比*Node.length)重新调整HashMap大小 变为原来2倍大小,扩容很耗时。

所涉及到的数据结构链表Node与红黑树TreeNode了,这两个数据结构为HashMap中的内部类,源码如下:

  1.  
    static class Node<K,V> implements Map.Entry<K,V> {
  2.  
    final int hash;
  3.  
    final K key;
  4.  
    V value;
  5.  
    Node<K,V> next;
  6.  
     
  7.  
    Node(int hash, K key, V value, Node<K,V> next) {
  8.  
    this.hash = hash;
  9.  
    this.key = key;
  10.  
    this.value = value;
  11.  
    this.next = next;
  12.  
    }
  13.  
     
  14.  
    public final K getKey() { return key; }
  15.  
    public final V getValue() { return value; }
  16.  
    public final String toString() { return key + "=" + value; }
  17.  
     
  18.  
    public final int hashCode() {
  19.  
    return Objects.hashCode(key) ^ Objects.hashCode(value);
  20.  
    }
  21.  
     
  22.  
    public final V setValue(V newValue) {
  23.  
    V oldValue = value;
  24.  
    value = newValue;
  25.  
    return oldValue;
  26.  
    }
  27.  
     
  28.  
    public final boolean equals(Object o) {
  29.  
    if (o == this)
  30.  
    return true;
  31.  
    if (o instanceof Map.Entry) {
  32.  
    Map.Entry<?,?> e = (Map.Entry<?,?>)o;
  33.  
    if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue()))
  34.  
    return true;
  35.  
    }
  36.  
    return false; }

可以看到该链表Node是一个单向链表(因为只存在一个Node<K,V> next属性)它实现了Map.Entry<K,V>接口。

  1.  
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
  2.  
    TreeNode<K,V> parent; // red-black tree links
  3.  
    TreeNode<K,V> left;
  4.  
    TreeNode<K,V> right;
  5.  
    TreeNode<K,V> prev; // needed to unlink next upon deletion
  6.  
    boolean red; //表示颜色的属性,红黑树是一种自平衡二叉查找树,用red与black来标识某个节点,它可以在O(logn)内进行查找,插入与删除
  7.  
    TreeNode(int hash, K key, V val, Node<K,V> next) {
  8.  
    super(hash, key, val, next);
  9.  
    }
可以看到TreeNode它实现了LinkedHashMap.Entry<K,V>接口.

 

HashMap的底层实现原理:

即HashMap是采用数组 Node<K,V>[ ] table来存储<K,V>的,数组中的每个元素是Node类型(可能会将该Node类型转换为TreeNode类型),通常称这种方式为位桶+链表/红黑树,当某个位桶的链表的长度达到TREEIFY_THRESHOLD临界值的时候,这个链表就将转换成红黑树。本质上是一个Hash表,用来解决冲突的(这一点将在HashMap中的put<K,V>方法中看到)。用图示表示如下:

 

在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特点是查询时间是O(logn))存储(有一个阀值控制,大于阀值(8个),将链表存储转换成红黑树存储)

 

 

 

 

HashMap实现了Map接口,Map接口设置一系列操作Map集合的方法,如:putgetremove...等方法,而HashMap也针对此有其自身对应的实现。

HashMap继承AbstractMap类。AbstractMap类对于Map接口做了基础的实现,实现了containsKeycontainsValue...等方法。

 

HashMap常用的方法:

1  put(K,V):

在构造函数中最多也只是设置了initialCapacityloadFactor的值,并没有初始化tabletable的初始化工作是在put方法中进行的。

  1.  
    public V put(K key, V value) {
  2.  
    return putVal(hash(key), key, value, false, true);
  3.  
    }
  4.  
     
  5.  
    //插入值, onlyIfAbsent,为真的话,就是不替换,无就插,有就不插  methods evict,表示需要调整二叉树结构 在LinkedHashMap使用
  6.  
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
  7.  
    Node<K,V>[] tab; Node<K,V> p; int n, i;
  8.  
    if ((tab = table) == null || (n = tab.length) == 0)     //1 首先判断table数组的长度是否为0或table数组是否为空,即通常情况下表示刚创建一个空的HashMap时,当你调用put(K,V)方法时才会分配内存,即tab = resize()
  9.  
    n = (tab = resize()).length;
  10.  
    if ((p = tab[i = (n - 1) & hash]) == null)        //2首先判断tab[(n - 1) & hash]处是否为空,如果是代表该数组下标为[(n - 1) & hash]的位置无元素,可直接put
  11.  
    tab[i] = newNode(hash, key, value, null);         // 没有数据,就是放一个链表头节点
  12.  
    else {
  13.  
    Node<K,V> e; K k;
  14.  
    if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //哈希值和equals值均同 则用新值换旧值
  15.  
    e = p;
  16.  
    else if (p instanceof TreeNode)                     //否则 判断是否需要红黑树结构
  17.  
    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  18.  
    else {                     //否则 为链表结构
  19.  
    for (int binCount = 0; ; ++binCount) {
  20.  
    if ((e = p.next) == null) {
  21.  
    p.next = newNode(hash, key, value, null);
  22.  
    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  23.  
    treeifyBin(tab, hash);                  //把链表转为二叉树存储
  24.  
    break;
  25.  
    }
  26.  
    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))//如果Hash值相同,则调用equals方法来确定是否存在该元素,则执行break语句
  27.  
    break;//跳出for循环,执行下面的if语句,更新value的值.
  28.  
     
  29.  
    p = e;
  30.  
    }
  31.  
    }
  32.  
    if (e != null) { // existing mapping for key // 替换操作,key一样,旧值换为新值
  33.  
    V oldValue = e.value;
  34.  
    if (!onlyIfAbsent || oldValue == null)
  35.  
    e.value = value;
  36.  
    afterNodeAccess(e);//
  37.  
    return oldValue;
  38.  
    }
  39.  
    }
  40.  
    ++modCount;
  41.  
    if (++size > threshold)
  42.  
    resize();
  43.  
    afterNodeInsertion(evict);  //LinkedHashMap使用
  44.  
    return null;
  45.  
    }

put方法主要包括两大部分:

1 根据传入的key计算hash值得到插入的数组索引 i,如果tab[i]==null,表示此下标处无元素存在,可直接添加元素,否则出现冲突,那么就要用到链表或红黑树来解决冲突,可参看上图帮助理解,

2 如果出现冲突,则扫描链表或红黑树,在此过程中通过equals方法来确定是否存在该元素,如果存在,则直接更新,否则采用链表或红黑树的方式将元素添加到tab[i]对应的链表或红黑树中,可参看上图帮助理解。

即通过hash的值来判断是否存在该元素,如果hash值不存在(tab[i]==null),则一定不存在该元素,若hash值存在,则可能存在该元素,需要通过equals方法来确定,如果hash值存在且key.equals.(k)则表明存在该元素,直接更新其值,否则表明不存在,则采用链表或红黑树的方式将元素添加到tab[i]对应的链表或红黑树中

Note:   如果key=null,那么hash(key)=0,table[0] 位置存在 ,且初始时 存放的是null值(有判断是否存放null值的步骤)。

 

2 V get(Object key) 

  1.  
    public V get(Object key) {
  2.  
    Node<K,V> e;
  3.  
    return (e = getNode(hash(key), key)) == null ? null : e.value; //null值返回
  4.  
    }
  5.  
    final Node<K,V> getNode(int hash, Object key) {
  6.  
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  7.  
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //通过该hash值与table的长度n-1相与得到数组的索引first
  8.  
    if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
  9.  
    return first;  // always check first node
  1.  
    if ((e = first.next) != null) {
  2.  
    if (first instanceof TreeNode)        //代表该HashMap为数组+红黑树结构
  3.  
    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  4.  
    do {                                //否则代表是数组+链表结构
  5.  
    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
  6.  
    return e;
  7.  
    } while ((e = e.next) != null);
  8.  
    }
  9.  
    }
  10.  
    return null;
  11.  
    }

 

从上述代码可以看到:

get某个元素与put某个元素是一一对应的关系:

先通过key得到对应的hash值,然后通过该hash值与table的长度n-1相与得到数组下标的索引first,

然后先判断传入的hash是否与数组索引first节点对应的hash值相等,

如果是则直接返回该数组元素first,

否则则通过first.next不断查找该数组元素所对应的链表/红黑树中是否存在hash与key均和传入的hash与key相等的节点,

如果存在则代表在HashMap集合中找到了该元素,则返回其对应的Value。

 

 

3  V  remove(Object key)

 先找到节点,然后在判断哪种方法删除,以及删除之后的调整。

  1.  
    /**
  2.  
    * 根据key,删掉这个节点。
  3.  
    */
  4.  
    public V remove(Object key) {
  5.  
    Node<K, V> e;
  6.  
    return (e = removeNode(hash(key), key, null, false, true)) == null ? null
  7.  
    : e.value;
  8.  
    }
  1.  
    /**
  2.  
    * 删除某一个节点。
  3.  
    * @param matchValue
  4.  
    * 如果为真,那么只有当value也想等时,才能删除。
  5.  
    * @param movable 能否删除
  6.  
     
  7.  
    */
  8.  
    final Node<K, V> removeNode(int hash, Object key, Object value,
  9.  
    boolean matchValue, boolean movable) {
  10.  
    Node<K, V>[] tab;
  11.  
    Node<K, V> p;
  12.  
    int n, index;
  13.  
    if ((tab = table) != null && (n = tab.length) > 0
  14.  
    && (p = tab[index = (n - 1) & hash]) != null) {
  15.  
    //寻找node节点过程
  16.  
    Node<K, V> node = null, e;
  17.  
    K k;
  18.  
    V v;
  19.  
    if (p.hash == hash
  20.  
    && ((k = p.key) == key || (key != null && key.equals(k))))
  21.  
    node = p;
  22.  
    else if ((e = p.next) != null) {
  23.  
    if (p instanceof TreeNode)
  24.  
    node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
  25.  
    else {
  26.  
    do {
  27.  
    if (e.hash == hash
  28.  
    && ((k = e.key) == key || (key != null && key.equals(k)))) {
  29.  
    node = e;
  30.  
    break;
  31.  
    }
  32.  
    p = e;
  33.  
    } while ((e = e.next) != null);
  34.  
    }
  35.  
    }
  36.  
    //node节点就是已经找到的,符合条件的要删除的节点。
  37.  
    if (node != null
  38.  
    && (!matchValue || (v = node.value) == value || (value != null && value
  39.  
    .equals(v)))) {
  40.  
    if (node instanceof TreeNode)
  41.  
    ((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
  42.  
    else if (node == p)
  43.  
    tab[index] = node.next;
  44.  
    else
  45.  
    p.next = node.next;
  46.  
    ++modCount;
  47.  
    --size;
  48.  
    afterNodeRemoval(node);
  49.  
    return node;
  50.  
    }
  51.  
    }
  52.  
    return null;
  53.  
    }

 

 

总结:

1    HashMap内部是基于Hash表实现的,该Hash表为Node类型数组+链表/红黑树,其中链表与红黑树是用来解决冲突的,即当往HashMap中put某个元素时,相同的hash值的两个值会被放到数组中的同一个位置上形成链表或红黑树。

2    HashMap存在扩容机制,是通过resize()方法实现的,即当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,数组的大小*loadFactor=threshold(即扩容的临界值),默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍

3   另外从put与get的源码可以看到HashMap的Key与Value都允许为null,同时可以看到HashMap中的put与get方法均无synchronized关键字修饰,即HashMap不是线程安全的。

4   HashMap中的元素是唯一的(即同一个key只存在唯一的V与之对应),因为在put的过程中如果可能出现相同元素(K相同V不同),则原来的V将会被替换。

 

问题分析:

你可能还知道哈希碰撞会对hashMap的性能带来灾难性的影响。如果多个hashCode()的值落到同一个桶内的时候,这些值是存储到一个链表中的。最坏的情况下,所有的key都映射到同一个桶中,这样hashmap就退化成了一个链表——查找时间从O(1)到O(n)。

随着HashMap的大小的增长,get()方法的开销也越来越大。由于所有的记录都在同一个桶里的超长链表内,平均查询一条记录就需要遍历一半的列表。

JDK1.8HashMap的红黑树是这样解决的:

如果某个桶中的记录过大的话(当前是TREEIFY_THRESHOLD = 8),HashMap会动态的使用一个专门的treemap实现来替换掉它。这样做的结果会更好,是O(logn),而不是糟糕的O(n)

它是如何工作的?

前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进行查找。但是超过这个阈值后HashMap开始将列表升级成一个二叉树,使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。这对HashMap的key来说并不是必须的,不过如果实现了当然最好。如果没有实现这个接口,在出现严重的哈希碰撞的时候,你就并别指望能获得性能提升了。

 

posted @ 2020-03-11 16:10  无敌是多么寂寞啊  阅读(284)  评论(0编辑  收藏  举报