您的位置:首页 > 编程语言 > Java开发

HashMap源码分析-基于JDK1.8

2017-11-13 17:07 931 查看
hashMap数据结构

类注释

HashMap的几个重要的字段

hash和tableSizeFor方法

HashMap的数据结构

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;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: