HashMap源代码阅读初总结-看完线程安全之后再来看一遍

一:注释阅读

HashMap不是线程安全的。它实现了fail-fast iterator功能。即当他被遍历的过程中如果出现了结构改变的情况(增删元素,改变value的值不算),那么会报错。如果要实现线程安全的HashMap的话建议使用如下代码来在初始化的时候就实现

Map m = Collections.synchronizedMap(new HashMap(...));

二:应用注释

在普通初始化的时候,HashMap中每个值是List,当List中元素多到一定程度的时候会转化为(红黑)树。具体什么时候转化在下面会说。

在HashMap中的元素多到一定程度的时候会将HashMap进行扩容

节点首先根据哈希值进行排序,如果实例哈希值相同,并且都实现了Comparable接口,那么会用compareTo方法进行进一步排序(?)

在节点中元素变少时,树结构的链表会转化会普通的链表

在理想情况下,每个树中的节点的个数服从泊松分布(?)

三:变量注释

变量中主要规定了一些初始化要用到的量以及对HashMap进行resize要用到的量。

具体来说,规定了Map中元素的初始、最多数量和resize的阈值。以及将List结构改为树结构时的阈值。这里主要说一下改变结构(不是扩容)时的三个量,分别是TREEIFY_THRESHOLD=8,UNTREEIFY_THRESHOLD=6和MIN_TREEIFY_CAPACITY=32。当List中元素多于第一个值时,结构从List转化为Tree。当Tree中元素少于第二个值时,Tree改为List。在刚开始的时候,HashMap中数量比较小,此时应该更倾向于将HashMap扩容而不是将List改为Tree。第三个变量就是标识这种情况。当List中元素多于第一个值(8)而HashMap整体元素少于第三个元素(64)时,对HashMap进行扩容而不是改变结构

四:方法或内部类

