java集合HashMap源码解析(基于JDK1。8)
一、Hashmap简介
类继承关系图如下:
HashMap实现了三个接口,一个抽象类。主要的方法都在Map接口中,AbstractMap抽象类实现了Map方法中的公共方法,例如:size(),containsKey(),clear()等,主要方法由子类自己实现。
HashMap结构如下图:
HashMap的主要结构由数组、链表/红黑树组成,当数组中某个节点大于等于8个并且数组长度大于等于64时,链表会转换为红黑树。 二、HashMap的主要属性public class HashMap extends AbstractMap implements Map, Cloneable, Serializable { private static final long serialVersionUID = 362498820763181265L; /* ---------------- 默认值 -------------- */ /** * 默认初始化大小,必须是二的次幂 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * 数组最大值,当大于该值时使用该值 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 默认负载因子 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 某个节点由链表转为红黑树时候的阈值 */ static final int TREEIFY_THRESHOLD = 8; /** * 节点由红黑树转为链表时的阈值 */ static final int UNTREEIFY_THRESHOLD = 6; /** * 节点转为红黑树时数组的阈值 */ static final int MIN_TREEIFY_CAPACITY = 64; /* ---------------- 字段 -------------- */ /** * HashMap的数组(划重点,主要结构) * HashMap的由数组+链表/红黑树组成,数组指的就是这个数组,链表和红黑树则是由Node结构组成 */ transient Node[] table; /** * 保存缓存的set.AbstractMap字段用于实现keySet()和values()。 */ transient Set> entrySet; /** * HashMap中数据量大小. */ transient int size; /** * HashMap的修改次数 */ transient int modCount; /** * 阈值,当HashMap中数据大于该值时将进行扩容 */ int threshold; /** * 加载因子 */ final float loadFactor; /* ---------------- Node结构 -------------- */ /** * HashMap的基础节点 */ static class Node implements Map.Entry { //当前节点的hash值 final int hash; //当前节点的key final K key; //当前节点的value V value; //指向下一个节点的引用 Node next; //node的构造函数 Node(int hash, K key, V value, Node next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } //...其他方法省略 } 复制代码三、源码解析1、初始化方法
HashMap共有四个构造函数,参数分别是1、初始化数组大小、加载因子。2、初始化数组大小。3、无参。4、HashMap结构。
其中1、2、3三个参数的构造函数性质相同,都是传入初始化数组大小或加载因子,没传的使用默认值。构造函数4使用默认的初始化大小和加载因子,并且是将传入的HashMap添加到新的结构中。
具体代码如下:/** * 有初始化大小和加载因子大小的构造函数 * @param initialCapacity 初始化大小 * @param loadFactor 加载因子 * @throws IllegalArgumentException 非法参数异常 */ public HashMap(int initialCapacity, float loadFactor) { //如果初始化大小小于0,抛出异常 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //如果初始化大小大于最大值,那么就把初始化大小设置为最大值 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //如果加载因子小于等于0或者是非法的float类型,则抛出异常 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); //设置加载因子 this.loadFactor = loadFactor; //设置下一次扩容阈值 this.threshold = tableSizeFor(initialCapacity); } /** * 有初始化大小的构造函数 加载因子使用默认值(0.75) * * @param 初始化大小 * @throws IllegalArgumentException 非法参数异常 */ public HashMap(int initialCapacity) { //调用第一个构造函数,加载因子使用默认值DEFAULT_LOAD_FACTOR(0.75) this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * 无参构造函数,使用默认大小(16)和默认加载因子(0.75) */ public HashMap() { //设计加载因子为默认值,其他所有值都是要默认值 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } /** * 使用一个HashMap为参数的构造函数,加载因子使用默认(0.75) * * @param 一个HashMap数据 * @throws NullPointerException 空指针异常 */ public HashMap(Map<? extends K, ? extends V> m) { //加载因子为默认值 this.loadFactor = DEFAULT_LOAD_FACTOR; //将传入的HashMap数据放入当前结构中 putMapEntries(m, false); } 复制代码2、get方法
先看源码,再做总结,源码:/** * HashMap的get方法 * @param 要查找的key */ public V get(Object key) { //定义一个node Node e; //通过getNode()方法获取node,getNode返回null则get方法返回null,否则返回node的value return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * 实现Map.get和相关方法 * @param key的hash值 * @param key * @return 结构中的node或者null */ final Node getNode(int hash, Object key) { //定义说明 tab:数组,first:该数组节点中的第一个值,n:数组大小,k:first的key Node[] tab; Node first, e; int n; K k; //如果数组不为null、数组大小大于0、通过hash获取到的数组中的节点不为null if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //该节点的hash等于要查找的hash值(始终检查第一个节点)、该节点的key与要查找的key相等(==为true或者equals为true) if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //如果第first节点不为null并且first节点不是要查找的节点(上面的if判断,如果是要查找的接口则上一步就返回了) if ((e = first.next) != null) { //如果是红黑树类型 if (first instanceof TreeNode) //遍历红黑树 return ((TreeNode)first).getTreeNode(hash, key); //循环遍历链表 do { //当hash值相同、该节点的key与要查找的key相等(==为true或者equals为true) if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } //如果hash值对应的数组为null,则返回null return null; } 复制代码
get方法总结:根据key的hash值找到数组中对应的位置。判断该位置上的值和要查找的值是否相等(==或者equals),如果是则返回如果不是则判断该节点的下一个节点是否为空,为空则返回null。判断结构是否是红黑树,如果是,遍历树。如果不是树,则遍历链表。如果不符合上面的条件则返回null。3、put方法
先看源码:/** * 添加key-value,如果key已经对应value,则替换,返回之前的值 * * @param key * @param value * @return 返回之前的value */ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * 实现Map.put和相关方法 * * @param hash值 * @param key * @param value * @param onlyIfAbsent 是否只在不存在的时候修改值,true不修改,false修改 * @param evict 如果为false,则为创建模式 * @return 返回之前的value,如果没有则为null */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //变量说明tab:当前数组,p:当前节点,n:数组大小,i:要插入的数据在数组中的位置 Node[] tab; Node p; int n, i; //数组为空或者数组大小为0 初始化数组(resize()扩容函数,也包括初始化数组,后面扩容会分析) if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //如果对应数组中的位置为null,将当前数据构造成Node放入该节点 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node e; K k; //当hash值相同、该节点的key与要插入的key相等(==为true或者equals为true),则替换该value if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //如果是树结构,插入树节点 else if (p instanceof TreeNode) e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); else { //遍历链表节点 for (int binCount = 0; ; ++binCount) { //如果没有遍历到与该key相同的数据,则在链表最后添加该数据节点 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //如果该链表长度大于等于8,则将链表转换为树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //如果key相同,则跳出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //如果存在key对应的数据,替换数据,返回之前的数据 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; //LinkedHashMap使用,HashMap中方法体为空 afterNodeAccess(e); return oldValue; } } ++modCount; //如果数组中的值大于阈值,则扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } 复制代码
put方法总结:判断HashMap中的数组是否为空或者大小为0,如果是则初始化数组。如果该hash值对应的数组中的位置为空,则将该数据组成的节点直接插入到该位置中。如果数组对应位置数据不为空,判断该位置节点的key与要插入的key是否相等,如果是设置e(局部变量)等于该节点。如果Node结构为树结构,则遍历树结构找到key对应的节点,设置为e。如果Node结构为链表,遍历链表,如果链表中没有找到对应的key,将数据构造成Node节点插入链表最后,如果链表长度大于等于8,则将结构转为树。如果在链表中找到对应的key,则将该节点设置为e.如果e(上面遍历找到的节点)不为null,则设置新的value,返回旧的value.如果e为null,说明是新增,HashMap大小加一,判断是否大于阈值,如果大于,则扩容。4、扩容
先看源码:/** * 初始化或者扩容 * * @return the table */ final Node[] resize() { //旧的数组 Node[] oldTab = table; //旧的数组大小 int oldCap = (oldTab == null) ? 0 : oldTab.length; //旧的阈值 int oldThr = threshold; //新的数组大小和阈值 int newCap, newThr = 0; //如果旧的数组大小大于0(只要初始化过就都会大于0) if (oldCap > 0) { //旧的数组大小已经达到最大,那么设置阈值为最大值 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //如果旧的数组扩大两倍小鱼最大值并且旧的数组大于等于初始化值,那么设置新的阈值为旧的阈值的两倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } //如果数组大小为0,阈值大小大于0,则设置新的初始化大小为阈值,否则全部使用默认值 else if (oldThr > 0) // 初始容量设置为阈值 newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //如果新的阈值等于0,那么设置新的阈值为新的数组大小*加载因子 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //阈值等于新的阈值 threshold = newThr; //新的数组 @SuppressWarnings({"rawtypes","unchecked"}) Node[] newTab = (Node[])new Node[newCap]; table = newTab; //旧的数组不为null,说明不是初始化,需要扩容 if (oldTab != null) { //遍历旧得数组 for (int j = 0; j < oldCap; ++j) { Node e; //如果该位置的节点不为null,那么遍历链表或者树放入新的数组中 if ((e = oldTab[j]) != null) { oldTab[j] = null; //如果只有一个节点,直接放入新数组中对应的位置 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //如果是树结构,拆分树 else if (e instanceof TreeNode) ((TreeNode)e).split(this, newTab, j, oldCap); //链表结构,拆分链表 else { // 保持之前的顺序 //低位头、尾节点 Node loHead = null, loTail = null; //高位头、尾节点 Node hiHead = null, hiTail = null; Node next; do { next = e.next; //如果hash值&旧的数组大小为0,说明放到新数组后还是之前的位置,否则为(当前位置+旧数组大小)的位置 //遍历第一个节点时,头、尾都设置为该节点,之后的节点添加到该节点之后,并设置尾节点为后添加的节点 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; } 复制代码
扩容方法总结:数组是否已经达到最大值,如果已经最大,设置阈值也为最大值,否则数组大小和阈值都改为之前的两倍。数组是否已经初始化,如果没有则初始化数组和阈值。旧数组不为null,遍历旧数组,将对应位置的链表/树分为成两个链表/数组,一个在原先的位置上,一个在原先的位置+原先数组大小的位置上,将两个链表/树放入新数组的对应位置。
兰州财经大学女生笔下的兰州,感动了无数人相册是不能再看下去了,泪水已经模糊了视线。我终究是离开了,这个陪了我五年的城市,毫无征兆地走。没有告别,没有相送,没有眼泪,只是回过神来的时候,忽然发现这个城市像极了很久以前的恋人
谨言慎行,谨言非常重要谨于言才能慎于行。谨言的人总是与理智结盟,却不会与冲动为友。谨言慎行的处世方式,能使你避免不必要的误会,让你的人际交往更加成功。语言是人际沟通的一把双刃剑,用得好,纵使三九寒天,也
今日美文树叶上的童话问一棵老树站在小河边,注视着水面的光波,谛听着它喁喁的细语,禁不住弯下腰来,掬一口清冽的甘泉,一饮而尽。他目送着小河无声地流去,无法名状的依恋之情,便从心底涌起。他问小河请你告诉我
山东独生子女费标准调整,每人每月不低于100元?属于什么水平?独生子女费,又称独生子女奖励金独生子女父母一次性养老补助等等。2016年1月1日,我国实施二孩政策以后,标志着持续30多年的独生子女时代结束了。不过,国家明确对于独生子女政策实施期
秋日私语(原创)作者熬君则秋日生活打卡季停车坐爱枫林晚,霜叶红于二月花,秋天的韵味,是一份儿恬静,陶醉于被秋风染红了的枫叶,任思绪飞扬,在秋日里沉淀。看,树上还有几片叶,是叶对树的留恋,还是树对叶的垂怜?我
我在岛屿读书即将上线,余华苏童西川等人相聚海岛,分享好书当好看的风景和有趣的灵魂相遇,会碰撞出怎样的火花?11月10日,今日头条与江苏卫视联合出品的外景纪实类读书节目我在岛屿读书即将上线。节目邀请余华苏童等十余位写书人出书人爱书人,前往
科普丨冬季森林防火知识请牢记央广网兰州11月9日消息(记者邸文炯)森林火灾是一种突发性强破坏性大处置救助较为困难的自然灾害,是中国森林面临的最主要的风险源。严重的森林火灾会对居民的日常生活造成影响,给国家和人
20句温暖整个冬天的暖心句子1迷茫的本质就是在本该拼搏的时候,想得太多,做得太少,所以少想,多做。2太阳升起带来新的希望,告别昨日的紧张,新的一天,你要大踏步迈向前方。3很多时候,心里明明不是那样想的,却控制
早安语录愿你朝着阳光,做一个温暖的人,不卑不亢,清澈生活早安语录Nov8(1)生活有苦有甜,有进有退愿我们都拥有积极阳光的正能量照亮自己,也温暖别人新的一天,早上好!(2)人生,顺其自然就好拥有时,珍惜失去时,珍重不用刻意掩饰自己不用势
你是一生的真爱,是我心中最温暖的陪伴遇见,擦肩,人世间总有太多缘不过昙花一现,有些人来去匆匆,不留痕迹,有些人惊鸿一场,铭记一生。我铭记着你的一切,虽然你只是我生命中那一场擦肩而过的缘,但是那花开一瞬的美,却惊艳了我
牵挂无声,却最温暖牵挂,是世间最真切可辨,却又最若有若无的情。人和人之间的距离或远或近,大多时候就在于心与心之间的牵挂是深或是浅。牵挂最浓之时,是无声的。因为很多时候,言语并不能完全地表达一个人的情