关于HashMap的一些
这里附上这个链接:
https://mp.weixin.qq.com/s/ce_No728D2H18BDeGecjng(为什么 HashMap 加载因子是0.75?而不是0.8,0.6?)
这篇博客的内容包括:
- 1,HashMap的概念:
- 2,HashMap的实现原理:
- 3,为什么我们建议在定义HashMap的时候,就指定它的初始化大小呢?
- 4,那HashMap什么时候扩容呢? 扩容到多少?怎么扩容呢?
- 5,你知道为什么HashMap是线程不安全的吗?
- 6,HashMap怎样解决hash冲突
- 7,为何HashMap的数组长度一定是2的次幂?
- 8,hashmap扩容时,每个entry需要重新再计算一次hash吗?
- 9,Hashmap的结构,1.7和1.8有哪些区别?
- 10,HashMap与Hashtable的区别?
- 11,有没有可能两个不相等的对象有相同的hashcode?
- 12,我们可以在hashcode()中使用随机数吗?
- 13,为什么重写equals()方法的同时也要重写hashcode()方法?
- 14,若两个键的hashcode相同,你如何获取值对象?
- 15,附上几个连接:
1,HashMap的概念:

Hash表是基于Map接口实现的。该实现提供了所有可选的映射操作,并允许使用空值和空键。 这个类不能保证数据的顺序;尤其它不能保证顺序会随着时间的推移保持不变。
HashMap的底层实现是:数组+链表,在jdk8以后又加入了红黑树
HashMap 的特殊存储结构使得在获取指定元素前需要经过哈希运算,得到目标元素在哈希表中的位置,然后再进行少量比较即可得到元素,这使得 HashMap 的查找效率变得很高。下面是hashmap的存储方式

2,HashMap的实现原理:
HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。整体结构图:

