HashMap的小总结 + 源码分析
一、HashMap的原理
所谓Map,就是关联数组,存的是键值对——key&value。
实现一个简单的Map,你也许会直接用两个LIst,一个存key,一个存value。然后做查询或者get的时候,就遍历key的list,然后返回相应的value。
这样时间复杂度显然就是线性的,但这在map中已经是效率最低的get的方法了。而Hash主要提高效率的,也就是在这个位置——key的定位和查询这。
在数据结构中,我们学了hash这一技术,也就是散列表的技术。我们把整个表格看作是许多许多的空桶,然后散列函数也就是hash函数(拿质数来取模是一个很经典的hash的方法)会把你传入的参数处理后,散列到这些桶中。一个完美的哈希函数呢,是可以将你的输入无冲突的散列到表格中,也就是你传入的参数一人进一个桶,互相之间不冲突。但这是不可能的,然后数据结构中学到了很多处理冲突的方法,有链表处理法——就是在桶中冲突的元素用链表将他们存起来;还有线性探测法等,这些大概就是碰到冲突,然后根据一些算法来换位置,再冲突就再换。
Java的HashMap就是用散列表这一技术来存key和value。在HashMap中,我们hash函数的对象是Key instance,用一个Node[][]的二维数组来做table。这个Node是结点,一个Node代表着一个key和一个value,可以理解成HashMap中存储的一个对象。这个table可以看作是桶群,每个node都是一个桶。
当然传进hash函数的不可能直接是这个Key的实例对象,而是它的hashCode()方法产生的一个hash code。这个hashCode()方法是基类Object的一个方法,如果你不对这个方法进行重写的话,hashCode()方法返回的是根据这个对象的地址生成的一个int的散列码。
jdk对这个key instance的hashcode进行一个处理后,然后将它散列出一个值,作为桶的index,然后将这个key代表的node放进桶里面。对hashCode的处理会在下面的源码分析中介绍到。
jdk1.8后的hashMap还对数据结构做了一个处理,当一个桶冲突的链表太长了的时候,会把链表改成红黑树,然后当冲突小了的话,又会变回链表。
补充:看了源码后发现,只是对那个桶的链表进行转换。
tips:如果你的类直接当作key在hashMap中使用的话,equals和hashCode这两个方法用的都是Object默认的,也就是说主要比较的是对象的地址。如果你想根据你的类的实例的内容来进行散列的话,请重写hashCode;如果你想通过你的类的实例的内容来进行key的查找的话,请重写equals方法。
而且你重写的hashCode不一定要是unique的,但你重写的equals方法一定要严格区分不同的对象!!
可以参考String这个例子,这个类就可以很好地在hashMap中当key使用,它重写了hashCode()和equals(),都是根据String的内容来生成的。
二、源码重要部分解析
类的声明部分:
继承了AbstractMap,这是个Map接口的简单实现类。然后实现了Map、Cloneable接口(之前的博客有讲过,这里是为了实现浅复制)还有序列化的Serializable接口。
常量部分:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
首先这个capacity指的是HashMap中的桶表格——table (下面会说到)所分配的内存。所以这个常量的意思是table的初始length也就是初始化的长度,为16。
static final int MAXIMUM_CAPACITY = 1 << 30;
table的capacity的最大值,如果用户在构造器中间接指明的的参数大于等于这个最大值的时候,table的capacity就会取这个。这个值是2的30次方。
这里我们会看到,而且源码中的备注中也有写到,这里的capacity一定要是2的整数次幂,因为这样才能很方便地用&、^等位操作符来进行一些运算,速度更快,下面的hash算法还有很多算法中会看到。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
这个参数是加载因子,load_facotr = size/capacity,也就是map中存储的entry(键值对实体)除以table数组容量的值。这个值很重要,当这个facotr到达了这个数值,map就会进行扩容操作。如果在Map的构造器中没有特别指定load_factor,用的就是0.75.
static final int TREEIFY_THRESHOLD = 8;
这个值是说,table中,如果有桶(bucket)的链表的长度大于8,就有可能把所有的链表变成红黑树,是转变成树的一个阈值。
static final int UNTREEIFY_THRESHOLD = 6;
这个值是退化回链表的一个阈值,在扩容操作的时候,如果桶中的node数目小于6就变回链表喔。
static final int MIN_TREEIFY_CAPACITY = 64;
这个值是在转变成树之前,还会有一次判断,只有键值对数量大于 64 才会发生转换。这是为了避免在哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化。
嵌套类Node的声明
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {//这应该是普通节点的定义
final int hash;
final K key;
V value;
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; } public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);//大概是想让node的hash值和key和value都有关系吧
} public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
} 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;
}
}
这个Node的声明在LinkedHashMap还有TreeNode中都会用到,是他们的爸爸和爷爷。
这个类就是一一个链表结点,是每个桶的最常规的结点类。
类变量有一个final的hash,代表的是这个Node的key instance的处理过的hash值。(就是这个Key instance的hashCode()值经过处理后的一个值,代表这个Key instance);finall Key还有V value;Node next是指向下一个Node的指针,只有一个Next指针,可见这是个单向链表。
重写了hashCode()方法——key的hashCode()和value的hashCode()相亦或;重写了equals()方法——需要对key和value都进行比较。
hash方法——扰乱函数
static final int hash(Object key) {
//扰乱函数 h无符号右移16后,相当于处以高位的16位被移到了低半段的16位,这个时候高半段都是0
//然后再和本来的h做亦或,其实就是h的高半段和低半段做亦或(因为移位后的h前面都是0嘛),
//这样就混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
在本文的HashMap的原理介绍中有提到,我们会对Key instance的hashCode()进行一个处理后再序列化到table的桶中,这个函数就可以看作是那个处理的算法。具体的分析都写在注解中了。
得到的是一个随机性很高的只有低半位的整形,后面序列的时候我们会用一个方法只取这个整形的低位部分。
一个很好玩的方法——tableSizeFor()
/**
* Returns a power of two size for the given target capacity.
*/
//给一个整数,返回大于输入参数且最接近的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;
//累计移了31位,这段代码其实就是把最高位的1后面的全部变成1
//0100变成0111,4变成7 全是1加个1就变成了2的整数幂了,7+1=8返回
//所以第一步要先把参数减1,假如传进来是4,不减一的话就直接得到8返回显然错了
//减一再处理就是3=0011,处理完就是返回0011+0001=0010=4正确
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这个方法的作用和解析都写在注解中了。以前没怎么用过移位运算符的我觉得好厉害哈哈哈。
类变量声明部分
transient Node<K,V>[] table;//HashMap中的那个桶数组,键值对就是散列在这个Node,这个table分配的容量是capacity。
transient Set<Map.Entry<K,V>> entrySet;
这个类变量就是一个装了HashMap的所有键值对实体的一个Set。
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
注释说的很清楚了,map中键值对的数量。
transient int modCount;
//这个field是用来标识HashMap的结构被修改的的次数,比如键值对的添加或者是内部结构的改变,像覆盖update这些操作不算。
//这个是用来给 iterators做fail-fast机制用的,就是iterator的时候如果HashMap还被别的进程改变会抛出异常的机制。
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;
//准备扩容的阈值吧 说如果table array没有分配,这个值是原始array的capacity喔
这个值是capacity*load_factor得到的值,也就是说,当HashMap中的键值对的数量超过这个的时候,就要考虑扩容了。
这个值其实困扰了我蛮久的,注解上说什么如果table还没allocated,就设置为默认的array capacity……这里还真有点不懂,是指initial capacity对应的threshold吗?
这个threshold其实应该理解成,这个HashMap应该容纳的node的个数,因为一超过这个值就要扩容嘛,相当于这个HashMap的“capacity”。而且我们注意,HashMap的类变量中没有capacity这个变量!!所以其实构造器还有初始化只是和这个threshold在打交道。
构造方法系列
HashMap中有四个构造方法:
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
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);//一开始
} /**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
} /**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
} /**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
第一个是最典型的,用户声明hashMap容量还有load_factor。注意,这个initialCapacity其实应该指的是HashMap的容量,因为它是经过tableSizeFor()处理后再赋值给threshold。
第二个只是指定了initialCapacity,直接调用了第一个构造器方法。
第三个是什么都没指定,构造器里只是对threshold进行了默认赋值,素以这个构造器调用的话,获得的HashMap的instance的threshold应该为0.
第四个传进来的是个Map,然后调用putMapEntries()方法。
补充:
(下划线部分有误)
后面发现这个initialCapacity还是设的是table的capacity,因为在resize()方法中,要给新的table分配内存的时候,用的就是这个initialCapacity,只不过用的是这个threshold来传值。所以第一点说的initialCapacity是不对的,它指的还是HashTable中的table的容量。
但threshold是这个hashMap的容量是没错的,因为确实超过这个值就要扩容或者进行操作。
putMapEntries()方法
如果是构造器调用的初始化的话,这个evict参数就为false,否则的话就是true(putAll方法就是true)
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();//传进来的map的键值对的个数
if (s > 0) {
if (table == null) { // pre-size
//说明这个table还没分配内存初始化 float ft = ((float)s / loadFactor) + 1.0F;
//ft是,如果根据传进来的map的node的数量,创建table分配table,应该table的capacity是多少 int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold) //给threshold赋值
threshold = tableSizeFor(t);
}
else if (s > threshold) //说明m的个数比所在map最多可以存储的node数量要多,所以要扩容或者是为table分配内存。
resize();//这个方法可以扩容和为table分配初始的内存。 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
//利用entrySet获取这个Map中所有node的一个Set
//然后调用putVal()方法来为本HashMap添加node,或者说添加键值对。 K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
这个方法是putAll()方法还有构造器传map进来的话,这两个方法的内部相关实现。
这个float ft = ((float)s / loadFactor) + 1.0F;这一行代码中为什么要加1.0F我不太懂,网上有说是可以节省一次resize(),也有说精确小数点后几位……emm不知道。
关于这个putVal()方法,在下面会介绍到。
get方法和getNode
public V get(Object key) {
//直接调用getNode()然后返回node的key
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
} /**
* Implements Map.get and related methods
*
* @param hash hash for key 这个key instance的经过hash()过的hash值
* @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) {
//进来后tab是当前table,n是table的capacity,first是这个key对应的那个位置的桶的第一个node if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//看这个桶的第一个node的key是不是这个要get的key
//因为有些对象的引用不一样,但equals是一样的,像是文字一样的String对象 //看了后面的代码发现,很多涉及查找的都是这样先桶的第一个node进行比较。 return first;
if ((e = first.next) != null) {
//第一个node不是要get的那个key,而且有下一个元素,也就是可能是链表也可能是树 if (first instanceof TreeNode)//如果是树,则交给树的getTreeNode的实现来完成
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;
}
具体看里面的注解就行了。
put方法
public V put(K key, V value) {
//调用putVal方法
return putVal(hash(key), key, value, false, true);
} /**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value这个值如果为true,就不改变原来的值,就如果put是覆盖原来的key的值,这个又为true的话就不能改变喔,虽然不知道哪里用hh
* @param evict if false, the table is in creation mode.这个值如果为false的话,这个table就处于一个创造的模式,就是那个传map的构造器在初始化当前map,然后间接调用的这个方法。
* @return previous value, or null if none
*/
//put方法里面调用的后面的两个参数分别是false和true,说明可以覆盖原来的key的value;不是一个creation mode
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)
//进来这里的话说明这个table还没分配空间呢,tab是table,然后n是table的capacity
n = (tab = resize()).length;//这一步相当于调用resize()方法为table分配内存并返回一个node数组作为table,然后将这个新的table的capacity给n if ((p = tab[i = (n - 1) & hash]) == null)
//p是这个要put的key的对应的table中的那个桶的第一个node,i是这个key对应的那个桶的index
//如果这个桶为空,说明还没冲突,就新建一个普通的结点。 tab[i] = newNode(hash, key, value, null);
else {
//否则就是有冲突了,可能这个桶下是链表也可能是树 Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//先判断桶中的第一个node的key是不是这个要put的key,是的话将这个node赋值给e e = p;
else if (p instanceof TreeNode)
//如果第一个不是那个key,先看这个node是不是树,是的话交给树的操作。 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {
//说明这个桶下面接的是链表,而且第一个不是正确的要put的key,那就遍历链表找咯 for (int binCount = 0; ; ++binCount) {//遍历的时候要数着node的数量,因为可能添加的时候load_factor要超过,要变成树。
if ((e = p.next) == null) {//e往下遍历
//这句之后,e就是下一个结点,如果进入这个条件里面,说明链表已经到头了
//而且还没找到那个key,所以就插入新结点了
//插入后break,这时候e指向null p.next = newNode(hash, key, value, null);//这一步就是插入的那个语句 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//添加了新结点嘛,然后就要看这个桶的链表的node数量过多没,过多可能就要进行树化操作。
//在treeifyBin方法中还有个判断map中键值对总数超过64没的操作,超过了才树化。 treeifyBin(tab, hash);//这个方法的操作是,把这个桶的单向链表变成树
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//如果遍历中发现有相同的key,就跳出来
//这个时候e指的是key和要put的那个key相等的node
break;
p = e;
}
}
if (e != null) { // existing mapping for key
//e不为null的话,说明上面的操作找到了 一个key和要put的可以一样的node
//这个e指的就是那个key和put那个key一样的node V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)//条件允许,覆盖
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//到了这里说明这个key还不存在,那么就要insert一个node,hashMap结构要改变所以这个值加一
if (++size > threshold)
//插入后size大于阈值, resize();
afterNodeInsertion(evict);
return null;
}
关于这个treeifyBin()方法,下面也简单介绍下:
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
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)
resize();//这个情况下,还不至于进行结构的转换,只要扩容就好。
else if ((e = tab[index = (n - 1) & hash]) != null) {
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);
//这一步做的,其实就是1.把所有的node结点变成TreeNode
//2.把单向链表变成双向链表 if ((tab[index] = hd) != null)
//这一步就把桶的首元素变成这个双向链表的头结点 hd.treeify(tab);//把这个双向链表变成树
}
}
一开始我以为,一旦发现需要变树,是把所有桶的链表都变成树,看到这里才发现只是变这个桶的链表。
可以看到,这里的treeifyBin()方法其实还不是把链表变成树,只是把结点都变成了TreeNode,然后还把单向链表变成了双向,然后最后调用
树头指针.treeify(tab)
这个语句才是把这个双向链表树化,应该是这样转换后,可以更方便地写转换成红黑树的代码吧。
关于红黑树的构造就不在这里细讲了。
然后是我们很重要的resize()方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//获取当前的桶表格table
int oldCap = (oldTab == null) ? 0 : oldTab.length;//获取当前的table的capacity
int oldThr = threshold;//获取当前的阈值,就是现在这个HashMap能够装几个node int newCap, newThr = 0;
if (oldCap > 0) {
//进入这里说明table已经有了,准备进行扩容 //补充一下,如果当前有table,但table的capacity经过double后大于maxCap的话,下面两个if都不进入
//这种情况经过下面两个if后,newCap的值为大于maxCap的double * oldCap;newThr为0
//这个情况我们叫做%%,等等好描述 if (oldCap >= MAXIMUM_CAPACITY) {
//当前table的capacity大于maxCapability
//就把阈值赋值为Integer的max
//然后什么都不做返回当前table //一开始很奇怪,为什么当前capacity要和max比较,应该不可能大于max啊,后面分析其实是有可能的 threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//这个else应该就符合扩容的要求,准备扩容
//进入这个if的话,就newCap是当前capacity的double而且小于maxCap
//而且当前cap也正常,所以可以double //下面就阈值double,也就是newThr的值是当前的阈值的double newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//这里我们叫做情况#@好了,等等好描述
//这个else是说明还没有table
//这里的if说明用户在构造器中指定了initialCapacity newCap = oldThr;
//在构造器代码中,用户将initialCapacity传给了threshold,
//所以当前threshold传给这个新的capacity,完全没毛病 //可见这个情况下,newThr的值还是0 else { // zero initial threshold signifies using defaults
//这里就说明,没有table,而且构造器中没有指定threshold,也就是调用的是无参构造器
//所以newCap和newThr都为默认值。 newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//看上面的分析就知道,有两种情况newThr会为0:
//#@的情况,也就是没有table,用户指定了initialCapacity
//%%的情况,也就是有table但两倍oldCap大于MaxCap的情况 //根据用户给的initialCapacity算threshold
//或者根据大于maxCap的newCap算出它对应的threshold
float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
//根据newCap给newThr赋值,newCap太大的话就给newThr赋值为整型的最大值 //这里看到,如果是%%情况,newCap是不会变的,也就是还是>maxCap,所以是存在capacity大于maxCapacity的情况的,
//但这个不怕它越界,因为即使是maxCap*2也不会大于Integer的maxValue }
threshold = newThr;//好的,现在要更新这个阈值了 @SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//根据newCap来分配table的内存 table = newTab;赋值给table,真正扩容或者分配内存 if (oldTab != null) {
//这里做的,是如果是扩容操作,那么要把旧的table中的东西迁移到现在的table中 for (int j = 0; j < oldCap; ++j) {
//对旧table的桶进行遍历 Node<K,V> e;
if ((e = oldTab[j]) != null) {
//e是不为空的那个桶的首个node oldTab[j] = null;//将这个桶的引用置空,然后交给gc处理 if (e.next == null)
//说明这个桶没有冲突,只有一个node
//那么就直接根据散列算法取hash低位然后散列到扩容后的table中去
newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode)
//这个else就意味着,桶下面的可能是链表或者树
//这里是树的情况,就交给树的split操作来处理
//这里应该有如果数量下来了然后又变回链表的操作 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order
//这里是桶下面是个链表的情况,要做的是将这个桶下的链表变成两条链表,然后分别散列到新table中的两个桶中 Node<K,V> loHead = null, loTail = null;//一号新链表的头指针和用来遍历的尾指针
Node<K,V> hiHead = null, hiTail = null;//二号新链表的头指针和用来遍历的尾指针
Node<K,V> next;//用来遍历原来旧表中那个桶的链表的指针 do {
next = e.next;//next往下移 //这个e.hash & oldCap有两种情况,要么就等于oldCap,要么就等于0
//因为cap都是2的整数次幂嘛,所以都是10000的形式,也就只有最高位为1
//对旧链表中每个结点都进行这个&操作,就把node分成两类,形成两个链表
if ((e.hash & oldCap) == 0) {
//1号新链表的构造 if (loTail == null)//第一次遍历,先把头指针赋值
loHead = e;
else
loTail.next = e;
loTail = e;//可见这里用的是尾插法,jdk7好像用的是头插法,然后头插法好像在多线程环境下可能变成环然后死循环。
}
else {
//2号新链表的构造 if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null); if (loTail != null) {
loTail.next = null;//2号新链表收尾
newTab[j] = loHead;//j是旧桶在旧table中的index,新table中的这个桶第一个node变成一号新链表的头指针
}
if (hiTail != null) {
hiTail.next = null;//2号新链表收尾
newTab[j + oldCap] = hiHead;//新table中的与1号链表相隔oldCap个距离的桶第一个node变成二号新链表的头指针
}
}
}
}
}
return newTab;
}
最后讲一下这个新的两条链表的操作。
如果还是按照hash & cap - 1的操作的话,其实还是很不分散。因为你hash是不会变的嘛,然后cap在旧的基础上翻倍后,也就比之前多了一位。
也就是说hash & cap - 1就比之前那个旧的桶的index多了一位,那么原来桶中的这个index很可能仍然是旧的那个值。
而现在这个直接用旧桶的index然后另一个新链表的index为就桶index + oldCap的操作,就:
1. 让node在新table中的散列结果更分散。
2. 减少计算量。
putAll方法
/**
* Copies all of the mappings from the specified map to this map.
* These mappings will replace any mappings that this map had for
* any of the keys currently in the specified map.
*
* @param m mappings to be stored in this map
* @throws NullPointerException if the specified map is null
*/
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
调用putMapEntries()方法,然后这里不是初始化table,所以第二个参数传true。
remove方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
} /**
* Implements Map.remove and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored 如果下面那个参数为true,这个value参数就有用
* @param matchValue if true only remove if value is equal 如果这个为true,那么只有在这个key对应的value和上面那个参数value相同才删除
* @param movable if false do not move other nodes while removingfalse的话,删除的时候不移动其他node喔
* @return the node, or null if none找不到node返回null
*/
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) {
//tab为table,n为capacity,p为key那个桶位的第一个node Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//判断第一个是不是那个要删除的key的node,是的话赋值给node node = p; else if ((e = p.next) != null) {
//如果第一个不是要删除的node
//而且还有下一个,也就是下面可能是链表或者树 if (p instanceof TreeNode)
//如果这是棵树,交给树的getTreeNode node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else {
//否则,这里就是个链表,那就遍历找要删除的那个key呗
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
//找到,就赋值给node
//然后break出来 node = e;
break;
}
p = e;//p总是指向遍历中的那个e的爸爸,如果break出去的话,p就是要删除的那个node的爸爸
} while ((e = e.next) != null);
}
} if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//node不为空,说明找到了要删除的node
//而且如果matchValue,要删除的node的value和传进来那个相同 if (node instanceof TreeNode)
//如果要删除的是树结点,交给树的操作 //大概瞄了一眼,里面有如果node数量太少,变回链表的操作
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p)
//node不是树结点,而且要删除的node就是桶里面的首结点
//因为这个node==p只可能出现在,桶里面第一个元素就是要删除的node这种情况下 tab[index] = node.next;//那么就桶的首元素的next直接指向node的下一个咯
else
//不是树,而且要删除也不是第一个元素
//p是node的爸爸,素以直接p的next指向node的next
//剩下的就交给gc,Java就是爽哈哈哈 p.next = node.next; ++modCount;//删除了结点,hashMap的结构肯定改变了啊,那就这个值加一
--size;
afterNodeRemoval(node);//一个linkedHashMap的回调函数
return node;//返回删除的结点
}
}
return null;
}
Clear方法
就是遍历table然后把数组元素的引用都置空,gc舒服啊
/**
* Removes all of the mappings from this map.
* The map will be empty after this call returns.
*/
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;
}
}
containsKey和containsValue方法
containsKey源码:
/**
* Returns <tt>true</tt> if this map contains a mapping for the
* specified key.
*
* @param key The key whose presence in this map is to be tested
* @return <tt>true</tt> if this map contains a mapping for the specified
* key.
*/
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
containsKey方法就直接调用getNode方法,看找不找得到这个key的node
containsValue源码:
/**
* Returns <tt>true</tt> if this map maps one or more keys to the
* specified value.
*
* @param value value whose presence in this map is to be tested
* @return <tt>true</tt> if this map maps one or more keys to the
* specified value
*/
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
这个可读性也很高,就是遍历table的每个桶,每个桶再单向遍历链表。当然,只有桶中有node才会去遍历链表。
但这里有个问题,如果桶里面是红黑树,这里为什么也能用单向链表e = e.next的方式去遍历??
可能只有在迟点了解学习红黑树才能懂。
emmm我认为HashMap这些方法是比较重要的,HashMap的第一次源码学习就到这了,接下来应该学下红黑树。(数据结构课上竟然没教)
附:
1.7的HashMap和1.8的HashMap的区别——https://blog.csdn.net/qq_36520235/article/details/82417949
HashMap的小总结 + 源码分析的更多相关文章
- HashMap实现原理及源码分析之JDK8
继续上回HashMap的学习 HashMap实现原理及源码分析之JDK7 转载 Java8源码-HashMap 基于JDK8的HashMap源码解析 [jdk1.8]HashMap源码分析 一.H ...
- 每天学会一点点(HashMap实现原理及源码分析)
HashMap实现原理及源码分析 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希 ...
- 【转】HashMap实现原理及源码分析
哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景极其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出 ...
- java-通过 HashMap、HashSet 的源码分析其 Hash 存储机制
通过 HashMap.HashSet 的源码分析其 Hash 存储机制 集合和引用 就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并非真正的把 Java 对象放入数组中.仅仅是把对象的 ...
- HashMap实现原理及源码分析
哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出 ...
- HashMap实现原理及源码分析(JDK1.7)
转载:https://www.cnblogs.com/chengxiao/p/6059914.html 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技 ...
- HashMap实现原理及源码分析之JDK7
攻克集合第一关!! 转载 http://www.cnblogs.com/chengxiao/ 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如m ...
- HashMap实现原理及源码分析(jdk1.8)
HashMap底层由数组+链表+红黑树组成,可接受null值,非线程安全 1.基本属性 transient Node<K,V>[] table; //hashmap数组 static fi ...
- 2、JDK8中的HashMap实现原理及源码分析
本篇提纲.png 本篇所述源码基于JDK1.8.0_121 在写上一篇线性表的文章的时候,笔者看的是Android源码中support24中的Java代码,当时发现这个ArrayList和Linked ...
随机推荐
- Codeforces Round #303 (Div. 2) D. Queue —— 贪心
题目链接:http://codeforces.com/problemset/problem/545/D 题解: 问经过调整,最多能使多少个人满意. 首先是排序,然后策略是:如果这个人对等待时间满意,则 ...
- Appium——Error while obtaining UI hierarchy XML file:com.android.ddmlib.SyncException:
使用uiautomatorviewer查看页面元素时报这个错误,解决办法 cmd: adb root ok 解决
- IOS下WEBVIEW 的javascript数组与json定义 及交互
最近在折腾IOS新闻浏览客户端,当中需要用到webview传递JSON数据到IOS上,然后在IOS上解析.刚入门IOS不久,看了不少的书,但都是囫囵吞枣.在开发过程中,遇到不少问题.开发环境mac m ...
- C#开发遇到的常见问题及知识点
今天遇到的类型初始值设定项引发异常的原因是:类没有添加[Serializable]属性. this.DialogResult = System.Windows.Forms.DialogResult.O ...
- 开发自己的composer包
1. 创建一个开发目录 mkdir project cd project 2. 利用composer生成一个composer.json composer init > Welcome to th ...
- Java丨时间判断谁前谁后
直奔主题: String date_str1 = "2016-06-02 23:03:123"; String date_str2 = "2016-06-03 03:03 ...
- nyoj 86 --位标记
nyoj 86 --位标记 点击打开题目链接 : 找球号(一) 这道题目很多解法,其他解法请参考 http://www.cnblogs.com/play ...
- bzoj 2600 ricehub
2600: [Ioi2011]ricehub Time Limit: 10 Sec Memory Limit: 128 MBSubmit: 783 Solved: 417[Submit][Stat ...
- AJAX获取数据,需要添加事件
如果是通过异步请求从后端获取的数据来渲染页面,要添加事件,必须要在页面已有的元素上,添加时间代理.因为页面渲染需要时间,如果直接绑定在响应时间元素上面,很有可能触发不了事件.
- c语言里如何调用汇编里的变量?
c语言里如何调用汇编里的变量? 汇编语言:是声明全局变量 .globl _end_ofs _end_ofs: .word _end - _start c语言:声明这个变量,然后再调用这个变量 void ...