HashMap源码分析-基于JDK1.8

  1. hashMap数据结构
  2. 类注释
  3. HashMap的几个重要的字段
  4. hash和tableSizeFor方法

HashMap的数据结构

由上图可知,HashMap的基本数据结构是数组和单向链表或红黑树。

以下内容翻译于HashMap类的注释

HashMap是map接口的基础实现类。这个实现提供了所有可选的Map接口操作。并且允许null键和null值。HashMap类和Hashtable类差不多,只是HashMap不是线程完全的,并且HashMap允许null值和null键。这个类不保证map元素的顺序,也不保证顺序会随着时间保持不变。

假如hash函数能够使元素在桶中(buckets)均匀地分散,对于基本的get,put操作HashMap的性能还是比较稳定的。集合视图的遍历(EntrySet之类的操作,也许这些方法返回的结果都是集合类型,所以叫做集合视图)需要的时间和HashMap的"capacity"(buckets的数据)乘以数量(bucket中的健值对的数量)成正比。因此如果遍历性能非常重要,那么就不要把初始的CAPACITY设置的太大(或者LOAD_FACTOR太小)。

HashMap实例有有两个属性影响它的性能:CAPACITYLOAD_FACTORCAPACITYhash表里桶的数量,并且初始的CAPACITY仅仅是hash表创建时的容量。LOAD_FACTORhash表在自动地增加它的CAPACITY前,允许CAPACITY有多满的测量方式。当hash表里的条目的数量超过当前CAPACITY乘以LOAD_FACTOR的数量时,hash表被重新计算hash。(也就是说内部的数据结构被重建)。以便hash表具有大概两倍于原来桶数量。

一般来说,默认的loadfactory(0.75)在时间和空间消耗上提供了一个好的折中。更高的值减小了空间压力,但是增加了查询消耗(反映在HashMap中的大部分操作,包括getput)。为了减小rehash的操作次数,当设置它的初始capacity时应该考虑将来的map中的条目数量和它的loadfactory。如果初始capacity大于条目最大数量除以loadfactory,就不会有rehash操作发生。

如果很多映射(键值对)将被存储在HashMap中。与在需要的时候自动地执行rehash操作来扩大hash表大小相比,创建一个足够大capacityhashMap来存储映射将是更高效的。注意,很多key具有相同的hashCode()值是降低任何hash表性能的方式。

注意这个实现不是synchronized(线程安全)的。如果多个线程同时访问hashMap,并且只要有一个线程修改map结构,它就必须在外面被加上synchronized。(结构的修改是指任何增加或删除一个或多个的映射,仅仅修改一个健的值不是结构的修改)。这通常通过在天然地包裹map的对象上同步来实现。如果没有这样的对象存在。map应该用Collections.synchronizedMap方法包装一下。为了防止对map意外的不同步的访问,最好在创建的时候完成这样的操作。例如

Map m = Collections.synchronizedMap(new HashMap(...))

 

被这个类的”集合视图方法”返回的所有遍历器都是快速失败的:在这个遍历器创建之后,用任何方法除了iterator自身的remove方法修改map的结构将会抛出ConcurrentModificationException。因此面对同时的修改,遍历器快速而干净利落地失败。而不是在不确定的未来冒着不确定的危险。

 

注意,遍历器快速失败的行为不能被用来保证它看起来的样子。换句话说,在不同步的同时修改前面不能做任何强的担保。快速失败的遍历器尽量地抛出ConcurrentModificationException。写的程序依赖这个异常来保证正确性将是错误的。iterators的快速失败行为应该只被用于检测错误。

 

HashMap的几个重要的字段

/**
* 默认的CAPACITY值,也就是16,这个值必须是2的幂
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
 * capacity最大值, 当在构造器中传入一个比这大的参数的时候使用。
 * 也就是说,当传入的值大于这个值,就使用这个值
 * 必须是2的幂
 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
 * 在构造器中没有指定的时候被使用的默认的加载因子.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
  * 使用树而不是链表的bin(就是之前常说的桶(bucket))中的数量阀值,当在bin中增加数据的时候,大于这个值
 * 就会把bin中的数据从链表转换成红黑树结构来表示。这个值必须大于2并且应该小
 * 小于8。 
*/
static final int TREEIFY_THRESHOLD = 8;
/**
 * 在resize操作中把bin中数据变为列表结构的数量阀值,如果小于这个值,就会
 * 从树结构变为列表结构。这个值应该小于TREEIFY_THRESHOLD并且最大为6。
 * most 6 to mesh with shrinkage detection under removal.
 */
static final int UNTREEIFY_THRESHOLD = 6;
/**
 * 当bin中的结构转换为树的时候,CAPACITY的最小值.
 * 否则就会resize当bin中数据太多的时候。应该至少4 * TREEIFY_THRESHOLD
 * 来避免resizing和树转换阀值之间的冲突。
 * between resizing and treeification thresholds.
 */
static final int MIN_TREEIFY_CAPACITY = 64;

节点实现类

  1. Node

 1 /**
 2      * 基本的bin节点, 被用于表示大部分的数据条目。
 3      * 
 4      */
 5     static class Node<K,V> implements Map.Entry<K,V> {
 6         final int hash;         // 这个记录的是K的hash值
 7         final K key;           // map的键
 8         V value;              // map的值
 9         Node<K,V> next;          // 指向下一个节点
10 
11         Node(int hash, K key, V value, Node<K,V> next) {
12             this.hash = hash;
13             this.key = key;
14             this.value = value;
15             this.next = next;
16         }
17 
18         public final K getKey()        { return key; }
19         public final V getValue()      { return value; }
20         public final String toString() { return key + "=" + value; }
21      // 节点的hashCode,key的hashCode和value的hashCode的异或 
22         public final int hashCode() {
23             return Objects.hashCode(key) ^ Objects.hashCode(value);
24         }
25 
26         public final V setValue(V newValue) {
27             V oldValue = value;
28             value = newValue;
29             return oldValue;
30         }
31      // 重写的equals,如果节点的key和value都相等,两个节点才相等。
32         public final boolean equals(Object o) {
33             if (o == this)
34                 return true;
35             if (o instanceof Map.Entry) {
36                 Map.Entry<?,?> e = (Map.Entry<?,?>)o;
37                 if (Objects.equals(key, e.getKey()) &&
38                     Objects.equals(value, e.getValue()))
39                     return true;
40             }
41             return false;
42         }
43     }

静态的工具方法

