前沿
jdk1.8+结构,由数组 + 链表 + 红黑树 组成,用于存储 K-V键值对;HashMap属于散列表,实现Map接口的集合。
所有K (真实)储存在Set<K>集合中,并且确保唯一、不变,若后者加入的K已存在,直接把对应的V值覆盖掉;K允许为null(且只允许存在一个null)。
所有V(表象上)存储在Collection<V>类型的List<V>中,V值可重复,V允许为null。
K-V 键值对,一一对应,唯一,并且存储在 Set<Entry<K,V>>集合中。
HashMap的结构图如下:

源码解析
上面所述不够详细,可能看着有点空洞,那开始从源码着手慢慢领悟,就会比较具象明晰。
HashMap的属性
public class HashMap<K, V> extends AbstractMap<K, V>implements Map<K, V>, Cloneable, Serializable {//todo 唯一的序列号,供序列化时使用。private static final long serialVersionUID = 362498820763181265L;//todo 默认容量,1向左移位4个,00000001变成00010000,2^4=16;计算机底层用位运算,自然执行效率比较高static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//todo 最大容量,2的30次方。static final int MAXIMUM_CAPACITY = 1 << 30;//todo 默认加载因子,用于扩容使用。static final float DEFAULT_LOAD_FACTOR = 0.75f;//todo table[i]上,同一个链表节点数量大于8时,会开始尝试着 转换为红黑树。static final int TREEIFY_THRESHOLD = 8;//todo 当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。static final int UNTREEIFY_THRESHOLD = 6;//todo 当整个HashMap中元素数量大于64 && 同一个链表节点已达到8时,才会进行链表转为红黑树结构。static final int MIN_TREEIFY_CAPACITY = 64;//todo 存储元素的数组,transient关键字表示该属性不能被序列化transient Node<K, V>[] table;//不被序列化//todo 存储K-V键值对。transient Set<Entry<K, V>> entrySet;//todo table的元素数量(个数)transient int size;//todo 统计该put元素成功的次数 (一般 和 size 相等)transient int modCount;//todo 扩容阀值,当元素数量达到时,会进行扩容。int threshold;// 阀值 = 容量 * 加载因子//todo 也是加载因子,只不过这个是变量。final float loadFactor;......................
HashMap#put方法
public V put(K key, V value) {//put key-valuereturn putVal(hash(key), key, value, false, true);}
hash(key)
/** 对key进行hash **/static final int hash(Object key) {int h;/** todo h ^ (h>>>16)* [(^ 操作, 同位相同为0,不同为1); ]**/// todo 高16位 ^ 低16位 运算return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// todo 高16位 ^ 低16位 运算;「同位值不同 得 1」}
为什么要使用 h ^ (h>>>16) 得到K的最终hash值???
原因有以下几点:1、获得K的待存储 index = (n-1)& hash(key);2、若key.hashCode()的值,只有高位在变化(n比较小时,其实质起作用的是低位),很容易发生 hash碰撞,最坏情况得到一大批的 K 待存储 index 在同一个位置。put逻辑处理更复杂,执行效率大打折扣;浪费数组其他空间;get获取元素时需要更多次遍历才可以获取到V值,查询效率也极低。3、采取 h ^ (h >>> 16) 这种方式获取最终的hash值,再进行 index 计算,元素最大程度的分散,避免多次扩容,也减少了hash碰撞的次数,存取元素效率更高。TODO 具体详细原因,待单独解析(读者也可自行领会)。
key.hashCode()
//key.hashCode() 是根据 K 的类型选择调用那个hashCode方法//此处 K为String时,调用 java.lang.String#hashCode//jdk1.8 String的hash算法// todo n=value.length 算法// todo hash算法 = val[0]*31^(n-1) + val[1]*31^(n-2) + … + val[n-1]public int hashCode() {int h = hash;if (h == 0 && value.length > 0) {char val[] = value;// todo 要put的 String key,转为 char[] val//todo 此处进行hash算法for (int i = 0; i < value.length; i++) {h = 31 * h + val[i];}hash = h;}return h;}
HashMap#putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {//todo tab:用于容器存值;p: 取出 table[index = (hash%n)] 容器这个index中的值,待用Node<K,V>[] tab; Node<K,V> p; int n, i;// todo tab的作用:1、扩容把oldValue,重新赋给tab;2:把tab[i]的值取出,赋给 pif ((tab = table) == null || (n = tab.length) == 0)//todo 判断当前hash表是否是空,如果为空,进行扩容n = (tab = resize()).length;//todo 扩容(扩容之后,把原来的值赋给新容器)后的容器长度if ((p = tab[i = (n - 1) & hash]) == null)//todo 取出当前值 赋给 p = tab[i]tab[i] = newNode(hash, key, value, null);//todo tab[i] = newNode(hash,key,value,null);else {//todo tab[i] != null, 则 当前index下已经有值存在Node<K,V> e; K k;// todo k:取出当前值 p.key; e: 临时Node<K,V>节点if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// todo 把 tab[i]值赋给 临时节点e [改变e的值,就相当于改变了 当前值 p] todo 0、【可能存在key=null,value=null 的情况, --> 交给 2 替换】else if (p instanceof TreeNode)//如果数组中, 这个元素 p 是TreeNode类型e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//判定成功则在红黑树中查找符合的条件的节点并返回此节点else {//todo 若以上条件均判断失败,则执行以下代码 单向链表put新值 p.next = newNode(hash,key,value,null)for (int binCount = 0; ; ++binCount) {//todo 向Node单向链表中添加数据if ((e = p.next) == null) {// todo 取出当前节点p的 下一个节点 值, 为null newNodep.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //todo 循环7次,统一个数组中的链表 数 为 8treeifyBin(tab, hash);//todo 数组元素数为8时,尝试转换为红黑树break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) //todo 1、e的key 和 新值 key 相等 break;交给 2 统一 replace 值break;p = e;//p记录下一个节点 todo e = p.next 取出的p的下一个节点有值,则把这个节点e赋给临时变量p,再一次循环取 p.nex put 新值 value 给 p.next (p的下下一个值)}}if (e != null) { // existing mapping for key todo 这里处理链表添加时,e.key = key(新), 替换掉 value;返回 oldValueV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)// todo onlyIfAbsent = falsee.value = value;//todo 2 把新传过来的 value值,赋给 e.value = valueafterNodeAccess(e);return oldValue;//todo key相同,覆盖value值后,返回原来的 老值 oldValue。}}++modCount;//并发修改计数器 ,有并发修改就抛异常 ConcurrentModificationException todo 记录集合被修改的次数 (put的次数)if (++size > threshold)//todo 新put一次 容器长度(size + 1), 并判断是否需要扩容,【size 容器总长度】resize();afterNodeInsertion(evict);// todo Callbacks to allow LinkedHashMap post-actionsreturn null;}
文章转载自编程是个坑,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




