内容:本次源码分析的内容为HashMap的put(K k,V v)方法
主要流程:
一、先判断Entry数组是否被初始化了,没有则先做初始化的工作 二、Entry数组创建后,再判断key是否为空,为空则进入putForNullKey() 三、Entry数组创建了并且key不为null:
1、根据key进行哈希码的多次扰动处理得出hash 2、再根据key的哈希和数组长度减一作与运算计算出该key在数组上的位置(索引) 3、遍历索引对应的桶(链表),看是否能找到想要的节点(hash码和key都一致的那个key所在的节点),找到了就将传入的新value覆盖旧的value,并将旧value返回 4、走到这里说明key在数组中的索引那个桶没有找到想要的节点或者这个桶还未形成(头结点为null),需要进行添加操作,即步骤5 5、addEntry():先判断是否需要扩容:【元素个数大于等于阈值并且指定桶的头结点不为null】, 5.1、扩容 5.2、无需扩容,进入createEntry(),创建一个新的节点,将其next指向桶的头结点,然后将自己设置为桶的头结点
public V put(K key, V value) { if (table == EMPTY_TABLE) { // table尚未初始化,先做初始化的工作 inflateTable(threshold); } if (key == null) // key为null的情况的处理方式 return putForNullKey(value); // 根据key生成hash(进行过多次扰动操作, // 意在提高哈希码高位的使用率和散列性) int hash = hash(key); // 根据hash和数组长度计算出该key在数组中的位置 int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; // 遍历这个索引位置上的桶,找到哈希码和key一致的节点 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 找到了,将新值替换旧值,返回旧值 V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; // 没找到key所在的节点或者该位置还没有节点,那就创建一个新的节点 addEntry(hash, key, value, i); return null; }addEntry():
void addEntry(int hash, K key, V value, int bucketIndex) { // 判断是否要扩容(元素个数大于等于扩容阈值并且桶的头结点不为null) if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } // 真正进行添加节点操作 createEntry(hash, key, value, bucketIndex); }createEntry():
void createEntry(int hash, K key, V value, int bucketIndex) { // 获取桶的头结点 Entry<K,V> e = table[bucketIndex]; // 创建新的节点,将其next属性指向桶的原头结点, // 并将自己作为桶的新的头结点(也就是从头部插入到桶) table[bucketIndex] = new Entry<>(hash, key, value, e); size++; } Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }数组初始化操作:
private void inflateTable(int toSize) { // Find a power of 2 >= toSize // 返回大于等于入参的2次方的数 int capacity = roundUpToPowerOf2(toSize); // 扩容阈值=容量乘以加载因子 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; initHashSeedAsNeeded(capacity); } private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; } public static int highestOneBit(int i) { // HD, Figure 3-1 // 多次 将i 与 i位右移运算 做或运算,这样的目的是为了让i的低位都为1 i |= (i >> 1); i |= (i >> 2); i |= (i >> 4); i |= (i >> 8); i |= (i >> 16); // 再将上述运算后的结果(记为result) 与 result右移1位的结果 做减法,这样的到的结果就是(只有一个位是1,1后边的低位全部为0,这样的话就得到了一个小于等于初值的2的次方的数) return i - (i >>> 1); }key值为null的情况
private V putForNullKey(V value) { // 在数组上第一个桶里边找key为null的节点, // 找到了就将新值覆盖旧值,并且返回这个旧值 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; // 数组中第一个桶里边没找到空key对应的节点或者通里边根本没有值, // 就新建一个节点 addEntry(0, null, value, 0); return null; } void addEntry(int hash, K key, V value, int bucketIndex) { // 扩容操作 if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } // 真正进行创建工作的操作 createEntry(hash, key, value, bucketIndex); } // 真正创建节点的操作 void createEntry(int hash, K key, V value, int bucketIndex) { // 获取桶的头结点 Entry<K,V> e = table[bucketIndex]; // 新建一个节点,将其next指向桶的头结点, // 后将自己置为桶的新的头结点 table[bucketIndex] = new Entry<>(hash, key, value, e); // 元素总数自增1 size++; } // 扩容操作:主要两部分内容,1、创建原长2倍的新数组 2、将旧数组中的节点数据迁到新数组上,让table重新指向新数组并更新扩容阈值 void resize(int newCapacity) { // 获取老数组 Entry[] oldTable = table; // 老数组的长度 int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } // 创建一个2倍原长的数组 Entry[] newTable = new Entry[newCapacity]; // 迁移操作 transfer(newTable, initHashSeedAsNeeded(newCapacity)); // 迁移后,让table指向新数组,更新扩容阈值 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } // 转移操作【遍历老数组,从0开始遍历每个位置上的桶的节点,中间可能会对节点中的key进行重hash, //也就是决定将来迁移后在新数组上的位置,每次迁移都是将当前节点插到新数组中桶的头部】 void transfer(Entry[] newTable, boolean rehash) { // 新数组的容量大小 int newCapacity = newTable.length; for (Entry<K,V> e : table) { // 遍历老数组 while(null != e) { // 遍历每个数组中的桶 // 当前节点的下个节点 Entry<K,V> next = e.next; if (rehash) { // 重新哈希 e.hash = null == e.key ? 0 : hash(e.key); } // 根据哈希和数组长度计算出当前节点在新数组上 // 的位置(索引) int i = indexFor(e.hash, newCapacity); // 将当前节点的next指向新数组的桶的头结点 e.next = newTable[i]; // 将自己作为新数组上桶的头结点 //(也就是从头部插入,操作完成后, // 新数组上的桶上的节点和老数组的顺序是反过来的) newTable[i] = e; // 继续操作下个节点 e = next; } } }写在后面:本人水平有限,仅仅是将自己对知识的理解进行分享,如有错误之处,恳请大家批评指正,谢谢。_