今天唠唠HashMap

今天唠唠HashMap_第1张图片

今天唠唠HashMap_第2张图片

切入点,说说几个典型的:

HashMap/ Hashtable/TreeMap

 

 

其中HashMap其实都被讲烂了,但是呢,咱还是说一说。

说道HashMap,我个人觉得,先从构造入手,毕竟先new,才不会空指针。也可以从静态入手。

先看看HashMap中有哪些常量

/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;

/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;

/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;

 

挨个聊聊

一、DEFAULT_INITIAL_CAPACITY

这里为什么要1<<4??????? 然后又名16  ;装逼。。。

其实也不是装逼,目的是:为了运算迅速,因为上述描述中,要求容量为2的整数次幂。至于为什么要是2的整数?并且我们观察最大容量也是2的整数次幂????

为什么呢?

今天唠唠HashMap_第3张图片

其实原因就在于减少hash碰撞;得到一个充分的散列数组

怎么减少呢?举例说明:

今天唠唠HashMap_第4张图片

今天唠唠HashMap_第5张图片

代码中确定一个元素在数组中的位置,是通过该元素的hash值 & newCap-1 得到数组下标。那么我们示例看看,该运算计算出的下标是什么:

1.hash算法

今天唠唠HashMap_第6张图片

今天唠唠HashMap_第7张图片

注:以上截图来自知乎大佬(说实话我没看懂,应该本质就是对质数的特性进行运算)

 

关于上述解释:s为什么取值为131,使用前面的字符串数组中,取得字符的ascii码,数值<=127,为了尽可能保证获取的hash的唯一性(减少hash冲突),因此需要让s是一个大于127的质数,二为了提高散列的密度,又要使得s尽可能小,因此大于127的最小质数,就是131。这个值就是最佳的散列质数和散列密度。

 

 

 

1.1解决hash冲突

今天唠唠HashMap_第8张图片

此处以hashMap中讲一讲链地址法:

将冲突的元素建立一个链表,将两个冲突的元素进行链表尾部增加。(后面会讲讲HashMap的数据结构,以及jdk8改动)

此处不深入。

 

 

二、

hash表的默认负载因子

什么是负载因子?负载因子是表示hash表中元素的填满的程度,如果负载因子越大,那么对于hash表中填满的元素就越多,好处就是:空间利用率高了,但是冲突的机会加大了。反之,负载因子越小,填满的元素越少,好处就是,冲突的机会减小了,但是空间的浪费就越多了。

冲突的机会越大,那么我寻址的成本越高,反之,查找的成本越小,因而查找的时间越小。

那么就得权衡一种阈值,泽中的值,官方给出的就是0.75f

换句话来说,就是当数组中的数据达到数组的容量的0.75的时候再扩容,提供了空间利用率,效率避免hash冲突,降低数据结构横向纵向。

( Java 是 0.75,Go 中是 0.65,Dart 中是0.8,python 中是0.762)

 

 

 

此处分析一下为什么是负载因子越大,填满就越多,越多就容易冲突???????

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs.  Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put).  The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations.  If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.

通常,默认的负载系数(.75)在时间和空间成本之间提供了一个很好的折中方案。 较高的值会减少空间开销,但会增加查找成本(在 HashMap 类的大多数操作中都得到体现,包括 get put )。 设置其初始容量时,应考虑映射中的预期条目数及其负载因子,以最大程度地减少再哈希操作的次数。 如果初始容量大于最大条目数除以负载因子,则将不会发生任何哈希操作。

这也是HashMap中的原话。

 

