JDK8 源码之HashMap(1)
2017-02-24 14:04
246 查看
HashMap是什么
源码出发:public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {...}
可以看到HashMap实现了Map、Cloneable、Serializable接口,实现了AbstractMap抽象类。
Map(附录有详细介绍)为一个将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。
HashMap在jdk1.8中,实现上有了一个很大的优化,实现方式有原来的数组加链表,变成了数组、链表和红黑树。在性能上有了一个较大的提升,也一定程度上解决了hash值碰撞带来的性能损失。
HashMap-Fields
再继续看看Fields://默认初始容量16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //当table容量大于该值,threshold设置成为固定Integer.MAX_VALUE。 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; //Node节点数组(hash表) transient Node<K,V>[] table; //Map.Entry Set对象,用于遍历 transient Set<Map.Entry<K,V>> entrySet; //存储元素数目 transient int size; //结构变更次数 transient int modCount; //下次调整大小的临界值(table大小*负载因子) int threshold; //哈希表的负载因子。 final float loadFactor;
可以看到,hashMap存在一个负载因子,当hashMap容量超过threshold(负载因子*数组长度)会发生扩容。链表与红黑树的相互转化各存在一个阈值,同时也使用modCount来记录‘结构变更’用以保证fail-fast。
HashMap-Node
Node节点还是一览无余:static class Node<K,V> implements Map.Entry<K,V> { //hash值 final int hash; final K key; V value; //指向下个结点 Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } //获取节点hash值 public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } //设置节点值 public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } //判断节点是否相同:节点值的key且value调用equals为true public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
node节点为链表节点,除了记录当前的key、value值及next节点时,也将key的hash值记录来,杜绝重复计算带来的性能损失。
HashMap-TreeNode
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; .... }
当链表的长度超过默认阈值时,该链表会转化成红黑树,防止链表过长,造成查找性能缓慢,也是jdk8中一个比较重要的优化。关于TreeNode的实现和操作会在后面单独开一篇博客来说明,有兴趣的可以看看
HashMap构造方法
//默认传参数 public HashMap() { //负载因子默认0.75 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } //指定初始容量 public HashMap(int initialCapacity) { //负载因子默认为0.75 this(initialCapacity, DEFAULT_LOAD_FACTOR); } //指定初始容量和负载因子 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) f010 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //初始容量最多为MAXIMUM_CAPACITY if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); //计算得到下载扩容容量的临界值(返回值都为2幂次方-1) //可以看下面的解释,这里threshold的值会实际是table的初始容量 this.threshold = tableSizeFor(initialCapacity); } //设置初始值 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; //该方法后面再做介绍 putMapEntries(m, false); }
可以看到的,hashMap初始化未添加元素的时,并未实例化table,只是记录来容量和负载因子。而是在第一次添加的操作的时候,才会实例化table。
而且可以看出设置initialCapacity时,并没有按照你给的initialCapacity取初始化table容量,而是将tableSizeFor(initialCapacity)的返回值newCap赋值给threshold,结合后面的resize方法,初始化时会走‘分支二’,table的容量会被初始化成newCap大小。
HashMap-resize扩容
接下来,看看hashmap怎么扩容的:final Node<K,V>[] resize() { //获取当前table对象 Node<K,V>[] oldTab = table; //获取当前容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; //获取扩容临界值 int oldThr = threshold; //声明新容量、新负载因子 int newCap, newThr = 0; if (oldCap > 0) {//“分之一” //当前容量大于0时 if (oldCap >= MAXIMUM_CAPACITY) { //当前容量大于于MAXIMUM_CAPACITY //直接设置threshold的值为Integer.MAX_VALUE threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //新容量newCap=当前容量*2 //新临界点threshold=当前临界点threshold*2 newThr = oldThr << 1; } else if (oldThr > 0) //“分之二” //当前容量等于0,但threshold已经指定,则容量=threshold //例如:HashMap(initCap,0.75)初始化时,oldCap=0,threshold>0 newCap = oldThr; else { //“分之三” //当前容量等于0时(例如第一次添加元素时) newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //走“分之二”,newThr==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<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 { // preserve order 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 { //记录在新table中位置需要变动的 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); //这一段的算法应该来说比较巧妙哦, //已知:新扩容=当前容量*2 //假设当前容量为16(二进制:10000), //假设元素hash值后5位为(a:10010,b:00010) //a和b元素位置一致(cap-1)&hash为2位(00010) //新容量:32(100000) //新位置 a:第18位(10010),b第2位(00010) //可以看出,a和b元素的位置正好相差oldCap if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
resize方法注释已经很详细了,主要有一个点妙传就是扩容时,每个链表中原有元素的位置要么不动,要么移动oldCap个位置。而且插入顺序不变,jdk1.7中则会反序。
其他操作
限于篇幅,可以看后面的博客附录 Map接口
Map 方法说明
public interface Map<K,V> { //map大小 int size(); //是否为空 boolean isEmpty(); //map是否含有当前key boolean containsKey(Object key); //map是否含有当前value boolean containsValue(Object value); V get(Object key); V put(K key, V value); V remove(Object key); //批量设置 void putAll(Map<? extends K, ? extends V> m); void clear(); //获取key Set列表 Set<K> keySet(); //获取value Set列表 Collection<V> values(); //获取遍历对象列表 Set<Map.Entry<K, V>> entrySet(); //遍历节点接口定义 interface Entry<K,V> {....} boolean equals(Object o); int hashCode(); }
可以看到Map接口定义了一个map必须实现的操作,增加、插入、修改、遍历等基本方法,同时定义了内部遍历节点Entry接口。不过在jdk8之后,可以看到Map接口中多了很多default方法,虽然都不复杂,但是可以借鉴一下这种思想,还是很有好处的。
Map default方法
大概浏览下,默认方法如:getOrDefault、forEach等方法实现都比较简单,就不描述细节了,只是大概说一下是什么功能了。//通过键值key获取值为null时,则返回defaultValue default V getOrDefault(Object key, V defaultValue){...} //遍历键值对,执行指定动作 default void forEach(BiConsumer<? super K, ? super V> action){...} //遍历键值对,对每个键值对执行指定动作后的返回值覆盖当前值 default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function){...} //当前键值key获取值为null时,则设置值为value default V putIfAbsent(K key, V value) {} //删除键值为key且值为value的键值对 //map已经存在键值key default boolean remove(Object key, Object value){} //覆盖键值对(键值为key且值为oldValue)的值为newValue,否则不覆盖 //map已经存在键值key default boolean replace(K key, V oldValue, V newValue){} //设置map中键值为key的的值为value //map已经存在键值key default V replace(K key, V value){} //当get(key)为空时,则mappingFunction.apply(key)计算得到的新值newValue不为空时,则put(key,newValue)并返回newValue,否则返回null default V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction) {} //与computeIfAbsent基本相同,新增了一个当get(key)为空时,remove(key) default V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {} //mappingFunction.apply(key)计算得到的新值newValue //不为空时,put(key,newValue) 返回newValue //为空时,remove(key) 返回null default V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {} //1.mappingFunction.apply(key)计算得到的新值newValue //2.get(key)为null时,put(key,value),否则 put(key,newValue) //3.remove(key) default V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remappingFunction){}
可以看到接口中的默认方法还是很丰富,而且都比较简单,但阅读过程中,这些接口的定义,方法的抽象,对于一个程序猿来说还是能学到很多东西。
Map-Entry default方法
interface Entry<K,V> { public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() { return (Comparator<Map.Entry<K, V>> & Serializable) (c1, c2) -> c1.getKey().compareTo(c2.getKey()); } public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() { return (Comparator<Map.Entry<K, V>> & Serializable) (c1, c2) -> c1.getValue().compareTo(c2.getValue()); } public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) { Objects.requireNonNull(cmp); return (Comparator<Map.Entry<K, V>> & Serializable) (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey()); } public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) { Objects.requireNonNull(cmp); return (Comparator<Map.Entry<K, V>> & Serializable) (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue()); } }
相关文章推荐
- jdk 1.7 hashMap源码解读
- jdk集合源码之HashMap
- 【源码解析】JDK源码之HashMap
- JDK8源码学习——HashMap
- 深入JDK源码系列:HashMap详解
- Java Jdk1.8 HashMap源码阅读笔记一
- 【JAVA秒会技术之ConcurrentHashMap】JDK1.7与JDK1.8源码区别
- HashMap源码分析_JDK1.8版本
- 【集合框架】JDK1.8源码分析之HashMap & LinkedHashMap迭代器(三)
- jdk_Collection_HashMap源码
- JDK源码解析之HashMap类
- Java类集框架之HashMap(JDK1.8)源码剖析
- JDK源码阅读:实现自己的HashMap
- JDK1.8 HashMap源码分析
- HashMap源码分析(基于JDK1.6)
- HashMap源码分析(JDK1.7)
- jdk源码分析之LinkedHashMap
- 【JDK源码阅读8-util】Map接口----HashMap
- HashMap源码解析(基于JDK1.7)
- java基础提高篇--集合源码分析--jdk1.8 HashMap源码