目录
HashMap为什么速度快?/为什么要使用hashcode?
HashMap的初始容量和加载因子如果自己设置的话,设置什么比较好呢
扩容原理?/如果HashMap超过了负载因子定义的容量会怎么办?
可以使用ConcurrentHashMap来代替HashTable吗?
为什么String,Integer这样的warpper类更适合作为键
什么是HashMap?
HashMap是一个存储key-value的键值对集合,每一个元素都是一个Entry,这些键值对分散在数组当中。
你为什么要用HashMap?
- 解决问题需要的数据结构是一种键值对的数据结构
- HashMap是线程不安全的,其速度比较快
- HashMap在存储key的值时,允许为NULL
- 对于输入数据的顺序与输出数据的顺序没有特别要求(如果有特别要求,要用LinkedHashMap)
Hash函数的构造方法?
Hash函数的构造原则是简单和均匀,一般用到的hash()函数的构造方法有:
1.除留余数法 H(key)=key%p p是小于等于表长的最大素数
2.数字分析法 假设每个关键字都是由s位组成的,如果可以预先估算出全体关键字每一位数字输出的频率,可以从中提取出分布均匀的若干位,或者他们的组合作为hash地址
3.平方取中法 对于每一个数字,先平方,然后同2方法。采用平方是为了扩大相近数的差别。
4.分段叠加法 将关键字分割成位数相同的几部分,(最后一部分的位数可以不同),然后取这几部分进行叠加,叠加和舍去进位作为散列地址。
5.基数转换法 将关键字看成是另一种进制的函数,最后转换为本进制的函数,选择其中几位作为散列地址。
HashMap的数据结构?
数组+链表+红黑树
一个节点存放四个值:hash值,key值,value值,next执行该条链的下一个节点的指针。
HashMap的工作原理?
HashMap基于哈希原理。当我们通过put()方法和get()方法存取元素时,将键值对传给put方法时,通过调用hash方法得到hash值,然后计算出在数组中存储的位置,找到相应的桶的位置,将对象[entry]进行存储。如果key已经存在,value值会更新(hashMap不存放key重复的值)。用拉链法解决hash冲突。在获取对象时,通过hash值&(length-1),找到存储位置,通过equals方法,来确定是否找到
HashMap key的存储下标是怎么计算的?
用的哈希算法,首先根据key的值计算出hashcode的值,然后根据hashcode计算出hash值,最后通过hash&(length-1)计算得到存储的位置
计算hash值:
(h = key.hashCode())^(h >>> 16);简而言之,hashcode的值异或hashcode无符号右移16位的值
为什么要hashcode要异或其右移十六位的值?
对于计算出来的两个hashcode,如果其高位不同,低位基本相同,&(length-1)之后,hash碰撞的机会也别大,采用位运算,将其分散的更为均匀。
为什么hash值要与length-1相与?
Hash值要在数组中找一个对应的长度,本应该用hash值对数组的长度进行取模运算,但是hash值比较大,除数运算较为复杂,所以用hash值与length-1相与,取得一个在0到length-1之间的值,作为数组存储的下标。
HashMap存储的时候数组的长度为什么要是2的n次幂?
为了均匀分布,如果低位是1,相与之后得到的结果是尽可能多的。否则,有的位永远取不到,浪费了空间,增大了冲突,减慢了查询效率
HashMap为什么速度快?/为什么要使用hashcode?
取的时间复杂度是o(1),通过散列表取元素,根据hash值&(length-1),找到存储位置,如果是链表的话,就要在链表中查找,但是链表的查询时间复杂度为o(n),这是为了让hashmap的存储时间复杂度变为o(1),就要是hash冲突尽可能减少,采用了空间换时间的策略,在理想的状态下,取的时间复杂度为o(1),存储也是同样的。
怎样解决Hash冲突?
参考:https://www.cnblogs.com/wuchaodzxx/p/7396599.html
四种方法:
- 开放定址法
1.1线性探测再散列:
冲突发生时,顺序查看表中下一个单元,直到找到一个空单元,或查遍全表。表后面查完,如果没有,在表头继续查看(相当于是一个循环),步长为1.
1.2二次线性再散列
冲突发生时,分别在表的左右进行跳跃式探测,较为灵活,不易产生聚集,但缺点是不能探测到整个散列的地址空间。步长是1,-1,4,-4…
1.3伪随机探测再散列
建立一个随机数发生器,并给定一个随机数作为起始点。(随机数序列是 预先产生的)步长是一个随机序列。
2.拉链法
把所有具有地址冲突的关键字链在同一个链表中。
3.再哈希法
构造多个hash函数,当发生冲突时,用其他的哈希函数,产生hash值, 直到不冲突为止。
4.建立公共溢出区
将哈希表分为基本表和溢出表两个部分,凡是和基本表发生冲突的,一 律填入溢出表。
HashMap怎么解决hash冲突?
采用拉链法(链地址法)。当好多bin被映射到同一个桶时,如果桶中bin<6,采用链表存储,如果bin>8但是Hash表的容量小于64,依然用链表存储,对hash表进行扩容。,然后进行hash表的重构。如果bin>8并且Hash表的容量大于64,则将链表的结构转换为红黑树的结构进行存储。
HashMap什么时候开辟数组,占用内存
第一次put的时候,而不是new的时候,在put的时候调用resize方法来开辟数组
初始化,构造函数?
Hashmap一共有四个构造函数
HashMap()没有指定时,使用默认的初始化大小16,使用默认的加载因子0.75
HashMap(int inititalCapacity)
HashMap(int inititalCapacity,float loadFactor)
指定初始化大小,hashmap调用tablesize,找到大于指定初始化容量大小的最小二次幂值
int n=cap-1;
n|=n>>>1;
n|=n>>>2;
n|=n>>>4;
n|=n>>>8;
n|=n>>>16;
返回n+1表示初始的大小
table初始化不是在初始化时完成的,而是在resize的时候完成的。
HashMap的初始容量和加载因子如果自己设置的话,设置什么比较好呢
扩容非常耗时,我们在设计初始容量时,应该尽可能避免这种情况。一般情况下:
(需要的数组容量)/加载因子+1;
加载因子默认是0.75,如果加载因子过小的话,就会浪费内存,如果加载因子过大的话,就会产生hash冲突比较多
HashMap put()的步骤
1.首先根据key的值计算hash值,找到该元素在数组中存储的下标
2.如果数组是空的,则调用resize进行初始化
3.如果没有碰撞直接放在对应的数组下标里
4.如果碰撞了,且节点已经存在,就替换掉value
5.如果碰撞后,发现该节点是树结构,就将这个节点挂在树上
6.如果碰撞后是链表,就把节点添加到链表的表尾,然后判断该链表是否>8,如果大于8但是数组容量小于64,就进行扩容
7.如果链表节点大于8并且数组的容量大于64,则将这个结构转换为树形结构
扩容原理?/如果HashMap超过了负载因子定义的容量会怎么办?
Hashmap在容量超过负载因子所定义的容量之后,就会扩容。Hashmap的默认负载因子是0.75.阈值=0.75*容量,也就是说当前hashmap存储的容量超过负载因子的容量就会扩容。这时就会将hashmap的大小扩大为原来数组的两倍。并将原来的对象放入新的数组中。这个过程叫做再hash,因为他调用hash方法来确定。
怎样进行重构hash表
https://segmentfault.com/a/1190000015812438
扩容时,要进行hash表的重构,对于一个链上挂了多个节点的情况,遍历这条链表,对于每个节点的处理方法如下:
建立两条链表,lo和hi, 如果当前节点的hash值&oldCap(原数组的容量)==0,将其挂在lo链表上,如果hash&oldCap(原数组的容量)==1,将其挂在hi链表上。最后将lo链表存储在j的位置,就是原来的位置,将hi链表存储在j+oldCap的位置。
原理:
假设数组之前是16位,0000 0000 0000 1111&hash值,相当于取hash值的第四位,当数组扩容之后32位,0000 0000 0001 1111&hash值取得是hash值的低五位,新的存储位置与原来的存储位置相比较,只有第五位发生了变化,第五位要么是0要么是1,这就使得新的结果要么和原来的结果相同,要么在原来的结果上加10000,也就是加上oldCap的值。新旧位置的不同在于hash值的第4位不同,(0是最低位),这个时候要拿到这一位,就可以hash&oldCap,如果相同,如果结果是0,则还在原来的位置存放。如果结果不相同,在原来的位置上+oldCap
重新调整HashMap的大小存在什么问题?
https://www.cnblogs.com/andy-zhou/p/5402984.html#_caption_0
在多线程的环境下存在条件竞争。如果两个线程都发现HashMap需要调整大小,那么他们会同时尝试调整大小。就有可能出现环形链表,导致程序出现死循环,(不是死锁)
因为jdk1.8之前链表的插入用的是头插法,所以在多线程的环境下有可能导致环形链表的出现,而jdk1.8之后链表的插入用的是尾插,不会出现这个问题,所以jdk1.8之后hashmap扩容不会出现死循环。
多线程并发情况下使用HashMap可能会存在什么问题?
- 多线程同时操作put()方法可能会导致get()操作发生死循换,环形链表的出现[jdk1.8之后没有这个问题]
- 多线程put()操作导致元素丢失
如果两个线程都同时获得了e,指向该链表的尾指针,则各自执行各自的,最后执行的覆盖原来的,另外一个线程所做的更改就会丢失。
- put非null元素之后get出来的却是null。
线程1在重建表的时候会把原来的数组中的元素置空,而线程2在刚开始的时候得到的却是原数组,这是原数组被置空,线程2得到的就是null
HashMap怎样转换为线程安全的?(三种方法)
- 用HashTable替换HashMap
- Collections.synchronizedMap将HashMap包装起来,里面所有的方法都加上了同步锁
- ConcurrentHashMap替换HashMap
可以使用ConcurrentHashMap来代替HashTable吗?
Concurrent可以代替HashTable,HashTable可以提供更强的安全性,因为每一个方法都用了同步锁,但是在线程竞争激烈的情况下,HashTable的性能非常低下,ConcurrentHashMap的同步性能较好,因为他是根据map的性能级别,对于map中的一部分进行上锁。
介绍ConcurrentHashMap
https://blog.csdn.net/qq_27093465/article/details/52279473
Java.util.concurrentHashMap
采用了分段锁技术。首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据的时候,其他段的数据也能被线程访问。
ConcurrentHashMap是由segment数组和HashEntry数组组成,Segment是一种可重用锁ReenTrantLock扮演锁的角色,HashEntry用于存储键值对数据。一个ConcurrentHashMap里面包含一个segment数组。一个segment中包含一个HashEntry数组。
为什么String,Integer这样的warpper类更适合作为键
String,Integer这些类不可变,是final的,其hashcode和equals方法重写了。其他的wrapper类也有这个特点,如果hashcode值是可变的,就不能从HashMap中找到你想要的对象。
可以使用自定义的对象作为键吗
可以,可以使用任何对象作为键,只要他遵循equals和hashcode方法的规则
为什么大部分hashcode方法使用31?
31是一个奇素数,如果是偶数的话,会产生溢出。习惯上在散列中用素数。31有一个很好的性能,用移位和减法来代替乘法,可以得到很好的性能。31*i=i<<5-i;
HashMap在jdk1.8之后添加了什么?
- Jdk1.7之前是数据每个节点声明是Entry类型的,jdk后声明为Node类型,成员基本没变,只是把hash声明为了final
- 在一定情况下,拉链链表中的元素超过8个时,采用红黑树
- 扩容时,不用重新结算位置,要么在原位置,要么在原位置的基础上移动二次幂的位置
- 链表的插入用尾插法,jdk1.7链表插入用头插,多并发的情况下容易形成环形链表,造成死循环。用尾插法解决了这个问题。
- Jdk1.8的hash方法在求得hash值得同时,将其与右移16位的结果异或,高低位都参与运算,是的hash结果更为均匀。
HashMap的遍历
public class Demo05 {
public static void main(String[] args) {
//HashMap遍历的两种办法
Map map=new HashMap();
map.put(1,7);
map.put(2,9);
map.put(3,3);
Iterator it1=map.entrySet().iterator();
while (it1.hasNext())
{
Map.Entry entry=(Map.Entry)it1.next();
Object key=entry.getKey();
Object value=entry.getValue();
System.out.println(key+" "+value);
}
Iterator it2=map.keySet().iterator();
while(it2.hasNext())
{
Object key=it2.next();
Object value=map.get(key);
System.out.println(key+" "+value);
}
}
}
第一种效率高且推荐使用
因为HashMap的这两种遍历分别是对keySet和EntrySet进行迭代,对于keySet实质上是遍历了两次,一次转为itertor迭代器遍历,一次就从HashMap中取出key所对应的value进行操作。(通过key值hashcode和equals索引),而entrySet方式只遍历了一次,他把key和value都放在了entry中,所以效率高