举个栗子,例如,我们有10间房间,我们有5个人需要住进去,那么第一个人好办,直接挑一间住;但是第5个人不好搞。有4/10的概率会碰到这间房间已经被住了人,那么怎么办呢?他需要再找下一间房间,那么但是又有3/9的概率碰到下一间房间也被住了人。这就是“hash碰撞,冲突”。当然此处在hash运算中不会是随机找房间。是有运算的,这也是减少了房间冲突的概率,例如,我们按照年龄来住房间顺序。这样就降低了房间冲突概率,较好散列。但是呢又会出现两个人年龄一样的概率。这个时候又得碰撞了。所以,在数据量达到一定范围,hash碰撞是会产生,所以需要制定碰撞解决方法。上述也说了。那么如果我们有10000间房间,有5个人住进去,那么这基本不冲突了。但是,10000间房间,这空间成本。。。。。

这是一个很简单的例子,可能不是很正确表达hash的原理。只是简单提供思路

这是笔者的原话。

 

 

三、阈值

今天唠唠HashMap_第9张图片

为什么将上述三个静态常量称为阈值呢?原因在于他们是用于比较大小居多。

今天唠唠HashMap_第10张图片

因为TreeNode的大小大约是常规节点的两倍,所以我们仅在垃圾箱包含足以保证使用的节点时才使用它们(请参阅TREEIFY_THRESHOLD)。 当它们变得太小(由于移除或调整大小)时,它们会转换回普通纸槽。 在使用具有良好分布的用户hashCode的用法中,很少使用树箱。 理想情况下,在随机hashCodes下,bin中节点的频率遵循泊松分布