/**
 * 计算key的hashCode值并且把hashCode的高16位和低16位异或。
 * 这是一个折中的做法。因为现在大部分情况下,hash的分布已经
 * 比较分散了,而且如果冲突比较多的时候,我们会把bin中的数据转
 * 换为树结构,来提高搜索速度。
 */
  static final int hash(Object key) {
      int h;
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

疑问?

  为什么采用这样的算法。或者说为什么要把hashCode的高16位和低16位异或。我一开始也想不明白,看其它的文章也很难找到把这一点解决明白的。

  于是我就动手做实验,来验证,如果不采用异或会怎么样。采用异或之后有什么效果。

  当然,不能忘记一点,计算hash值是为了找这个key对应的table数组之中的下标的。计算下标的算法是tab[i = (n - 1) & hash]。这里的n是table数组的数量。hash就是hash()方法返回的    值。他们两个求'与'。在源码的类说明里,说了一种情况,就是几个连续的Float类型的值在一个小的table中会冲突。我就以几个连续的Float值为样例测试。代码如下

   

  

 1 /**
 2  * 描述:
 3  * 日期:2017年11月13
 4  * @author dupang
 5  */
 6 public class DupangTest {
 7     public static void main(String[] args) {
 8         Float f1 = 1f;
 9         Float f2 = 2f;
10         Float f3 = 3f;
11         Float f4 = 4f;
12 
13         String f1_hashCode = Integer.toBinaryString(f1.hashCode());
14         String f2_hashCode = Integer.toBinaryString(f2.hashCode());
15         String f3_hashCode = Integer.toBinaryString(f3.hashCode());
16         String f4_hashCode = Integer.toBinaryString(f4.hashCode());
17 
18         System.out.println(f1_hashCode);
19         System.out.println(f2_hashCode);
20         System.out.println(f3_hashCode);
21         System.out.println(f4_hashCode);
22 
23         int size = 198;
24         int f1_index = f1.hashCode()&(size-1);
25         int f2_index = f2.hashCode()&(size-1);
26         int f3_index = f3.hashCode()&(size-1);
27         int f4_index = f4.hashCode()&(size-1);
28 
29         int f1_index_1 = hash(f1)&(size-1);
30         int f2_index_2 = hash(f2)&(size-1);
31         int f3_index_3 = hash(f3)&(size-1);
32         int f4_index_4 = hash(f4)&(size-1);
33 
34         System.out.println(f1_index);
35         System.out.println(f2_index);
36         System.out.println(f3_index);
37         System.out.println(f4_index);
38         System.out.println("=========华丽的分割线===========");
39         System.out.println(f1_index_1);
40         System.out.println(f2_index_2);
41         System.out.println(f3_index_3);
42         System.out.println(f4_index_4);
43     }
44 
45     static final int hash(Object key) {
46         int h;
47         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
48     }
49 }
View Code

 

 

输出结果如下

111111100000000000000000000000
1000000000000000000000000000000
1000000010000000000000000000000
1000000100000000000000000000000
0
0
0
0
=========华丽的分割线===========
128
0
64
128

从输出结果可以看到,Float类型的1,2,3,4的hashCode都比较大,低位的都是0。如果table的size比较小的时候,和hashCode直接与的话,结果都是0。也就是找到的下标都是一样的,

由于在操作过程当中就会冲突。

分割线下的结果,就是把hashCode的高16位移到低16位异或,然后计算下标得到的结果,可以看到,计算的下标还是比较分散的,至少比都是0强多了。

这就是计算hash的时候,为什么要把高16位和低16位做异或的原因了,就是能够让高16位在计算下标的时候,能够参与进来

而且在计算hash值的时候,当key等于null的时候,hash值是0。这也是为什么HashMap为什么允许null键的原因

 

  • comparableClassFor

 

 1    /**
 2      * 返回x的Class类对象,如果x实现了接口Comparable<x>。否则就返回Null
 3      * Comparable<C>", else null.
 4      */
 5     static Class<?> comparableClassFor(Object x) {
 6         if (x instanceof Comparable) {
 7             Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
 8             if ((c = x.getClass()) == String.class) // bypass checks,如果x的类型是String,直接返回x.getClass(),为什么其它的像Integer不直接返回?
 9                 return c;
10             if ((ts = c.getGenericInterfaces()) != null) {//通过反射获取c的接口类型。
11                 for (int i = 0; i < ts.length; ++i) {//循环,如果 1是参数化类型,2,并且类型是Comparable.class,3,并且参数类型的参数不为null,4并且参数长度是1,5并                                        且参数类型是x.getClass();就返回x.getClass();
12                     if (((t = ts[i]) instanceof ParameterizedType) && 
13                         ((p = (ParameterizedType)t).getRawType() ==
14                          Comparable.class) &&
15                         (as = p.getActualTypeArguments()) != null &&
16                         as.length == 1 && as[0] == c) // type arg is c
17                         return c;
18                 }
19             }
20         }
21         return null;
22     }

 

  • compareComparables

 

 /**
     * 返回k.compareTo(x)的结果,如果x和k可比较。
     * 否则就返回0
     */
    @SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
    static int compareComparables(Class<?> kc, Object k, Object x) {
        return (x == null || x.getClass() != kc ? 0 :
                ((Comparable)k).compareTo(x));
    }
  • tableSizeFor

   /**
     * 返回大于等于指定cap值的最小的2的幂.比如cap值是5,计算结果就是8,cap值是16,计算结果还是16,因为16是2的幂
     */
    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;
    }

看到这个方法是否有点晕。首先把cap的值减1赋值给n。n无符号右移1位然后和n求或,再把结果赋值给n。再把n无符号右移2位然后和n求或,再把结果赋值给n。如此往反。

