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

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 是基本的存储数据的节点
包含4个字段,hash值,key、value,以及指向下一个Node的引用next

真正保存数据的table
transient Node[] table;

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


构造函数分析:
public HashMap(Map m) {
调用了putMapEntries

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

方法分析
final Node 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 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 root() {
取得包含这个节点的根节点

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

方法分析
final TreeNode 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[] 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 untreeify(HashMap map) {
简单的将链表所有TreeNode替换成Node

方法分析
final TreeNode putTreeVal(HashMap map, Node[] 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 map, Node[] 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 map, Node[] 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 TreeNode rotateLeft(TreeNode root, TreeNode 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 TreeNode balanceInsertion(TreeNode root, TreeNode 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 TreeNode balanceDeletion(TreeNode root, TreeNode 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

你可能感兴趣的:(java源码学习:1.8)