java1.8 常用集合源码学习:HashMap

来源:互联网 发布:子宫纵隔 知乎 编辑:程序博客网 时间:2024/05/17 04:42
1、先看api

基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。

HashMap 的实例有两个参数影响其性能:初始容量 和加载因子容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。

通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。

如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。

2、源码学习

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; //The bin count threshold for using a tree rather than list for a bin.
static final int UNTREEIFY_THRESHOLD = 6;

内部类 Node<K,V>是基本的存储数据的节点
包含4个字段,hash值,key、value,以及指向下一个Node的引用next

真正保存数据的table
transient Node<K,V>[] table;

transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount; //改变结构的次数,比如rehash
int threshold; //下一次需要进行resize的size的阈值(容量*加载因子)(capacity * load factor) ?
final float loadFactor; //加载因子


构造函数分析:
public HashMap(Map<? extends K, ? extends V> m) {
调用了putMapEntries

方法分析
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
如果table没有初始化
利用传入集合m的大小算出threshold的大小
如果table已经初始化,而且集合m的大小大于threshold,则进行resize操作
最后遍历集合m,调用putVal方法将他们加入本map中

方法分析:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
1、首先判断table是否未初始化,如果未初始化,则调用resize方法初始化table
2、然后判断table表中这个hash所在位置是否有值,如果没有,直接创建一个Node放入这个位置
判断一个hash值在这个table中位置的方法为:(当前table大小-1)按位与 待插入的这个hash值,如果不好理解,可以举例子测试一下
3、如果这个table表中这个hash所在位置有Node:
A、判段这个Node的hash、key是否和传入的相等,如果相等,则直接修改value值并返回oldValue
B、如果这个Node的key值和传入的key值不同,而这个Node是TreeNode的实例的话,调用putTreeVal方法将传入的hash、key、value值存入这个TreeNode中。这时候有两种可能,如果这个TreeNode中已经有这个hash和key的话,则会返回这个已存在的子TreeNode,然后像上一个判断一样,直接修改这个value值并返回oldValue;如果这个TreeNode中没有这个hash和key,则会创建一个TreeNode,并且返回一个null。后面会判断如果不是修改的现有的Node,则会更新modCount的值,并且增加table的size,而如果size的大小超过了threshold的话,会进行resize操作。
C、如果这个Node的key值和传入的key值不同,而这个Node又不是TreeNode的实例的话:
a、首先取得这个Node的next元素,即这个链表的下一个元素,如果这个元素是null即没有下一个元素,则使用传入的hash、key、value创建一个新的Node并放在旧的Node的尾部,然后判断这个链表的长度是否超过了TREEIFY_THRESHOLD的阈值,如果超过了则调用treeifyBin方法将这个链表转换成为TreeNode,然后会做更新modCount、判断是否需要resize等操作。
b、如果这个Node元素的next元素不是null,即存在,则判断这个next元素是否和传入的hash、key相同,如果相同,则判断这个next元素就是我们要处理的Node,将其值修改,并退出方法,返回oldValue
c、如果这个Node元素的next元素不是null,即存在,但是它的key和传入的key不同,则继续对这个next元素做a、b的操作,直到满足退出条件。
这个方法里需要注意的是,如果已经存在该hash和key的Node,则会修改它并返回旧值,如果不存在这个Node则会创建,并修改modCount、按需resize,并且返回null。

方法分析
final Node<K,V> getNode(int hash, Object key) {
get等方法实际会调用这个方法,实现如下:
如果table没有初始化或者大小为0,返回null
如果table中这个hash的第一个Node为null,返回null
如果第一个Node不为null:
如果这个Node的key和传入key相同,则返回这个Node
如果这个Node的key和传入key不相同,并且没有next元素,则返回null
如果这个Node的key和传入key不相同,并且有next元素,则判断这个next元素是TreeNode元素还是普通Node链表
如果是TreeNode元素,调用getTreeNode方法查找相关数据,如果找到则返回找到的元素,如果找不到则返回null
如果是Node元素,则直接遍历这个Node链表,找到则返回该元素,找不到返回null

方法分析
final Node<K,V>[] resize() {
这个方法初始化table或者将它的容量加倍,实现如下:
1、首先判断旧table的容量是否大于0(即是否初始化过),如果大于0,则
A、如果旧table的容量已经大于MAXIMUM_CAPACITY,则将threshold设置为Integer.MAX_VALUE并返回table,不再进行操作
B、如果旧table的容量乘以2(代码中使用了<<1)没有达到MAXIMUM_CAPACITY并且旧的容量大于默认的初始化容量DEFAULT_INITIAL_CAPACITY的话,那么设置新的容量为旧容量的两倍,并设置新的threshold为旧的的两倍
2、如果旧table没有初始化过,则判断旧的threshold是否初始化过
A、如果旧的threshold初始化过,则设置新的容量等于旧的threshold
B、如果旧的threshold没有初始化过,则设置新的容量和新的threshold都为默认值(threshold的默认值为DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)
3、如果新的threshold没有设置过,则初始化(新容量*加载因子),如果这个值大于最大容量限制的话,这新的threshold设置为Integer.MAX_VALUE
4、以新的容量创建一个新的table(即Node数组)
5、如果旧的table不为null,则把旧table中的数据移动到新的table中,逻辑如下,遍历oldTable中的节点,针对每一个节点Node:
A、如果这个节点没有next元素,则直接重新计算这个Node在新的table中的位置,并设置它在新的table中的位置(注意,这个Node在新的table中的索引位置只有两种可能:保持不变或者移动2的幂数个位,这个特性和他的计算方式有关系,可以举例测试)。
B、如果这个节点有next元素,并且这个元素本身是TreeNode元素,则调用它的slipt方法处理,这个在后面看TreeNode时再看
C、如果这个节点有next元素,并且这个元素本身个是链表,则遍历这个链表,把它一分为二:如果节点hash计算出的index在旧table的总index容量内,则分到低位链表中,如果计算出的index不在旧table的总容量中,则分配到高位链表中。判断节点hash的index是否在旧table的总index容量中的方法是:(e.hash * oldCap)==0)他和计算hash的index方法很类似,只少了一个“-1”。分为两个链表以后,将低位链表存放到索引和旧table相同的位置,高位链表存放到索引为“旧index+旧table容量”的位置,注意,在将链表分别存储前,要把每个链表的尾部的next元素置为null,防止它还有对旧链表元素的索引。

方法分析
final void treeifyBin(Node<K,V>[] tab, int hash) {
首先判断table大小,如果table大小太小(小于MIN_TREEIFY_CAPACITY,即小于64),则直接进行resize操作
如果table大小足够大,则判断这个hash的位置是否有Node,如果有,则遍历这个Node链表,将其中每一个Node转化成TreeNode,并且重新按原顺序组成双向链表。然后把table中的Node链表替换成这个新的TreeNode链表,并且调用TreeNode链表头的treeify方法将其调整成树,这个方法后面分析

方法分析
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
这个方法和getNode方法类似
首先判断这个hash是否有Node,如果没有(或table没有初始化等)直接返回null
然后确定delNode节点
A、判断这个Node的hash和key是否和传入的hash、key相同,如果相同,则这个Node就是delNode
B、如果不同,而这个Node是一个TreeNode,则调用这个TreeNode的getTreeNode方法来取得delNode
C、如果不同,而这个Node是个链表,则遍历这个链表,寻找key和传入key相同的Node,并把这个Node确定为delNode节点
如果前面没有找到delNode,返回null
如果找到delNode,并且满足!matchValue(如果matchValue为true,则只有待删除值和delNode的值相同时才执行删除操作)条件或者delNode的value和传入value相同,才真正执行执行操作,也分为三种情况:
A、如果delNode是TreeNode,则调用他的removeTreeNode方法删除
B、如果这个delNode就是table在这个hash的index上的Node(或链表的头,或者单一的Node都可以)的话,则设置这个Node的next元素为delNode的next元素(delNode的next元素有可能为null)
C、如果这个delNode是table在这个hash的index上的Node链表中的一个元素,则从这个链表中删除delNode元素(将delNode的前一个元素的next指向delNode的next元素)

方法分析
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
1、如果mappingFunction是null,抛异常
2、按需resize
3、取得key的hash的索引的位置的Node,然后寻找这个key所对应的oldNode
A、如果是TreeNode,调用getTreeNode方法找oldNode
B、如果是Node链表,则遍历链表找oldNode
4、如果找到了oldNode,并且oldNode的value不为null,则退出方法,并返回这个oldNode的value
5、通过给定的mappingFunction方法计算value,如果value为null,退出方法返回null
6、如果找到了oldNode,则置oldNode的value为新计算出来的value
7、如果没有找到oldNode,并且key的hash的索引的位置的Node是TreeNode,则调用putTreeVal方法将新的value存入
8、如果没有找到oldNode,并且key的hash的索引的位置的Node是链表,则把这个新计算出的value代表的Node放在这个链表的头部,然后按需调用treeifyBin
9、修改modCount、size等值


类分析:
abstract class HashIterator {
有指向下一个Node的next,有指向当前Node的current
nextNode的核心是:
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
就是取了table的下一个不为null的元素

类分析
HashMapSpliterator,没用到过,暂时不看//TODO

方法分析:
public boolean containsValue(Object value) {
这个方法没啥可说的,就是遍历table,所以比较耗时


类TreeNode
treenode是java1.8新增的
红黑树的特点:
1、节点是红色或黑色。
2、根是黑色。
3、所有叶子都是黑色(叶子是NIL节点)。
4、每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
5、从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

方法分析
final TreeNode<K,V> root() {
取得包含这个节点的根节点

方法分析
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
这个方法确保root节点是table在这个位置的第一个节点,其实就是把TreeNode树中的root节点放到了链表(这个TreeNode因为是继承自Node并且增加了prev,所以同时也是一个链表)的最前端,实现方式如下:
1、如果是这些情况不做任何处理直接退出:table未初始化、table大小为0。
2、如果root本身就已经是在table的这个索引位置的链表的头部,直接跳过3~6执行7
3、如果root的next元素不为null,则让这个元素的prev元素跳过root直接指向root的prev元素
4、如果root的prev元素不为null,则让这个元素的next元素跳过root直接指向root的next元素
5、将链表头元素的prev元素指向root
6、将root的next元素指向链表头元素,将root的prev元素置为null
7、最后调用checkInvariants方法递归的检查这个树是否有异常

方法分析
static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
1、检查t的prev节点的next节点是否指向t
2、检查t的next节点的prev节点是否指向t
3、检查t的parent节点的left、right节点中是否有t
4、检查t的left节点的parent节点是否为t,检查left节点的hash是否<t的hash
5、检查t的right节点的parent节点是否为t,检查right节点的hash是否>t的hash
6、检查t和他的叶子节点是否为连续的红树
7、递归检查t的left节点
8、递归检查t的right节点

方法分析
final TreeNode<K,V> find(int hash, Object key, Class<?> kc) {
1、检查传入hash,如果小于p的hash,则将p指向p.left(下一轮循环直接查找左侧树)
2、否则检查传入hash,如果大于p的hash,则将p指向p.right(下一轮循环直接查找右侧树)
3、否则,如果传入hash等于p的hash,则判断传入key是否等于当前p的key,如果相同,返回当前p节点作为返回值
4、否则,如果左树为null,p指向右树(如果右树也为null,则会在本轮查找完成后退出返回null)
5、否则,如果右树为null,p指向左树(如果左树也为null,则会在本轮查找完成后退出返回null)
6、否则,如果k和pk都实现了Comparable接口,并且调用compareTo方法不等0,则根据返回结果是负数还是正数,确定p指向左树还是右树
7、否则,直接递归查找右树,如果能找到,则返回找到的结果
8、否则,p指向左树,如果p不为null,重新进行1~8步,直到有查找结果或者在p为null时返回nul

方法分析
getTreeNode
调用了find方法

方法分析
final void treeify(Node<K,V>[] tab) {
1、按照链表的方式遍历这个TreeNode
2、首先把第一个节点设置成root,并且设置red为false(黑树)
3、遍历下一个节点x时
4、先将root设置为比较节点p
5、首先判断x的hash值和当前p的hash值(如果hash无法判断并且没有实现Comparable接口的话使用默认的tieBreakOrder方法简单给出大小)
6、如果比当前p小,则将p指向p的左节点,反之则将p指向p的右节点
7、如果此时p节点仍然存在,则继续执行前面的逻辑(5~7)判断x的hash和p的hash
8、如果此时p节点不存在,则将x放在这棵树的这个位置,并调用balanceInsertion方法平衡红黑树
9、继续遍历下一个节点,执行4~8,直到链表全部遍历完
10、最后调用moveRootToFront确保root节点在链表的头部

方法分析
final Node<K,V> untreeify(HashMap<K,V> map) {
简单的将链表所有TreeNode替换成Node

方法分析
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) {
1、首先将root节点设为当前查找节点p
2、如果传入hash值小于p的hash值,则下一轮查找从p的左侧树查找
3、如果传入hash值大于p的hash值,则下一轮查找从p的右侧树查找
4、如果传入hash值等于p的hash值,并且p的key和传入的key相同,则返回当前查找节点p(注意,这时候并没有在这里直接修改其value值)
5、如果当前p的key和传入的key都实现了Comparable接口,并且比较结果不为0,则根据比较结果如3、4一样确定下一轮要查找的节点
6、如果两个key没有实现Comparable接口或比较结果为0,则直接调用find方法分别从做子树和右子树查找
7、如果能查找到,则直接返回查找到的节点(不修改其value值)。
8、如果不能查找到,则说明当前树中没有这个传入key的节点,则直接调用tieBreakOrder方法简单判断应该从左子树查找还是从右子树查找
9、如果下一轮要查找的子树存在,则继续查找,进行2~8步
10、如果不存在,则将传入的hash、key、value等值创建一个新的TreeNode,并且放在不存在的子树的位置。
11、并且把这个新的TreeNode插入到链表的当前查找节点p和p的next之间(如果p的next节点不存在,则将新建的TreeNode节点放到p的next位置,即链表尾部)

方法分析
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) {
1、如果table未初始化或者大小为0,直接退出
2、、取得当前hash值索引的TreeNode节点
3、取得待删除节点的next节点succ,以及待删除节点的prev节点pred
4、如果待删除节点就是链表的头(根节点),则去掉链表头,table索引指向链表的第二个节点。
5、否则从链表中间删除这个节点
6、如果这个TreeNode太小,直接将它的结构改回Node链表,并且返回
7、前面是在处理链表结构,下面开始处理树结构
8、找到待删除节点p的右子树中的子树中的最左边节点s,交换s和p的颜色
9、下面一系列操作很乱,其实就是把这两个节点交换在树中的位置
注:这里为什么挑右树中的子树中的最左边节点呢,因为他一定比原p大,而比p的右树中的其他节点都小,用它来替换原来p的位置最合适
10、如果最终交换完毕后p仍然有子叶(或者p只有一个子节点,这样也不会进行前面的替换),则再进行一次交换,将这两个节点交换一次,然后删除掉p
11、如果删除的p的颜色(之前已经交换过颜色)不是红色,则进行一次balanceDeletion
12、如果最终交换完毕后p没有子叶,则直接从树结构中删除p
13、按需进行moveRootToFront

方法分析
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
1、传入的index是当前TreeNode在旧table的索引,bit是旧table的容量
2、以链表的方式遍历这个树,遍历的同时打乱他们的链表关系
3、如果节点hash计算出的index在旧table的总index容量内,则分到低位树中
4、如果节点hash计算出的index不在旧table的总index容量内,则分到高位树中
5、如果低位树长度太小,直接转成Node链表,存在原来table的index位置中,如果低位树长度不小,则调用treeify方法将其树化并存放在原来table的index位置中
6、高位树逻辑相同,只是存在table的(index + bit)位置中
7、另外在5和6方法中,如果只有一个低位树,高位树没有生成(反之亦然),则不需要再进行树化

最后的四个方法是树相关的操作,先看一下树和树的旋转的概念
http://www.cnblogs.com/skywang12345/p/3245399.html

方法分析
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) {
1、如果待旋转的p为null或者p的右子树为null,直接返回,否则设p的右节点为r
2、将p的右节点指向r的左节点,并且将r的左节点的父节点设成p
3、将r的父节点指向p的父节点,如果没有父节点,则将r的颜色置位黑色
4、如果有父节点,则看p原来是左节点还是右节点,相应的把p的父节点的相应位置的引用改成r
5、将r的左节点指向p,p的父节点指向r,旋转完成

方法分析
rotateRight和rotateLeft类似,只是方向相反

红黑树增加节点、删除节点时调整树结构(旋转,改变颜色等)
http://www.cnblogs.com/skywang12345/p/3245399.html
其中2.1,2.2,2.3示意图应该是有问题的,删除操作的case4说法也是有些问题的

方法分析
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
1、设置插入节点是红色
2、把插入节点作为当前调整节点x开始调整整个树
3、如果当前调整节点没有父节点,则设置它的颜色为黑,返回当前调整节点
4、如果当前调整节点的父节点为黑色,或者祖父节点不存在,则不需要再调整,退出
5、如果当前调整节点的父节点xp是“当前调整节点的祖父节点的左节点xppl”,则做如下操作
A、如果存在叔叔节点(当前调整节点的祖父节点的右节点xppr),并且叔叔节点的颜色为红色,则
a、叔叔节点、父节点设置成黑色,祖父节点设置成红色,把祖父节点当做当前调整节点,继续调整
B、如果叔叔节点不存在或者其颜色为黑,则做如下操作
a、如果当前调整节点是其父节点的右子节点,则把父节点左旋,并把父节点(已经左旋到了原子节点的位置)当做当前调整节点
b、这时候当前调整节点一定是其父节点的左子叶。设置父节点颜色为黑,设置祖父节点为红,并把祖父节点右旋
6、如果当前调整节点的父节点xp是“当前调整节点的祖父节点的右节点xppr”,则做如下操作
A、如果叔叔节点存在并且是红色,则置叔叔节点、父节点为黑色,祖父节点为红色,把祖父节点当做当前调整节点
B、如果叔叔节点不存在或者是黑色,则
a、如果当前调整节点是其父节点的左节点,则对父节点右旋,并且把父节点当做当前调整节点
b、这时当前调整节点应该位于其父节点的右节点,置父节点颜色为黑色,祖父节点为红色,对祖父节点左旋
7、继续上述3~6直至满足退出条件退出

方法分析
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) {
1、如果当前调整节点为null或为root,直接退出
2、如果当前调整节点没有父节点(是根节点),则设置颜色为黑,退出
3、如果当前调整节点为红色,则置为黑色,退出
4、如果当前调整节点是他的父节点的左子节点,则
A、如果有兄弟节点,且兄弟节点是红色,则设置兄弟节点为黑色,父节点为红色,把父节点左旋,重新设置兄弟节点
B、如果没有兄弟节点,则将父节点作为当前调整节点
C、如果兄弟节点的左右节点都不是红色,则设置兄弟节点为红色,将父节点作为当前调整节点
D、如果兄弟节点的右子节点是黑色,左子节点为红色,则设兄弟的左子节点为黑色,兄弟节点为红色,然后对兄弟节点右旋,然后重新设置兄弟节点
E、兄弟节点的颜色设置为当前调整节点的父亲的颜色,兄弟节点的右子节点设置成黑色
F、设置当前调整节点的父节点颜色为黑色(到这里其实就是调换了当前调整节点的父节点和兄弟节点的颜色,以便左旋),对当前调整节点的父节点左旋
G、将当前调整节点指向root(退出循环)
5、如果当前调整节点是他的父节点的右子节点,则类似处理,只是方向相反

小细节:
判断float是否为Nan需要用下列方法,直接判断loadFactor==Float.Nan不可用
Float.isNaN(loadFactor)
它内部的方法是判断loadFactor != loadFactor

原创粉丝点击