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实例有有两个属性影响它的性能:CAPACITY和LOAD_FACTOR。CAPACITYhash表里桶的数量,并且初始的CAPACITY仅仅是hash表创建时的容量。LOAD_FACTOR是hash表在自动地增加它的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

 /**
* 基本的bin节点, 被用于表示大部分的数据条目。
*
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;         // 这个记录的是K的hash值
final K key;           // map的键
V value;             // map的值
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; }
     // 节点的hashCode,key的hashCode和value的hashCode的异或 
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
} public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
     // 重写的equals,如果节点的key和value都相等,两个节点才相等。
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;
}
}

静态的工具方法

/**
* 计算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值为样例测试。代码如下

   

  

 /**
* 描述:
* 日期:2017年11月13
* @author dupang
*/
public class DupangTest {
public static void main(String[] args) {
Float f1 = 1f;
Float f2 = 2f;
Float f3 = 3f;
Float f4 = 4f; String f1_hashCode = Integer.toBinaryString(f1.hashCode());
String f2_hashCode = Integer.toBinaryString(f2.hashCode());
String f3_hashCode = Integer.toBinaryString(f3.hashCode());
String f4_hashCode = Integer.toBinaryString(f4.hashCode()); System.out.println(f1_hashCode);
System.out.println(f2_hashCode);
System.out.println(f3_hashCode);
System.out.println(f4_hashCode); int size = 198;
int f1_index = f1.hashCode()&(size-1);
int f2_index = f2.hashCode()&(size-1);
int f3_index = f3.hashCode()&(size-1);
int f4_index = f4.hashCode()&(size-1); int f1_index_1 = hash(f1)&(size-1);
int f2_index_2 = hash(f2)&(size-1);
int f3_index_3 = hash(f3)&(size-1);
int f4_index_4 = hash(f4)&(size-1); System.out.println(f1_index);
System.out.println(f2_index);
System.out.println(f3_index);
System.out.println(f4_index);
System.out.println("=========华丽的分割线===========");
System.out.println(f1_index_1);
System.out.println(f2_index_2);
System.out.println(f3_index_3);
System.out.println(f4_index_4);
} static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
}

输出结果如下

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

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

     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) // 1
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) // 2
tab[i] = newNode(hash, key, value, null);
else { // 3
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) // 3.1
e = p;
else if (p instanceof TreeNode) // 3.2
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) { // 3.3
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) // 3.3.1
break;
p = e;
}
}
if (e != null) { // 3.4
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; // 4
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
  /**
* 最常用的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) // 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;
}

HashMap源码分析-基于JDK1.8的更多相关文章

  1. HashMap 源码分析 基于jdk1.8分析

    HashMap 源码分析  基于jdk1.8分析 1:数据结构: transient Node<K,V>[] table;  //这里维护了一个 Node的数组结构: 下面看看Node的数 ...

  2. CopyOnWriteArrayList 源码分析 基于jdk1.8

    CopyOnWriteArrayList  源码分析: 1:成员属性: final transient ReentrantLock lock = new ReentrantLock();  //内部是 ...

  3. HashMap源码分析(基于jdk8)

    我们知道在jdk7中HashMap的实现方式是数组+链表.而在jdk8中,实现有所变化,使用的是数组+链表+红黑树实现的. 当链表长度达到8时转化为红黑树. static final int TREE ...

  4. ArrayList 源码分析 基于jdk1.8:

    1:数据结构: transient Object[] elementData;  //说明内部维护的数据结构是一个Object[] 数组 成员属性: private static final int ...

  5. LinkedList的源码分析(基于jdk1.8)

    1.初始化 public LinkedList() { } 并未开辟任何类似于数组一样的存储空间,那么链表是如何存储元素的呢? 2.Node类型 存储到链表中的元素会被封装为一个Node类型的结点.并 ...

  6. ArrayList的源码分析(基于jdk1.8)

    1.初始化 transient Object[] elementData; //实际存储元素的数组 private static final Object[] DEFAULTCAPACITY_EMPT ...

  7. 源码分析系列1:HashMap源码分析(基于JDK1.8)

    1.HashMap的底层实现图示 如上图所示: HashMap底层是由  数组+(链表)+(红黑树) 组成,每个存储在HashMap中的键值对都存放在一个Node节点之中,其中包含了Key-Value ...

  8. JDK1.8 HashMap源码分析

      一.HashMap概述 在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里.但是当位于一个桶中的元素较多,即hash值相等的元素较多时 ...

  9. 【JAVA集合】HashMap源码分析(转载)

    原文出处:http://www.cnblogs.com/chenpi/p/5280304.html 以下内容基于jdk1.7.0_79源码: 什么是HashMap 基于哈希表的一个Map接口实现,存储 ...

随机推荐

  1. 转 C#高性能Socket服务器SocketAsyncEventArgs的实现(IOCP)

    原创性申明 本文作者:小竹zz  博客地址:http://blog.csdn.net/zhujunxxxxx/article/details/43573879转载请注明出处引言 我一直在探寻一个高性能 ...

  2. Delphi 使用TAdoQuery执行存储过程的样例

    procedure TCustomerForm.FindCustomerInfo;var  strSql:string;begin //  BL_HV_FindCustomerInfo 存储过程的名称 ...

  3. golang build 的简单用法.(菜鸟初学)

    1. golang 里面的 go build 可以编译代码. go build helloworld.go 2. 这里面有一个注意事项事项. 如果引用非go语言的 内置package的话 需要在环境变 ...

  4. SpringBoot(四)_Spring Data JPA的使用

    JPA 绝对是简化数据库操作的一大利器. 概念 首先了解 JPA 是什么? JPA(Java Persistence API)是 Sun 官方提出的 Java 持久化规范.它为 Java 开发人员提供 ...

  5. Kafka日志存储原理

    引言 Kafka中的Message是以topic为基本单位组织的,不同的topic之间是相互独立的.每个topic又可以分成几个不同的partition(每个topic有几个partition是在创建 ...

  6. 计算机网络【9】—— HTTP1.0和HTTP1.1的区别及常见状态码

    一.HTTP1.0与HTTP1.1的区别 1.HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理 HTTP 1.0规定浏览器与服务器只保持 ...

  7. Spring(2):Spring Ioc

    1.下载spring-framework-3.2.0 完整包下载路径: https://repo.spring.io/webapp/#/artifacts/browse/tree/Properties ...

  8. SDOI2017硬币游戏

    题面链接 洛咕 sol 神题,幸好我不是SD的QAQ. 假设你们都会\(O(n^3m^3)\)的高斯消元,具体来说就是建出\(Trie\)图然后套游走的板子. 然后我们发现可以把不能匹配任何串的概率压 ...

  9. BZOJ 3224 普通平衡树 | 平衡树模板

    #include <cstdio> #include <cmath> #include <cstring> #include <algorithm> # ...

  10. 针对Weblogic测试的一些小总结(转)

    1. 管理员登录页面弱密码 Weblogic的端口一般为7001,弱密码一般为weblogic/Oracle@123 or weblogic,或者根据具体情况进行猜测,公司名,人名等等,再有就可以用b ...