这有什么意义呢。这么做的奥秘何在。光看是不行的。还是动手吧。以cap值是1073741825为例来走一遍这个操作。为什么选择这个数,因为更能看到经过移位后的效果。

 看到规律了没,经过移位之后,他把从高到低的位都变成1了。这还不算。因为都是1的话,换成10进制还不是2的幂。最后还有+1这个操作。二进制加1,1+1=2,逢2进1。后面的一串1都变成0,  最高位进1。相当于左移了一位,低位都变成0。这时候得到的值才是2的幂。这时候还可以联想一下,一个1从最低位开始左移,左移一位相当于乘以2。左移几次,相当于乘以几个2,等到的值当然是2的幂了。

最后强调一点,我举这个例子是为了说明右移的效果。如果真是这个值,最后就会大于MAXIMUM_CAPACITY,最后结果就是MAXIMUM_CAPACITY的值,也就是1<<30,2的30次方,当然也是2的幂了,还有为什么用32位表示,因为int在java中就是4个字节,占32位。还有,如果你的cap是0,n的值是-1;如果自己推结果,别忘记了负数用补码表示。

 

字段

   /**
     * 节点的数组,从这里可以看出map的底层实现是数组。这个数组并不是
     * 在构造方法里初始化,而是在第一次用到时候初始化它的大小。比较put操作。
     * 而且它的数组大小总是2的幂。它的大小也就是前面讲的tableSizeFor求得的。
     */
    transient Node<K,V>[] table;
   /**
     * 持有entrySet()方法的结果.
     */
    transient Set<Map.Entry<K,V>> entrySet;
   /**
     * map中键值对的数量.
     */
    transient int size;
   /**
     * 这个HashMap被结构化修改的次数。比如改变键值对的数量。或者内部结构的改变(rehash操作)。
     * 这个字段被用来遍历HashMap的集合视图的快速失败。
     */
    transient int modCount;
    /**
     * 触发resize操作的阀值。当capacity * load factor的值达到这个值的时候,就会执行resize操作。使table的数组扩大。
     *
     */
    int threshold;
    /**
     * HashMap的加载因子
     */
    final float loadFactor;

公共方法

    /**
     * HashMap的构造方法,可以指定初始大小和加载因子。一般很少直接用到,因为很少去自己指定加载因子的值。默认的0.75在大部分情况下都适用
     * 当初始值小于0的时候抛异常。
     * 当加载因为的值不是正数的时候也抛异常。
     * 当指定的初始大小大于MAXIMUM_CAPACITY时。初始大小为MAXIMUM_CAPACITY。也就是说初始大小不能大于MAXIMUM_CAPACITY。
     * 同时也调用tableSizeFor方法计算出下一次resize操作的阀值。这个方法前面详细讲过了。
   * 从这里也可以看了构造方法里,并没有初始化table的值。它把这个过程往后移了。可能在面试的时候会被问到这一点。
*/ 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); }
    /**
     * HashMap的构造方法。可以指定一个初始大小。加载因子用默认值(0.75)
     * 这个方法最终调用上面的构造方法。用的最多的就是这个构造方法。*/
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    /**
     * HashMap构造方法,用默认的初始值(16)和加载因子(0.75)。
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    /**
     * HashMap的构造方法。参数是一个map。加载因子是默认值。初始大小会根据map参数的大小计算得到。
     * 它会map中的键值一个一个地拷贝到HashMap中。当传入map为null时会抛空指针。
     * 它实际调用的是putMapEntries方法。下面分析一下这个方法*/
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
   /**
     * 这个方法被构造方法和putAll方法调用。当在构造方法中调用的时候evict参数是false。
     * 在putAll方法中调用的时候evict参数传true。这个在HashMap中没有实际意义。
     * 1 计算出map的大小赋值给s,当s的大于0的时候进入下一步
     * 2 如果table等于null,就会计算threshold的值,此时还是没有初始化table的大小,它把map的size除以加载因子,再加1(为什么要再加1呢?)。
   * 得到的值如果不大于MAXIMUM_CAPACITY,就再判断是否这个值大于threshold。这时肯定是大于的,因为这时threshold还没赋值,是0;干嘛还要比较呢,
   * 3 根据map的size除以加载因为的值为参数,求得一个下一次resize操作的阀值赋值给threshold。
   * 4 else if的条件是判断当map的size大于threshold的时候,就会执行resize操作。但是构造方法是否会走到这个逻辑的,只有putAll方法才有可能走到这个逻辑,我们一会再看resize逻辑
   * 5 最后会遍历map,以map的key和value执行putVal方法,把map中的键值一对一对地put到构造的HashMap中。下面让我们先看看putVal的方法。
*/ final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { // 1 if (table == null) { // 2 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) // 3 threshold = tableSizeFor(t); } else if (s > threshold) // 4 resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { // 5 K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }

  /**
   * 1 如果table为null或者table的长度为0,就调用resize方法初始化table相关数据。resize具体实现一会再看。从构造方法里走到这里的时候,table肯定是null的。
   * 2 下面就是插入数据的操作。tab[i = (n - 1) & hash],这个比较简单也比较重要,n是resize后初始化的table数组的大小,它把n-1和k的hash值与操作求得这个key在table中的下标。
   *   然后判断这个下标的值是否等于null,如果等于null,比较happy,说明没有冲突。就new一个Note节点把key和value赋值进去。然后把这个Note放到table中这个下标的位置。
   *    最后modCount加1,因为map的结构变化了。size加1并判断是否大于threshold,如果大于,就会做resize操作。
   * 3 处理key的hash冲突的情况。
   *  3.1 如果老节点的hash和新的hash相等,并且key相等。直接走到 3.6
   *  3.2 如果冲突节点的hash值不相等或者key不相等,然后判断节点类型是否是TreeNode,如果是说明是红黑树的结构,就调用putTreeVal方法,
   *  3.3 如果冲突节点的ahsh值不相等或者key不相等,并且节点类型不是TreeNode,就走这里的逻辑,遍历这个链表,先判断next节点是否为null,如果是null,说明当前节点是链表的最后一个节点。
   *       然后就new一个Note节点插入到链表的最后。接着判断遍历的次数,如果大于等级7就把链表结构转换为红黑树的结构。同时跳出循环

   *  3.3.1 如果遍历的过程中,下一个节点不为null,就判断hash是否相等,并且key是否相等,如果相等,就跳出循环,这时找到的节点的存储在变量e中。

   *  3.4 判断e是否为null,不为null说明存在和要put的key相同的节点。当onlyIfAbsent为false的时候,也就是key相同时覆盖旧的值。如果之前key的值为null也覆盖旧的值。并返回旧的value值。

   *   返回之前调用了一个afterNodeAccess方法,这个方法在HashMap里是一个空方法。没有具体意义。走到这里,是直接返回了,没有走方法最后几行的逻辑,因为找到了相同的key节点,并没有改变map的结构,size大小也没变。所以就直     *     接返回了。

   *  4 最后 modCount加1,所明map的结构发生改变了。并且判断size加1后是否大于阀值,如果大于就触发发resize的条件,进行rezize。同样调用了afterNodeAccess方法,最后返回null,也说明put的是一个新值,没有key相同的节点

        下面让我们看看resize方法都做了什么  

 1     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 2                    boolean evict) {
 3         Node<K,V>[] tab; Node<K,V> p; int n, i;
 4         if ((tab = table) == null || (n = tab.length) == 0) // 1
 5             n = (tab = resize()).length;
 6         if ((p = tab[i = (n - 1) & hash]) == null) // 2
 7             tab[i] = newNode(hash, key, value, null);
 8         else { // 3
 9             Node<K,V> e; K k;
10             if (p.hash == hash &&
11                 ((k = p.key) == key || (key != null && key.equals(k)))) // 3.1
12                 e = p;
13             else if (p instanceof TreeNode) // 3.2
14                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
15             else {
16                 for (int binCount = 0; ; ++binCount) { // 3.3
17                     if ((e = p.next) == null) {
18                         p.next = newNode(hash, key, value, null);
19                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
20                             treeifyBin(tab, hash);
21                         break;
22                     }
23                     if (e.hash == hash &&
24                         ((k = e.key) == key || (key != null && key.equals(k)))) // 3.3.1
25                         break;
26                     p = e;
27                 }
28             }
29             if (e != null) { // 3.4
30                 V oldValue = e.value;
31                 if (!onlyIfAbsent || oldValue == null)
32                     e.value = value;
33                 afterNodeAccess(e);
34                 return oldValue;
35             }
36         }
37         ++modCount; // 4
38         if (++size > threshold)
39             resize();
40         afterNodeInsertion(evict);
41         return null;
42     }

 

 

  /**
     * 最常用的put方法,这个方法一看上去很简单,其实具体实现都在putVal方法里。*/
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

 

   /**
     * 初始化一个table或使原来的table大小翻倍.  如果table为null,就以threshold的值作为初始大小来分配table。
     * 1 先初始化几个变量值,把当前table变量赋值给oldTab,如果oldTab是null,说明table还没有初始化,oldCap就为0。否则就是table的长度。把老的threshold赋值给oldThr中。
     * 2 如果oldCap大于0,所以之前table已经被初如化过,也就是map里之前有值。
      2.1 这时候看oldCap是否大于MAXMUM_CAPACITY,如果大于等于,就干脆把threshold定为Integer最大值。也没必要再乘以2了,因为MAXMUM_CAPACITY已      经是2的30次方了。再乘以2就越界了。 * 2.2 如果oldCap大于0并且小于MAXMUM_CAPACITY。就进入这个逻辑块,如果oldCap左移一位(乘以2)后还是小于MAXMUM_CAPACITY并且oldCap大于默认的CAPACITY(16),并把oldThr左移一位(乘以2),存到newThr变量中。
    3 如果oldCap小于等于0说明。之前没有初始化table,这时候判断oldThr,如果大于0,就把阀值当作table大小赋值给newCap。 * 4 否则就把新的table大小设置为默认值16,并根据默认值计算出resize的阀值。 * 5 判断新的阈值是否是0,如果走到3的条件里,就会是这种情况,这时会根据新的cap和默认的加载因子(0.75)。如果新的cap和阈值都小于MAXMUM_CAPACITY。就把计算出的阈值赋值给newThr      e
6 然后根据newCap大小,new一个Node数组,并把这个数组赋值给table。
    7 如果老的table不为空,就要把老的table一个一个的copy到新的table里。
    7.1 遍历老table中的元素
 
  7.1.1 如果table数组中的这个下标不为空,就准备copy到新的table里
    
7.1.2 如果table数组中的这个下标不为空,并且next结点为空,说明只有一个元素。没有链接结构。就根据hash和新的数组大小求一个下标,放到新table的这个下标里。
7.1.3 如果这个节点是树结点。就进行树操作。
    7.1.4 如果数据的这个下标有空,并且这个节点还有next节点。就把链接结构里的节点也一并cp到新的table里。这里有一点不同,就是如果这个节点和老的数组大小求与结果是0,就把这个节点还是放到新的table数组的
                                    的相同下标的位置,否则就移动oldCap个下标放置。有点不太明白。直接用if的逻辑不就行了。把e赋值过去,e.next这一大串不也跟着过去了么。

    8 最后返回扩容后的table * @return the table */ final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; // 1 int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { // 2 if (oldCap >= MAXIMUM_CAPACITY) { // 2.1 threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 2.2 newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // 3 initial capacity was placed in threshold newCap = oldThr; else { // 4// zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { // 5 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; // 6 @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { // 7 for (int j = 0; j < oldCap; ++j) { // 7.1 Node<K,V> e; if ((e = oldTab[j]) != null) { // 7.1.1 如果table数组中的这个下标不为空,就准备copy到新的table里 oldTab[j] = null; if (e.next == null) // 7.1.2 如果table数组中的这个下标不为空,并且next结点为空,说明只有一个元素。没有链接结构。就根据hash和新的数组大小求一个下标,放到新table的这个下标里。 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) // 7.1.3 如果这个节点是树结点。就进行树操作。 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order // 7.1.4 如果数据的这个下标有空,并且这个节点还有next节点。就把链接结构里的节点也一并cp到新的table里。这里有一点不同,就是如果这个节点和老的数组大小求与结果是0,就把这个节点还是放到新的table数组的
                                    的相同下标的位置,否则就移动oldCap个下标放置。有点不太明白。直接用if的逻辑不就行了。把e赋值过去,e.next这一大串不也跟着过去了么。
Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }

 

下面我们来看一下,当链表中的元素大于8个时,怎么怎么把单链表转成树的。

    /**
     * 1 而table等于null的或table的长度小于MIN_TREEIFY_CAPACITY(64)的,并不会去转成红黑树,而是进行resize。所以链表结构转成红黑树,需要满足两个条件。1 链表的元素大于8个。2 table的数组长度大于64.
     * 2 根据hash计算得出下标,获取这个下标中的值。然后遍历。把以这个下标元素为头的单链接表中的每一个元素。都转成TreeNode。TreeeNode其实继承于LinkedHashMap。所以它也是一个两向链表。在转成红黑树之前。把单链表中的节点转成TreeNode的同时。也把单链表转成了   
      双向链表。然后再调用TreeNode的方法hd.treeify(tab)。去把双向链表转成红黑树。 */ 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) // 1 resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { // 2 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); } }
     /**
         * 这里就是转成红黑树的核心代码。如果不知道什么是红黑树的,就不要继续看下去了。先是先去看看什么是红黑树再说吧。
     * 这里大部分代码就是再链表的节点遍历。做红黑树的插入。因为红黑树也是二叉搜索树,所以插入的时候也是小的在左边。大的右边。比较大小的时候,是用的hash值比较的。插入完后。因为可能会违反红黑树的性质。所以就需要调用balanceInsertion这个方法
     * 做一些重新着色和左旋和右旋这样的操作。最后使节点插入后,依然是一棵红黑树。所以看懂了红黑树。看这部分代码就很容易多了。   *
@return root of tree */ 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); }

 

