Java源码阅读之HashMap
来源:互联网 发布:和孩子一起学编程 编辑:程序博客网 时间:2024/05/22 09:43
HashMap是基于哈希表的Map实现,它提供了所有可选的映射操作,并允许空value和空key。(HashMap类似于Hashtable, 除了HashMap是线程不安全的,并允许null)。HashMap不保证映射顺序,而且也不保证顺序在一段时间内保持不变。
HashMap的迭代需要与其实例的容量(Hash桶数)与其size(键值映射数)成比例。因此,如果迭代性能很重要,就不要将初始容量设置得太高(或负载因子太低)。HashMap的一个实例有两个影响其性能的参数:初始容量和负载因子。容量是Hash表中的Hash桶数,初始容量只是创建哈希表时的容量;负载因子是在容量自动增加之前允许哈希表得到满足的度量。当哈希表中的条目数超过负载因子和当前容量的乘积时,就重新排列哈希表(这时HashMap内部数据结构被重新构建,以使散列表具有大约两倍的桶数,即容量增加为原来的2倍。默认负载因子(0.75)提供了时间和空间成本之间的良好折中,初始容量为16,如果初始容量大于最大条目数除以负载因子, 则不会发生rehash操作。
需要注意,HashMap是线程不安全的。如果多个线程同时访问HashMap, 并且至少有一个线程在结构上修改了映射, 那么它必须在外部进行同步。可以使用 Collections.synchronizedMap
方法“包装”Map以获取线程安全的HashMap。
HashMap的迭代器都是fast-fail
的,如果Map在迭代器创建之后的任何时间被结构化地修改, 除了通过迭代器自己的remove方法之外, 都会抛出一个ConcurrentModificationException异常。
总结一下:
1. HashMap容许空Key和空value
2. HashMap默认有两个初始参数:initial capacity和load factor
3. HashMap不是线程安全的
HashMap的数据存储
HashMap的数据存储在Node<K, V> [] table
的数组中。先看看Node的定义。
static class Node<K,V> implements Map.Entry<K,V> { 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; } //......}
存储数组的每一个节点都是一个单链表,链表的每一个节点存储该节点数据的key,value和hashCode。
再看看插入操作:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { 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; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
- 如果table数组为空,调用
resize()
为table分配空间,得到table大小为n
。 - 如果key的
hashCode
和n
计算key的索引i=(n - 1) & hash
。如果table数组中索引为i
的元素为空,则直接new一个Node对象存储到该位置。 - 如果索引为
i
的位置不为null,则从该节点的链表的头部依次迭代,如果发现链表的这个节点p
和要插入数据p
的key满足:p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
,则替换原来的节点p。 - 如果遍历单链表也没有找到满足
p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
的节点,则将插入的值放到该链表的尾部。 - 如果该节点的单链表大于8,则需要将单链表转化为红黑树,即其中的
treeifyBin
操作。 - 修改内部属性
modCount
和size
的值,如果修改后的size
大于threshold
,则需要执行resize()
操作。
对于get操作,代码如下:
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; 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; }
大概思路如下:
- 通过给定的key计算出hashCode,根据
(n - 1) & hash
计算出桶的位置。 - 找到桶,如果桶不为空,判断桶中链表的第一个节点
first
是否满足first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))
,如果满足,返回该节点的value值。 - 遍历这个单链表,如果有节点满足上述条件,返回该节点的value。
- 如果
index
桶中的元素为TreeNode
对象,则这个桶中的KV对数量大于8,该桶为一颗红黑树,调用红黑树的getTreeNode(key, hash)
方法查找key对于的节点,并返回value。 - 其他情况,返回null。
resize过程
final Node<K,V>[] resize()
resize用来初始化或者将table的容量翻倍。如果table为null, 则为其分配初始容量(16)。步骤如下:
- 根据当前的容量(
oldTab.length
)和门限threshold
,计算出resize之后的容量并更新会内部属性,在不超出最大容量(1>>30
)前提下,容量翻倍。 - 遍历table,如果某个位置
j
的桶不为空,并且桶中的链表只有一个节点e
,直接将其放到新的newTab中,newTab[e.hash & (newCap - 1)] = e
- 如果这个桶中的节点
e
是一个TreeNode
的节点,需要将这个红黑树拆两棵树,拆分的依据是:(e.hash & oldCap) == 0
,为0时放到loHead
中,不为0时放到hiHead
中。然后将loHead
放到新表newTab的j
索引的位置。HiHead
放到newTab的的j+oldCao
位置。 - 如果桶中的元素为链表的头结点,按照同样的方式拆分到新的两个新的链表
loHead
和hiHead
位置。然后将loHead
放到新表newTab的j
索引的位置。HiHead
放到newTab的的j+oldCao
位置。 - 遍历所有桶,重复2、3、4步直到结束。
对比
(1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。
- Java源码阅读之HashMap
- Java源码阅读之HashMap
- Java源码阅读之HashMap
- Java集合源码阅读之HashMap
- Java源码阅读-HashMap
- Java源码阅读-HashMap
- JDK源码阅读之 HashMap
- Java8源码阅读之HashMap
- Java数据结构 HashMap 源码阅读
- Java源码之HashMap
- Java源码之HashMap
- Java源码之HashMap
- JDK源码阅读之HashMap的实现
- JDK源码阅读之HashMap类
- Java Jdk1.8 HashMap源码阅读
- java集合源码阅读笔记-HashMap
- Java基础之源码阅读(一):jdk1.8的HashMap
- hashmap 源码阅读
- 简单的下载excel模板
- spring mvc高级篇(八):Spring+SpringMVC+Mybatis整合(采用泛型和注解优化)
- Codeforces Round #384 (Div. 2) B. Chloe and the sequence
- JVM笔记(二)——HotSpot虚拟机对象
- http一个完整的请求?
- Java源码阅读之HashMap
- 如何处理二进制文件
- android studio gradle/gradle plugin 配置详解,解决"Gradle sync failed: Unknown host 'services.gradle.org'."
- 杭电acm2017 字符数量统计
- Codeforces #428 (Div. 2) C. Journey (DFS
- Spring-AOP基础知识
- java版poi+excel导入实例1
- 尾递归
- DDNS(花生壳)