HashMap由数组+链表组成的。
数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
其他几个重要字段:
//实际存储的key-value键值对的个数
transient int size;
//阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到
int threshold;
//负载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;
//用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException
transient int modCount;
实现原理:
- (1)通过hash的方法,通过put和get存储和获取对象。
- (2)存储对象时,我们将K和V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load
Facotr则resize为原来的2倍)。 - (3)获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。
- (4)如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。(因为树的平均查找长度是log(n),当n为8时,树的平均查找长度是3,若继续使用链表O(n),因为树节点所占空间是普通节点的2倍,所以只有当节点足够多时,才使用树)
put方法:
(1)若hash表没有初始化,则会进行初始化扩容resize():
if((tab = table) == null || (n = tab.length )== 0)
n = (tab = resize().length);
(2)找到k对应的hash桶的位置,查看此位置是否为空,若为空,则直接新new一个节点,放到这个位置
if((p = tab[i = (n-1) & hash]) == null)
tab[i] = newNode(hash.key,value,null);
(3)若对应的位置不为空,先判断此位置的key是否和要插入的key是一个k:
①若相等,则用新值在链表中进行覆盖或者在红黑树中进行覆盖
if(p.hash == hash &&(kv==vp.key) == key || (key != null && key.equals(key)))
e = p;
else if(p instanceof TreeNode)
e = ((TreeNode<k,v>)p).putTreeVal(this,tab,hash,key,value)
②若不相等,则进入链表,同样的比较k相等,则新的替代旧的,否则插入到table[bucketIndex]处(即头结点处),要判断是否达到了链表转换为红黑树的阈值,达到了,则还要把链表转换成红黑树。
(4)map的调整次数+1:
++modCount;
(5)判断是否需要扩容。什么时候扩容?
- 初始化hashmap,第一次进行put操作
- 当键值对个数>threshold阈值时扩容,threshold=负载因子*size
- 扩容后的长度为原来hash表的2倍
下面为对应的源码:
public V put(K key, V value) {
//如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
int i = indexFor(hash, table.length);//获取在table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
addEntry(hash, key, value, i);//新增一个entry
return null;
}
//这是一个神奇的函数,用了很多的异或,移位等运算,对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
以上hash函数计算出的值,通过indexFor进一步处理来获取实际的存储位置:
/**
* 返回数组下标
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
h&(length-1)保证获取的index一定在数组范围内,举个例子,默认容量16,length-1=15,h=18,转换成二进制计算为
1 0 0 1 0
& 0 1 1 1 1
__________________
0 0 0 1 0 = 2
所以最终存储位置的确定流程是这样的:

get操作:
有了上面的put操作,那么这个get操作就比较简单了:
(1)先通过
hash(object)-->h
h&(length-1)得到hash桶的index
(2)根据index进行查找
get方法源码:
public V get(Object key) {
//如果key为null,则直接去table[0]处去检索即可。
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
get方法通过key值返回对应value,如果key为null,直接去table[0]处检索。

与上面这个图的过程是一样的找到对应的下标把值取出来。
3,为什么我们建议在定义HashMap的时候,就指定它的初始化大小呢?
当我们在对hashmap初始化时没有设置初始化容量,系统会默认创建一个容量为16的大小的集合,当HashMap的容量值超过了临界值(默认16*0.75=12)时,HashMap将会重新扩容到下一个2的指数幂(16->32)。HashMap扩容将要进行resize的操作,频繁resize,会导致降低性能。
(HashMap是线程不安全的,它的不安全就体现在resize的时候,多线程的情况下,可能会形成环形链表,导致下一次读取的时候可能会出现死循环。)
4,那HashMap什么时候扩容呢? 扩容到多少?怎么扩容呢?
- 1.添加元素的时候会检查容器当前元素个数。当HashMap的容量值超过了临界值(默认16*0.75=12)时扩容。
- 2.HashMap将会重新扩容到下一个2的指数幂(16->32)。
- 3.调用resize方法,定义长度为新长度(32)的数组,然后对原数组数据进行再Hash。这个过程是一个性能损耗点。
hashmap扩容机制:
hashmap的构造器里指明了两个对于理解hashmap比较重要的两个参数:int initalCapacity,float loadFactor,这两个参数会影响hashmap的效率,hashmap底层采用的散列表数据实现,利用initalCapacity这个参数我们可以设置这个数组的大小,也就是散列桶的数量,但若需要map的数据过多,在不断的add之后,这些桶可能会被占满,这时候有两种策略:一种是:不改变Capacity。因为即使桶被占满了,我们还是可以利用每个桶附带的链表增加元素,但是这个有缺点:此时hashmap就退化成了linkedlist,使put和get方法的时间开销上升,这就是采用另外一种方法:增加hash桶的数量,这样put和get时间开销又回退到近似于常数的复杂度上,hashmap就是采用这个方法的。
其实这个扩容时会造成环形链或数据丢失。也就是hashmap不安全。 这个接下来会写:
5,你知道为什么HashMap是线程不安全的吗?
jdk1.7中的HashMap
在jdk1.8中对HashMap做了很多优化,这里先分析在jdk1.7中的问题,相信大家都知道在jdk1.7多线程环境下HashMap容易出现死循环,这里我们先用代码来模拟出现死循环的情况:
public class HashMapTest {
public static void main(String[] args) {
HashMapThread thread0 = new HashMapThread();
HashMapThread thread1 = new HashMapThread();
HashMapThread thread2 = new HashMapThread();
HashMapThread thread3 = new HashMapThread();
HashMapThread thread4 = new HashMapThread();
thread0.start();
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
class HashMapThread extends Thread {
private static AtomicInteger ai = new AtomicInteger();
private static Map<Integer, Integer> map = new HashMap<>();
@Override
public void run() {
while (ai.get() < 1000000) {
map.put(ai.get(), ai.get());
ai.incrementAndGet();
}
}
}
上述代码就是开多个线程不断进行put操作,并且HashMap与AtomicInteger都是全局共享的。在多运行几次该代码后,出现如下死循环情形:

其中有几次还会出现数组越界的情况:

这里我们着重分析为什么会出现死循环的情况,通过jps和jstack命名查看死循环情况,结果如下:

从堆栈信息中可以看到出现死循环的位置,通过该信息可明确知道死循环发生在HashMap的扩容函数中,根源在transfer函数中,jdk1.7中HashMap的transfer函数如下:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
总结下该函数的主要作用:
在对table进行扩容到newTable后,需要将原来数据转移到newTable中,注意10-12行代码,这里可以看出在转移元素的过程中,使用的是头插法,也就是链表的顺序会翻转,这里也是形成死循环的关键点。
下面进行详细分析。
1.1 扩容造成死循环分析过程
前提条件:
这里假设
hash算法为简单的用key mod链表的大小。
最开始hash表size=2,key=3,7,5,则都在table[1]中。
然后进行resize,使size变成4。
未resize前的数据结构如下:

如果在单线程环境下,最后的结果如下:

然后在多线程环境下,假设有两个线程A和B都在进行put操作。线程A在执行到transfer函数中第11行代码处挂起,因为该函数在这里分析的地位非常重要,因此再次贴出来。

此时线程A中运行结果如下:

线程A挂起后,此时线程B正常执行,并完成resize操作,结果如下:

这里需要特别注意的点:由于线程B已经执行完毕,根据Java内存模型,现在newTable和table中的Entry都是主存中最新值:7.next=3,3.next=null。
此时切换到线程A上,在线程A挂起时内存中值如下:e=3,next=7,newTable[3]=null,代码执行过程如下:
newTable[3]=e ----> newTable[3]=3
e=next ----> e=7
此时结果如下:

继续循环:
e=7
next=e.next ----> next=3【从主存中取值】
e.next=newTable[3] ----> e.next=3【从主存中取值】
newTable[3]=e ----> newTable[3]=7
e=next ----> e=3
结果如下:

再次进行循环:
e=3
next=e.next ----> next=null
e.next=newTable[3] ----> e.next=7 即:3.next=7
newTable[3]=e ----> newTable[3]=3
e=next ----> e=null
注意此次循环:e.next=7,而在上次循环中7.next=3,出现环形链表,并且此时e=null循环结束。
结果如下:

在后续操作中只要涉及轮询hashmap的数据结构,就会在这里发生死循环,造成悲剧。
1.2 扩容造成数据丢失分析过程
遵照上述分析过程,初始时:

线程A和线程B进行put操作,同样线程A挂起:

此时线程A的运行结果如下:

此时线程B已获得CPU时间片,并完成resize操作:

同样注意由于线程B执行完成,newTable和table都为最新值:5.next=null。
此时切换到线程A,在线程A挂起时:e=7,next=5,newTable[3]=null。
执行newtable[i]=e,就将7放在了table[3]的位置,此时next=5。接着进行下一次循环:
e=5
next=e.next ----> next=null,从主存中取值
e.next=newTable[1] ----> e.next=5,从主存中取值
newTable[1]=e ----> newTable[1]=5
e=next ----> e=null
将5放置在table[1]位置,此时e=null循环结束,3元素丢失,并形成环形链表。并在后续操作hashmap时造成死循环。

jdk1.8中HashMap
在jdk1.8中对HashMap进行了优化,在发生hash碰撞,不再采用头插法方式,而是直接插入链表尾部,因此不会出现环形链表的情况,但是在多线程的情况下仍然不安全,这里我们看jdk1.8中HashMap的put操作源码:
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) // 如果没有hash碰撞则直接插入元素
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;
}
这是jdk1.8中HashMap中put操作的主函数, 注意第6行代码,如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中。
假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。
总结:(重要重要)
首先HashMap是线程不安全的,其主要体现:
在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
以上这个hashmap为什么不安全,要仔细看,看懂哦。。。。
6,HashMap怎样解决hash冲突
HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。
解决hash冲突的方法有很多,HashMap底层是通过链表来解决hash冲突的。

HashMap的底层数组Entry[] table结构如下:

Hashmap里面的bucket出现了单链表的形式,散列表要解决的一个问题就是散列值的冲突问题,通常是两种方法:链表法和开放地址法。
- 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位(hashmap中采用的就是单向链表法);
- 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。
7,为何HashMap的数组长度一定是2的次幂?
从key映射到hashmap数组的对应位置,会用到一个hash函数:
index = HashCode(key) & (Length - 1);
hashMap的数组长度一定保持2的次幂,比如16的二进制表示为 10000,那么length-1就是15,二进制为01111,同理扩容后的数组长度为32,二进制表示为100000,length-1为31,二进制表示为011111。
从下图可以我们也能看到这样会保证低位全为1,而扩容后只有一位差异,也就是多出了最左位的1,这样在通过 h&(length-1)的时候,只要h对应的最左边的那一个差异位为0,就能保证得到的新的数组索引和老数组索引一致(大大减少了之前已经散列良好的老数组的数据位置重新调换),个人理解。

还有,数组长度保持2的次幂,length-1的低位都为1,会使得获得的数组索引index更加均匀,比如:

我们看到,上面的&运算,高位是不会对结果产生影响的(hash函数采用各种位运算可能也是为了使得低位更加散列),我们只关注低位bit,如果低位全部为1,那么对于h低位部分来说,任何一位的变化都会对结果产生影响,也就是说,要得到index=21这个存储位置,h的低位只有这一种组合。这也是数组长度设计为必须为2的次幂的原因。

如果不是2的次幂,也就是低位不是全为1此时,要使得index=21,h的低位部分不再具有唯一性了,哈希冲突的几率会变的更大,同时,index对应的这个bit位无论如何不会等于1了,而对应的那些数组位置也就被白白浪费了。
小总结:
为何HashMap的数组长度一定是2的次幂?
- 最后的&运算取决于hashcode的后几位,一是:计算快,还有扩容后只有一位差异,也就是多出了最左位的1,这样在通过
h&(length-1)的时候,只要h对应的最左边的那一个差异位为0,就能保证得到的新的数组索引和老数组索引一致 - 不会因为length长度而对最后结果产生影响(数组索引index更加均匀)
8,hashmap扩容时,每个entry需要重新再计算一次hash吗?
当使用量达到临界值时(当前容量*0.75)时会扩容,扩容到原数组的2倍,把数据打散,在扩容后的数组中重新计算hash,找到新的位置。
9,Hashmap的结构,1.7和1.8有哪些区别?
不同点:
(1)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
(2)扩容后数据存储位置的计算方式也不一样:
在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。

扩容后,若hash值新增参与运算的位是0,那么元素在扩容后的位置=原始位置
扩容后,若hash值新增参与运算的位是1,那么元素在扩容后的位置=原始位置+扩容前的旧容量
在计算hash值的时候,JDK1.7用了9次扰动处理=4次位运算+5次异或,而JDK1.8只用了2次扰动处理=1次位运算+1次异或。
扩容流程对比图:

(3)JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率) (因为树的平均查找长度是log(n),当为8的时候,树的平均查找长度为3,若继续使用链表0(n),因为树节点所占空间是普通节点的2倍,所以只有当节点足够时,才会使用树。)

这里再进行补充两个问题:
(1)为什么在JDK1.7的时候是先进行扩容后进行插入,而在JDK1.8的时候则是先插入后进行扩容的呢?
//其实就是当这个Map中实际插入的键值对的值的大小如果大于这个默认的阈值的时候(初始是16*0.75=12)的时候才会触发扩容,
//这个是在JDK1.8中的先插入后扩容
if (++size > threshold)
resize();
其实这个问题也是JDK8对HashMap中,主要是因为对链表转为红黑树进行的优化,因为你插入这个节点的时候有可能是普通链表节点,也有可能是红黑树节点,当达到阈值时,就会扩容。
但是在JDK1.7中的话,是先进行扩容后进行插入的,就是当你发现你插入的桶是不是为空,如果不为空说明存在值就发生了hash冲突,那么就必须得扩容,但是如果不发生Hash冲突的话,说明当前桶是空的(后面并没有挂有链表),那就等到下一次发生Hash冲突的时候在进行扩容,但是当如果以后都没有发生hash冲突产生,那么就不会进行扩容了,减少了一次无用扩容,也减少了内存的使用
void addEntry(int hash, K key, V value, int bucketIndex) {
//这里当钱数组如果大于等于12(假如)阈值的话,并且当前的数组的Entry数组还不能为空的时候就扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容数组,比较耗时
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
//把新加的放在原先在的前面,原先的是e,现在的是new,next指向e
table[bucketIndex] = new Entry<>(hash, key, value, e);//假设现在是new
size++;
}
(2)为什么在JDK1.8中进行对HashMap优化的时候,把链表转化为红黑树的阈值是8,而不是7或者不是20呢(面试蘑菇街问过)?
如果选择6和8(如果链表小于等于6树还原转为链表,大于等于8转为树),中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
还有一点重要的就是由于treenodes的大小大约是常规节点的两倍,因此我们仅在容器包含足够的节点以保证使用时才使用它们,当它们变得太小(由于移除或调整大小)时,它们会被转换回普通的node节点,容器中节点分布在hash桶中的频率遵循泊松分布,桶的长度超过8的概率非常非常小。所以作者应该是根据概率统计而选择了8作为阀值。
//Java中解释的原因
* Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins. In
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 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
* more: less than 1 in ten million
10,HashMap与Hashtable的区别?
- (1)HashMap继承于AbstractMap,而Hashtable继承于Dictionary;
- (2)线程安全不同。Hashtable的几乎所有函数都是同步的,即它是线程安全的,支持多线程。而HashMap的函数则是非同步的,它不是线程安全的。若要在多线程中使用HashMap,需要我们额外的进行同步处理;
- (3)null值。HashMap的key、value都可以为null。Hashtable的key、value都不可以为null;
- (4)迭代器(Iterator)。HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的(jdk1.8之前没有使用。jdk1.8也使用了fail-fast。其用到了modcount++来记录状态,用这个值来标记状态,一旦在迭代过程中状态发生了改变,则会快速跑出一个异常,终止迭代行为)。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException。
- (5)容量的初始值和增加方式都不一样:HashMap默认的容量大小是16;增加容量时,每次将容量变为“原始容量x2”。Hashtable默认的容量大小是11;增加容量时,每次将容量变为“原始容量x2 - 1”;
- (6)添加key-value时的hash值算法不同:HashMap添加元素时,是使用自定义的哈希算法。Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。
- (7)速度。由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
11,有没有可能两个不相等的对象有相同的hashcode?
- 若两个对象equals,java运行环境会认为他们的hashcode一定相等
- 若两个对象不equals,他们的hashcode有可能是相等的
- 若两个对象hashcode相等,他们不一定equals
- 若两个对象hashcode不相等,他们一定不equals。
(插入/访问元素时根据hashcode值来计算其存储位置,equals是看2个元素的值()相等。所以,2个对象不equals,但有可能有相同的hashcode,也就是在这个位置上以链表式结构来保存多个对象==)
12,我们可以在hashcode()中使用随机数吗?
- java规范建议同一个类的equals()方法和hashcode方法应该是对应的
- 若两个对象的equals()返回值相等,hashcode()返回值也相等
- hashcode()计算过程中最好用到每个有意义字段的散列码
特别的是:不能使用随机数。因为同一个对象的hashcode值必须是相同的。也就是在程序运行过程中,同一个对象多次调用hashcode()方法应该返回相同的是,所以不能为随机数。
13,为什么重写equals()方法的同时也要重写hashcode()方法?
若只重写equals()方法而不重写hashcode()方法,将会违背equals()方法的特征原则: equals与hashcode的定义必须一致,2个对象equals为true,就必须有相同的hashcode,反之不成立。 也就是,在判断的时候,先根据hashcode进行判断,相同的情况下在根据equals()方法进行判断,若只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。
14,若两个键的hashcode相同,你如何获取值对象?
hashmap在链表中存储的是键值对**,找到哈希地址位置以后(即找到桶之后),会调用keys.equals()方法找到链表中正确的节点,最终找到要找的值对象**。
15,附上几个连接:
1,https://mp.weixin.qq.com/s/HgbyZZMmgIbHMRT1N7TgxQ
2,https://mp.weixin.qq.com/s/OPGMYl5P-VZXG__I1sIi8w
3,https://mp.weixin.qq.com/s/NZf9E95mt9EZ0sBUqwtI0Q
4,https://mp.weixin.qq.com/s/UdonpQAYbTqFvxZR1Cm5lg
5,https://mp.weixin.qq.com/s/dgYriZ-obbm1CoUDjHofhg
6,https://mp.weixin.qq.com/s/hS9jCS4LLK6pRfS85PSQ4A
7,https://mp.weixin.qq.com/s/qY7gFzL2thdjlYJunlgw4w
43万+

被折叠的 条评论
为什么被折叠?