在resize的文中。当节点是TreeNode时。会调用TreeNode的split方法。下面我们看看这个方法。

 

        /**
         * 这个方法和之前的链表结构的移动有点类似。就是遍历这个Tree。把节点的hash值和老的数组大小求与。如果是0.就把这些数据,放到新的table和老的table相同下标的位置。否则就偏移ol       dCap个位置放置。低位和高位的结构分别放在loHead和hiHead里。当loHead不为空的时候。还会判断lc的数值。它记录的是loHead结构的节点个数。如果小于等于6个。就会把树结构转为
       链表结构。调用的untreeify方法。这个方法比较简单。就是遍历树。把TreeNode节点转为Node节点。如果lohead和hiHead都不为空。说明原来的树结构改变了。可能就违背了红黑树的性 质。就会重度调一下
treeify方法。
         * or untreeifies if now too small. Called only from resize;
         * see above discussion about split bits and indices.
         *
         * @param map the map
         * @param tab the table for recording bin heads
         * @param index the index of the table being split
         * @param bit the bit of hash to split on
         */
        final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }

            if (loHead != null) {
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map);
                else {
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

 

 

put差不多说完了,下面看一下get方法

/**
     * 返回指定的key对应的值。或者如果指定key对应的值就返回null,
     * 返回null并不一定意为没有指定key对应的值。也可能它的值就是null。这时
     * 可以用containsKey的方法来区分是否包含key。
    这个方法主体是,调用getNode方法获取节点,如果这个节点等于null就返回null。
    否则就返回节点的值。主要逻辑都在getNode方法里。下面看一下这个方法。
* * @see #put(Object, Object) */ public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }

 

   /**
     * 1 首先先判断这个table不等于null。table数组长度大于0。根据hash求得的下标对应的数组元素不等于null。才会进入里面的逻辑,否则就会直接返回null。
     * 2 然后首先比较找到的元素的hash和传入的hash是否相等,并且key相等。如果都相等。所以这个正好就是要找的节点。直接返回。
    3 否则,如果找到的节点还有next节点。就会遍历以找到的节点为头的链表。一个一个地比较hash和key是否相等。如果相等就返回找到的节点。
    4 如果找到的节点是TreeNode类型的节点,说明就是
一个红黑树。就会调用getTreeNode方法进行树结构的查找。
* @param hash hash for key * @param key the key * @return the node, or null if none */ 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) { // 1 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { 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)))) return e; } while ((e = e.next) != null); } } return null; }

 

 

     /**
         * 这个是先找到树的根节点,如果parent不为空,就说明它不是根节点,就通过root方法返回这个节点所在树的根节点,然后从根节点调用find方法查询。
         */
        final TreeNode<K,V> getTreeNode(int h, Object k) {
            return ((parent != null) ? root() : this).find(h, k, null);
        }

 

 

     /**
         * 前两个if else就跟普通的二叉查找树的逻辑差不多了。这里是以hash来比较大小的。
       如果p的hash大于传入的hash值,就去从p的左孩子继续找。如果p的hash小于传入的hash。就去从p的右孩子继续找。
       如果相同,就比较key是否相等,如果相等。说明找到了,就直接返回。否则就进入其它的elseif
* The kc argument caches comparableClassFor(key) upon first use * comparing keys.
*/ final TreeNode<K,V> find(int h, Object k, Class<?> kc) { TreeNode<K,V> p = this; do { int ph, dir; K pk; TreeNode<K,V> pl = p.left, pr = p.right, q; if ((ph = p.hash) > h) p = pl; else if (ph < h) p = pr; else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p; else if (pl == null) // 如果左孩子为空。就把右孩子赋值给p继续找。 p = pr; else if (pr == null) // // 如果右孩子为空。就把左孩子赋值给p继续找。 p = pl; else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0) //这时hash是不会相等的。并且左右孩子都不为空。就去看key是否有可比性,并且 根据key的比较结果还判断,是从左孩子继续找,还是从右孩子继续找。 p = (dir < 0) ? pl : pr; else if ((q = pr.find(h, k, kc)) != null) // 如果根据key也没有比较结果的话,那就干脆从右孩子继续找吧。 return q; else p = pl; } while (p != null); return null; }

 

下面看一下clear方法

   /**
     * 它只是遍历table数组,然后把每一个数组元素赋值给null
     */
    public void clear() {
        Node<K,V>[] tab;
        modCount++;
        if ((tab = table) != null && size > 0) {
            size = 0;
            for (int i = 0; i < tab.length; ++i)
                tab[i] = null;
        }
    }

 

remove方法

 

   /**
     * 如果指定的key存在,就删除指定key对应的键值对。
     * 返回值是key对应的值,如果没有对应key的键值对。就返回null。返回null
   * 并不意为着没有这个key的键值对,也可能是这个key对应的值就是null。 * 删除方法主要逻辑都在removeNode里。
*/ public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }

 

removeNode()

   /**
     * 这个方法,有一半逻辑都是在查找要删除的节点,这些内容在get方法里已经说过,不再细说。
     * 主要是在如果找到的node不为null。再进入具体的删除逻辑,这时候还会判断,matchValue,如果matchValue为true,当value值和找到的节点的值相等才会删除。
   * 如果找到的节点类型是TreeNode会调用removeTreeNode来进行删除,这一部分,主要还是红黑树的删除,不再细说。不了解红黑树的,最好还是先理解红黑树,不然不容易看懂。
   *
*
@param key的hash值 * @param key * @param key的值,在matchValue为ture的时候,会比较value的值,当key和value都相等时才删除。 * @param 在matchValue为ture的时候,会比较value的值,当key和value都相等时才删除。 * @param 当movable为false时,当删除一个节点时,不会移动其它节点。 * @return the node, or null if none */ 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) { 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; }

 

posted @ 2017-11-13 17:07  dupang  阅读(389)  评论(0编辑  收藏  举报