这一篇是接着上一篇写的,
上一篇的地址是:基于JDK1.8版本的hashmap源码分析(一)
/**
* 返回boolean类型的值,当集合中包含key的键值,就返回true,否则就返回false;和get(key)方法调用的是同一个底层实现方法getNode()
*/
public boolean containsKey(Object key) { return getNode(hash(key), key) != null; }
/** *这个方法主要是实现在map中存入key-value键值对,调用putVal方法实现的,在调用的方法里面会把已存在的key键对应的值进行替换,下面会在具体的方法中标注出来怎么具体实现 * @param key 与value值关联的value值,此值是唯一的,在map集合中不能重复
* @param value:可以通过key键过去对应的值,这个是随着key一起存入map中的,不是唯一的,但通过key可以找到一一对应的value值
*/ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * 此方法具体实现了如何在map集合中放入键值对 * * @param hash 此值是hash值,可以确定键值对放入到table表中的哪个位置下表
* @param key ,此值是唯一的,用于区分没一个放到map中的key值,hash值也是根据key值进行计算得出的
* @param value 此值是表示key对应的value值,每个键值对都有一个key-value,表示要放入到Node结点中的值 * @param onlyIfAbsent 是一个布尔类型,表示若这个标志是true,则不改变已经存在的value值 * @param evict 是一个布尔类型,若这个标记为false,则这个表是在创建模式
* @return 返回结果,若新的key-value键值对在map集合中不存在,则返回null,若新的key键已经在map集合中存在,则在put key-value键值对时会返回已经存在的key的value值 */ 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) //此处是判断表是否初始化,若没有初始化,则初始化并获取表的长度n n = (tab = resize()).length; //具体调用resize进行表的大小设置 if ((p = tab[i = (n - 1) & hash]) == null)//根据hash%n计算出新结点的下标,hash%n和hash&(n-1)是一样的效果,不过取模比移位计算要慢,若table中的位置为空
tab[i] = newNode(hash, key, value, null);//直接新建一个结点Node,放到对应的table中,table的下标是根据hash值按位与上(表的长度-1) else {//else表示此处是出现冲突的情况进行的处理 Node<K,V> e; K k; if (p.hash == hash && //此处是表示表中的位置出存放的是hash值相同,若table表中已存在的结点key值相等(地址相等或者值相等),则把第一个结点Node赋值为e结点 ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode)//此处判断table中的结点Node是不是树结点TreeNode,若是树结点,则把新结点添加到树中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {//此处表示若不是树结点,
for (int binCount = 0; ; ++binCount) {//循环链表 if ((e = p.next) == null) { //循环到链表的尾部,即最后一个链表结点处 p.next = newNode(hash, key, value, null); //把新建的结点添加到链表的最后 if (binCount >= TREEIFY_THRESHOLD - 1) // 此处是判断链表的长度是否是等于8,若是,则把链表转换为树结构,树可以增加查询效率 treeifyBin(tab, hash);//此处具体实现怎么把链表转换为树结构 break; } if (e.hash == hash && //此处是判断,当循环结点不为空时,判断链表中的key值是否和put的key值相等,若是,则直接退出, ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e;//把p结点的下一个结点赋值为p结点,进行下一次的循环 } } if (e != null) { // 此处表示表中若已经存在新结点中的key值,则添加新Node的时候,会返回map集合中已经存在的key的value值
V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue;//put进集合中的key值在原有集合中已存在,则会返回集合中已存在的value的值 } } ++modCount; if (++size > threshold)//添加元素后,会判断表的大小是否已经超过限定的大小(这个大小不是集合容量的大小,而是真正的大小*加载因子的值),一般会预留一部分用于缓冲 resize(); //此处表示,表容量的大小超过限定的大小时,对容器进行扩容。扩容是向左移一位,容量变为原来的2倍,不过有一些校验,后面会有分析 afterNodeInsertion(evict); return null;//若插入的结点key值不存在table中,则插入成功后,返回null } /** * 此处是实现table的初始化或者扩容的,如何table为空,则通过容量的字段值初始化容量,否则就把容量扩容为原来的2倍
* 集合中的元素要么是同原来一样的索引位置,要么是在新表中的2的指数倍偏移量
* @return 返回的是表,表的类型是Node结点的
*/ final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length;//此处判断表是否为空,为空就返回0,不为空就返回表的长度 int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { //表容量已经初始化后,判断容量是否已经大于等于表定义对的最大容量值,若大于最大容量值,则把int类型所能表示的最大值赋值给表大小限定值 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; //因为int类型是32位,能表示的最大值是:2147483647=2^31-1,最高位是符号位,而原有表容量的限定值是30位 return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && //此处判断:原来的容量左移一位是否会超过最大容量,并且是否大于最大设置的默认容量,如都成立 oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // 此处实现把原来的容量扩大为原来的一倍(即左移一位实现扩容) } else if (oldThr > 0) // 表示初始化容量被设置为阀值,创建hashmap时使用的是带参构造方法,里面会有阀值的初始值设置容量大小 newCap = oldThr; else { // 此处表示若没有使用带参的构造方法设置阀值的值,则使用默认的容量大小,程序中使用的默认容量大小时16
newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//此处新的阀值等于默认的加载因子0.75*默认的初始化容量 } if (newThr == 0) //当新的阀值为0时的情况 float ft = (float)newCap * loadFactor;//计算 新的容量*加载因子,结果小于文件中自定义的集合最大容量并且新容量也小于定义的集合最大容量,则取计算结果值,否则取int类型能表示的最大值 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? //获取新的阀值容量值 (int)ft : Integer.MAX_VALUE); } threshold = newThr; //此处是重新给阀值赋值 @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//此处是重写初始化新数组 table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) {//此处是循环扩容前的数组,其实就是需要把扩容前的数组中的元素放到扩容后的数组中 Node<K,V> e; if ((e = oldTab[j]) != null) {//此处表示数组中的结点不为空时,把扩容前的数组赋值给e结点 oldTab[j] = null; //把数组中的元素置null if (e.next == null) //若e结点后面没有下一个结点,则把e结点放到新表中,新表的下标计算方式为hash&(新表长度-1),其实是取余操作,不过移位比取余更快 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) //判断解决是不是树结点,若是树结点,因为1.8版本增加了红黑树,故需要在此处进行判断,然后进行树的转移
((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // 链表重新优化后重新放置元素的实现,先判断是否为空,在判断是否是树,最后再进行处理链表
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) {//此处是使用原有的元素与扩容前的长度按位与,结果只能是0和oldCap这2个值,若为0,就是原来的索引, if (loTail == null) loHead = e; //若头结点为空,则作为头结点赋值给loHead else loTail.next = e; //若结点不为空,则作为结点赋值给当前结点的下一个结点
loTail = e; } else { //这个分支是针对上个IF对应的,走的是原索引+oldCap,把节点作为hiHead结点或者后面结点,处理同上 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; //最终返回扩容后的表 }
因为hashmap的扩容是在其他条件的允许下,直接向左移一位,相当于在最高位增加了一位,在最高位与hash值进行按位与的时候,只能有2中情况,要么是0,要么是1。
这样就出现了上面的新表创建过程中的分情况,当高位按位与位0的时候,旧表到新表的元素位置索引不变;当高位按位与位1时,索引的下标增加了高位1这个位置的数值,增加数值的大小
是原有大小的1倍,故,新索引的计算就出现了:旧索引+旧表的长度,省去了重新获取hash值的过程。
/** * Replaces all linked nodes in bin at index for given hash unless * 当链表的长度到8的时候,会把链表转为树进行存储结点,根据传入的hash值,把所有链表的结点转为树,若链表的长度小于6时,会把树从新resize. */ 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) //当表为空或者表的长度小于初始定义的树的最小容量6时,调用resize(),进行重写设置大小 resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { //此处根据传入的hash值获取table数组中对应的元素结点 TreeNode<K,V> hd = null, tl = null; do { //此处是循环结点,把链表节点转换为树的结点 TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; //把第一个结点给hd,作为头结点 else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab);//此方法具体实现把一个链表转换为一个树的过程 } }
下面是remove方法的笔记
/** *如果存在key键,移除对应key值的元素映射结点 * @param key 需要删除的集合中的key值 * @return 返回null,或者value值,当map集合中有对应的key值是,就返回key值对应的value值,否则返回null值
*/ public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }
具体实现删除方法removeNode:
/** * 根据key值可以具体实现如何删除对应的value值方法 * * @param hash 这个值是可以确定key值对应的数据放到table中的哪个索引下
* @param key 这个是具体找到table中的索引后,进一步去比较同一个table索引下,后面是哪个引用,是具体在链表中进行比较使用的
* @param value 这个值是value值,有就进行匹配,没有进行忽视
* @param matchValue 这个布尔类型若为真,则表示相等的时候才删除
* @param movable 为false时,就不会删除结点信息
* @return 当删除的元素存在时,就返回删除的结点,若删除的结点不存在时,就返回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) { //判断表是否为空,根据hash值和表的长度,计算出需要删除的key值对应在table中的索引位置 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 = 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);//此处是循环跳出条件,循环体内找到对应的key值时,也会跳出循环 } } 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; //数据移除后,存放的数据元素减少1 afterNodeRemoval(node); return node; //此处是找到key值对应的结点 } } return null; //此处返回的是没有找到对应key值时的返回值 }
下面是比较重要的值hash值,这个值决定了元素存放在table中的索引位置,这个值的获取方式,也决定了冲突的多少,若这个值的大小取值范围小,则冲突就会越多。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
上面这个方法中,hash值是根据key的hashcode值获取的,hashCode是Object中的方法,是唯一定位一个对象的值
这个方法中先把key.hashCode无符号右移16位,然后与本身的数据决心异或,得出的值就是hash值,若key值为空,则hash值为0,int型是32位,右移16位可以把数据打算,减少冲突的一种方式。
好了,暂时hashmap源码学习笔记就到这里了,后续有时间会继续记录。