内部类 Node

        Node类中定义了属性hash,Key,Value和next,提供了构造方法和get、set方法。重写了toString、hashCode和equals方法。

        改写的toString方法将key和value转为字符串返回。

        改写的hashCode方法返回key和code哈希值的异或和(同一个位置相同为1,不同为0,运算符号位^)

        改写的equals方法当Node和输入对象key、value值地址相等或地址相等时返回true。

        根据java规范,一般需要同时改写equals和hashCode方法,目的值保证equals认为相等的两个量哈希值也相等。比如在这里,如果不重写hashCode方法,那么调用hashCode会返回Node对应的哈希值。这时就可能出现两个被equals认为相等的量的哈希值不等。


                以下都为静态方法,我的理解是HashMap这个类为了方便自己而定义的方法

  • hash方法:在HashMap利用一个量的哈希值(32位整数)时,对低位(后16位)的利用会更加频繁,为了使高位信息也能被利用,因此在计算一个key的哈希值时选择将他的高位后移16位并与原哈希值做异或和
  • comparableClassFor:判断一个变量是否是可比较类型(实现Comparable接口),是的话返回他的类型,不是的话返回空。
  • compareComparables:如果x与kc(k的筛选可比较类)匹配,则返回 k.compareTo(x),否则为 0(原话,但没看懂)
  • tableSizeFor:返回给定值的二次方

                以下为成员变量(fields)

  • Node<K,V>[] table ;  transient类型变量:当对象被序列化的时候跳过这个变量。  这里是具体存放数据的地方

  • Set<Map.Entry<K,V>> entrySet ;  transient类型变量。储存缓冲entrySet()方法  (没看懂)

  • int size ; transient类型变量。  储存的key的个数

  • int modCount  ;transient类型变量。  被修改结构的次数(该Value不算)

  • int threshold 。 阈值,下一个重构的size值

  • float loadFactor  final类型变量。 负载因子 阈值 = 容量 * 负载因子


    以下为方法(棕色为私有方法)

  • 简单构造方法123  有两个可选参数,初始容量和负载因子。三种构造方法分别对应:两个都选,选初始容量和都不选。

  • 通过Map的构造方法4  要求Map的key是K的子类,value是V的子类。负载因子为默认。(其实就是把Map中的元素一个个地加到HashMap中)

  • void putMapEntries()   包级私有方法,外部无法调用。通过调用putVal实现把一个Map中的全部元素放到HashMap中

  • int size() 返回大小

  • boolean isEmpty() 返回是否为空(size是否为0)

  • V get() 返回key对应的value。 通过调用getNode方法实现。注意,返回值为空不代表不存在这个Key,也可能是值本身就是null

  • Node<K,V> getNode()  返回key对应的节点   需要输入参数值hash和Key。首先找到hash值对应的Node头在哪里(通过 & 来实现操作)。找到头之后如果树树结构就调用getTreeNode()来找到节点,链表结构就不停do-while向下迭代。代码中经常可以见到这样的比较语句。这样是为了保证不止比较地址,还可以比较值(如果key重写了equals方法的话)

    (k = first.key) == key || (key != null && key.equals(k)))
  • boolean containsKey()  返回是否有节点的key为给定值

  • V put()   调用putVal将key和val放入哈希表中(以默认方法将key和value放入哈希表中)。如果在放入之前哈希表中就已经有这个key了,则会用给定的value替换老的value,并返回老value。如果没有key则会把这个key和value作为新节点加到HashMap中,并返回null

  • V putVal() 有五个参数hash,key,value,onlyIfAbsent,evict(清除、驱逐)。前三个都好理解,解释一下onlyIfAbsent和evict。onlyIfAbsent:为true时不改变原有值,也就是只在缺失或节点value为空时put。evict:如果为 false,则表处于创建模式。用于标识是否是在创建HashMap中被调用(暂时的理解,这个值在HashMap中没用,主要是为什么实现一些钩子方法方便)。

  • Node<K,V> resize()   将HashMap重构的方法,返回重构后的table。首先更新容量和阈值。然后遍历老table的每一个链表。将链表中的节点依据  (e.hash & oldCap) 的值是否为0拆成高位链表与低位链表。高位链表的位置后移oldCap,低位链表不变(oldCap就是老容量)。这里的操作十分巧妙,我们来分析一下(8中操作与7中操作不同,这里说巧妙的8)。关于一个节点应该在哪个位置,应该是他的哈希值和(容量-1)做与运算。运算的结果实际上等于     hash值和(容量/2 - 1)的与运算结果 加上 hash值和(容量/2)的与运算结果。这么做的底气在于容量始终为2的次方,二进制数减一之前只有一个位置为1,减一之后这个位置之后的位置全为一。这个和的前半部分就是e的原始位置,后半部分就是 (e.hash & oldCap)。为0说明是低位节点、不移动;不为零说明是高位节点、并且取值只能是oldCap,向后移动oldCap个量。下面我们举个例子来说明。

  • void treeifyBin()   将哈希表中的链表替换为树(正如前面说的,替换要求HashMap达到一定的长度,太短的话选择扩容而非替换,短的标准为长度是否小于最小数化长度:64)
  • void putAll() 将给定Map中的全部元素放进HashMap中
  • V remove()  将给定key值的节点删除,返回对应节点的value。通过removeNode()方法实现
  • Node<key,value> removeNode()  有五个参数,分别为hash,key,value,matchValue,movable。后两个量顾名思义,一定要值匹配才删除和可移除。当matchValue为true时Value的值才有用,否则用不上。
  • void clear()   整个HashMap清空。 途径是将table中每个table[i]都改为null
  • boolean containsValue 判断是否包含某个value。包含的话返回true。途径就是遍历每个元素
  • Set<K> keySet() 返回HashMap中全部的key。这里key直接包含在一个transient的变量中。这个变量来自于HashMap继承的类AbstractMap。
  • Collection<V> values 返回HashMap中全部的value。这里value直接包含在一个transient的变量中。这个变量来自于HashMap继承的类AbstractMap。(和keySet()的实现逻辑是一样的)
  • Set<Map.Entry<K,V>> entrySet() 返回HashMap中全部的映射关系。(实现逻辑和keySet()一样)
  • V getOrDefault()   key有对应值且不为空时返回key对应节点的Value,否则返回参数defaultValue
  • V putIfAbsent()   只有key没有对应Value时才新建节点(老节点Value为空的话也新建),通过putVal实现
  • boolean remove()   实现逻辑和上面返回V的remove一样,通过调用removeNode()实现。不过这里是指定删除的key和对应的value。删除成功返回true
  • boolean replace()  输入参数key,oldValue和newValue。只有替换成功时才返回true
  • V replace()  输入参数key,value。返回被替换的值,如果key对应节点为空的话返回null
  • 下面四个函数作用很像,注意区分。
  • V computeIfAbsent()  有两个输入参数:key和mappingFunction。先检查HashMap中是否有key对应的Node,有的话返回对应的value,没有的话用给的函数计算一个value出来并插入到HashMap中。这里的mappingFunction是个一元函数
  • V computeIfPresent()  有两个输入参数:key和remappingFunction。这里是key对应的有值且不为空时才改变value,为空的话不进行任何操作。如果函数计算出来的值为空,那么会直接删去节点。要注意的是这里的函数是remappingFunction,即他的变量是节点的value和key,是个二元函数。
  • V compute()  和computeIfPresent()基本一样。有两个输入参数:key和remappingFunction。不过这里不管key有没有对应节点都将进行计算。有节点的话将节点对应的value传给remappingFunction进行计算,没节点的话把null传给remappingFunction。如果函数计算结果不为null,则更新节点或插入节点,为空的话移除该节点。注意,这里的remappingFunction是个二元函数。两个参数分别为key和value,value是可能为空的内个。
  • V merge()   merge:合并。有三个输入参数:key,value和remappingFunction。其实和compute()差不多。只不过merge的remappingFunction两个参数都是value。前一个是key对应节点的value,后一个是给的参数value。
  • void forEach()   没看懂BigConsumer是怎么用的,,,
  • void replaceAll()  依据给定的函数将每个节点的值重新计算。函数的两个变量分别为key和value
  • 下面的方法或内部类关系到clone、序列化迭代器。有的方法用来供LinkedHashMap重写。还没看懂,就不再罗列了

    下面说一下HashMap是如何将一个链表转化为红黑树的

  • 当向HashMap中加入节点的时候,如果满足链表向树转化的条件,就会调用treefiBin方法。转化条件为:链表长度大于等于8且HashMap长度大于等于64。这个方法只在putVal()、computeIfAbsent()、compute() 和merge()被调用过。(putAll等虽然也加入了节点,但他们都是通过putVal实现的)。

  • treefiBin方法将指定位置(根据输入参数hash确定)的链表上的全部节点由链表的Node节点类型转化为红黑树的TreeNode类型,然后调用treefi方法

  • treefi方法:内部类TreeNode的方法。从链表的头开始(头结点对应根节点),将已经转化为TreeNode<>类型的节点一个个以红黑树的规则挂到树节点上,然后将树放到原本链表的位置。

 五:其他

&运算(作为“与”)

        a和b两个数做&运算(a&b或b&a),会先将两个数转为二进制。然后将两个数通过高位补0的方式对齐。如果两个数的二进制值在某一位同时为1,则结果取1,有一个为0就取0。比如对二进制数1001和0011,做&运算的结果为0001。这里的两个数要求为整数,即int,byte,short和long以及相应的封装类(Integer等)

&运算(作为布尔运算)

        奖两个布尔类型转化为一个。作为结果来说和&&是一样的,但&&只有当第一个位真时才去判断第二个(短路运算符)。&会将两个都判断再输出。

接口中的default关键字

在java8中引入的关键字。在接口中,原本是不允许方法有方法体的。但是用default修饰之后就可以有方法体,并且不需要被重写。一个接口中可以有多个default方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值