(http://en.wikipedia.org/wiki/Poisson_distribution),默认调整大小阈值为0.75,平均参数约为0.5,尽管 由于调整粒度,差异很大。 忽略方差,列表大小k的预期出现次数是(exp(-0.5)* pow(0.5,k)/ factorial(k))。 第一个值是:

  0:0.60653066

  1:0.30326533

  2:0.07581633

  3:0.01263606

  4:0.00157952

  5:0.00015795

  6:0.00001316

  7:0.00000094

  8:0.00000006

  更多:不到一千万分之一

解释上述注释的原因是因为“树化阈值”为什么是8?上述表明了计算规则

(exp(-0.5)* pow(0.5,k)/ factorial(k))

今天唠唠HashMap_第11张图片

可以看出,oracle小老弟将通过上述公式,计算出当桶中出现元素个数出现的频率的概率,其原理就是按照泊松分布计算的。这里科普一下

Poisson分布,是一种统计与概率学里常见到的离散概率分布,由法国数学家西莫恩·德尼·泊松(Siméon-Denis Poisson)在1838年时发表。

今天唠唠HashMap_第12张图片

也就是说,非独立分布数据。

先说说其他优势:

红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为N/2,也就是等于8/2=4,那么这个时候:红黑树的查找效率是优于链表的,这个时候是需要树化的。

那么如果链表长度为6,那么红黑树的平均查找长度为log(6)=2.6,链表的平均查找长度为6/2=3。两者想差不大,但是转为树化结构和生成树的时间也会消耗,所以并不最优。这是网络上的答案,也是从查找效率上分析的。似乎不是oracle的想法。

 

那么回到正题,hash树化阈值(yu zhi)为什么是8,不是9,不是7呢?就是在于考虑到空间利用率和散列效率,在元素为8的时候,概率是比7低很多,然后比9就没必要了。

当元素为8个的时候,概率就基本为0了,0.00000006。。。。。。

那不会有童鞋天天往HashMap里面装千万条数据吧???有的话,也不要紧,我有红黑树。

此处有个问题需要注意,如果有人把对象的hashCode重写,那么将在哈希桶中出现多个元素的概率就会大大增加,例如,将某个元素的hashCode重写为返回1,那么对应1这个hash桶将全部的都是这个元素,将会横向增大hash表的数据。所以oracle引入了链表向红黑树转化的概念。

关于其中的0.5是什么,可能就是类似oracle小老弟计算的平均值。此处不深究

 

很多小老弟可能有疑问,为什么阈值不是一个值,而是两个值?

在元素大于等于8的时候树化,在小于等于6的时候树退化??????

因为两者如果都是8的话,那么就会陷入“树化”“树退化”来回循环转换的死锁中,如果是定义为7为树退化的阈值,那么在特定情况下,频繁的对同一个哈希桶的链表长度进行7~8之间转换(链表和红黑树来回转换),那么也会导致频繁的树化和树退化的转换。

那么为啥是6?不是5、4、3.呢?

1.比较概率:随着元素个数的减少,出现hash运算在同一个hash桶中的概率就会增加。

今天唠唠HashMap_第13张图片

2.比较成本,如果一个桶中,曾经元素个数达到9,但是,后续删除到了4,但是树退化阈值为3,然后结构一直为红黑树,那么就会增加查询成本,比如上述讲到:log(N)    和 N/2;那么就可以很清晰对比出log(3)== 0.47712125472,   3/2==1.5。红黑树结点占用空间大,并且在第三个常量。就是定义hash表树化阈值==64。除非说我们重写hashCode为常量。

查询数量虽然红黑树快,但是不利于增删改。这也是一个很大的弊端。那么对于权重来说,更多会把树退化阈值定义为6。

 

3.聊聊hash运算

今天唠唠HashMap_第14张图片

这个不能少

Computes key.hashCode() and spreads (XORs) higher bits of hash to lower.  Because the table uses power-of-two masking, sets of hashes that vary only in bits above the current mask will always collide. (Among known examples are sets of Float keys holding consecutive whole numbers in small tables.)  So we apply a transform that spreads the impact of higher bits downward. There is a tradeoff between speed, utility, and quality of bit-spreading. Because many common sets of hashes are already reasonably distributed (so don't benefit from spreading), and because we use trees to handle large sets of collisions in bins, we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.

计算key.hashCode()并将哈希的较高位扩展(XOR)到较低位。 由于该表使用2的幂次掩码,因此仅在当前掩码上方的位中发生变化的哈希集将始终发生冲突。 (众所周知的示例是在小表中保存连续整数的Float键集。)因此,我们应用了一种将向下扩展较高位的影响的变换。 在速度,实用性和位扩展质量之间需要权衡。 由于许多常见的哈希集已经合理分布(因此无法从扩展中受益),并且由于我们使用树来处理容器中的大量冲突集,因此我们仅以最便宜的方式对一些移位后的位进行XOR,以减少系统损失, 以及合并最高位的影响,否则由于表范围的限制,这些位将永远不会在索引计算中使用。

 

从理解来讲,其实这个注释已经讲清楚了。通过二进制运算,将key的hashCode运算并且将其高位扩展至低位。目的是为了提高散列效率。减少冲突频率。方式就是将其hashCode通过无符号向右位运算16位,然后再异或运算。至于为什么是16位,也是了一种较便宜的计算方式,折中损失。这样可以这样理解,就是对于直接使用对象的hashCode值不是很好,因为对于部分的对象,其hashCode值很有可能直接出现冲突(情况不举例了)。那么对于HashMap中的运用,为了有效的避免hash冲突,势必需要对其进行一些运算干扰,让其hash值更散列。减少冲突概率。但是还是有冲突概率。所以有链地址法。jdk8后出现了树化。

 

这里讲解一下异或,并且这里为什么要使用异或

关于异或的知识:

* A ^ 0 = A,即当0与一个数(0/1)进行异或操作时,结果等于这个数本身,如0 ^ 0 = 0、 0 ^ 1 = 1;

* A ^ 1 = ! A,即当1与一个数(0/1)进行异或操作,结果等于非-此数,或者说取反,如1 ^ 0 = 1, 1 ^ 1 = 0;

所以很好理解,就是将其二进制值打乱,通过与其无符号向右位运算16。通过二进制的特性,将其更效率的散列。减少hash碰撞。

具体运算过程不再赘述,(先调用hashCode(),再与其本身进行无符号右移16位的结果进行异或运算,然后再与(n-1)进行与运算)

 

 

下期我们分析一下hashmap源码,不得不说,HashMap是一个强大的工具类,里面全都是高级技术。

 

你可能感兴趣的:(Java基础)