HashMap源码分析
来源:互联网 发布:淘宝英文版网址 编辑:程序博客网 时间:2024/06/08 17:04
1.hashMap的工作原理
当我们执行put存值时,hashmap会先调用key的hashcode方法的到哈希码,也就是桶的索引bucketIndex,根据哈希码找到该桶,然后遍历桶用equal方法来比较key,如果桶为空,就把kv放入桶中,如果桶存在该key,就用新value代替旧value,返回旧value,如果不存在就放入桶中,每个桶用单链表维护。下面是原理图:
2.成员变量
要理解成员变量,必须先了解以下知识:
一个HashMap实例有两个参数会影响性能:初始容量(initial capacity)和装载因子(load factor),capacity是hashtable的桶数目,初始容量仅仅只是创建时的容量 。
装载因子是衡量哈希表自动增长前装满的程度,当哈希表中键值对的数目超出容量与装载因子之积,哈希表就会重新哈希(rehashed)(即,内部数据结构会重建)为之前容量的两倍 。
默认的装载因子0.75可以在时间和空间之间有很好的权衡,想避免重新hash的时间开销,可以设置比较大的初始容量。
HashMap通常作为桶式哈希表,当桶变得很大的时候就转化为树结点,和TreeMap中比较类似。
//初始容量,默认16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量,默认2的30次方 static final int MAXIMUM_CAPACITY = 1 << 30; //装载因子,默认0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; //树形和列表的阀值,大于该值,桶的结构由列表改成链式 static final int TREEIFY_THRESHOLD = 8; //树形和列表的阀值,小于该值,桶的结构由链式改成列表 static final int UNTREEIFY_THRESHOLD = 6; //转换树形后最小容量,至少是TREEIFY_THRESHOLD的四倍 static final int MIN_TREEIFY_CAPACITY = 64; //储存键值对的节点 transient Node<K,V>[] table; //用作缓存 transient Set<Map.Entry<K,V>> entrySet; //node的数目 transient int size; //HashMap的结构更改次数 transient int modCount; //下次分配空间时,table的大小 int threshold; //装载因子,定义成final,让它变成不可变成员,只能在构造器初始化。 final float loadFactor;
3.构造函数
构造函数用来初始化影响hashmap性能最大的两个参数initialCapacity,loadFactor。
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); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
代码很容易看懂,我们来看一下tableSizeFor(initialCapacity)
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; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
从这段代码中,方法完成了什么呢? n |= 自身右移多次,完成了将n最高位上的1开始的所有位都置1的操作,例如000101011110就变成了000111111111,也就完成了将利用cap找到2^n-1的数,并将其作为threshold,threhold是下次table执行reset后的大小。这段代码非常巧妙,最后n>>>16的原因是int占32位,16刚好可以满足所有int的需求,不得不说很漂亮的代码。
要避免rehash的时间开销,可以设置比较大的初始容量。否则,如果保持默认的16的话,又有1w对hash,意味着rehash要进行4~5次。
4.重要的成员方法
get方法
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //hash表不为空且size大于0,hash所在的桶不为空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //先判断桶的首个Key,相同则返回 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //如果首个Key不是,则根据链表遍历或者树遍历找到 if ((e = first.next) != null) { if (first instanceof TreeNode) 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) { return putVal(hash(key), key, value, false, true); }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //如果table数组尚未创建(第一次调用put),则新建table数组 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //table[i]中没有结点则创建新节点 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //如果table[i]的首个元素的关键字与给定关键字key相同,则替换旧值 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //table[i]是红黑树情况 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //table[i]是链表情况,遍历链表 for (int binCount = 0; ; ++binCount) { //找到最后也没找到,就新建一个节点,放在链表后面 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //判断链表长度是否超过阀值,超过则转换成红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //如果找到了,e就不为空,进行下一步 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //e不为空就是找到了,返回旧值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } //修改map结构的操作数加1 ++modCount; //如果超出重构阈值,需要重新分配空间 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
resize方法
resize方法的实现有点复杂,有余力的同学可以认真研究一下:
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 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; // 阈值加倍 } else if (oldThr > 0) // 如果oldCap<=0,初始容量为阈值threshold newCap = oldThr; else { // 零初始化阈值表明使用默认值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } 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<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) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((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) { 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); //新表索引:hash & (newCap - 1)---》低x位为Index //旧表索引:hash & (oldCap - 1)---》低x-1位为Index //newCap = oldCap << 1 //举例说明:resize()之前为低x-1位为Index,resize()之后为低x位为Index //则所有Entry中,hash值第x位为0的,不需要哈希到新位置,只需要呆在当前索引下的新位置j //hash值第x位为1的,需要哈希到新位置,新位置为j+oldCap if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
- 源码分析:HashMap
- 源码分析:HashMap
- HashMap源码分析
- HashMap 源码分析
- HashMap源码分析
- HashMap LinkedHashMap源码分析
- HashMap源码分析
- HashMap 源码分析
- HashMap源码分析
- HashMap源码分析
- HashMap源码分析
- Java HashMap 源码分析
- HashMap源码分析
- java HashMap源码分析
- 源码分析HashMap
- HashMap源码分析
- HashMap源码分析
- HashMap源码分析
- change color 001
- 【bzoj 1588】营业额统计(链表)
- Kaggle sea-lion总结
- PHP基础知识点
- IO流
- HashMap源码分析
- 【java基础】20.网络编程2
- Elasticsearch顶尖高手系列-高手进阶篇
- Google 开源的项目集合
- postgre 动态行转列
- 动态代理在反射中的运用
- 如何用命令测试Linux 硬盘的读写速度
- SOCKET通信基础
- android踩坑记录-持续更新