1.谈一下HashMap的特性?
1.HashMap存储键值对实现快速存取,允许为null。key值不可重复,若key值重复则覆盖。 2.非同步,线程不安全。 3.底层是hash表,不保证有序(比如插入的顺序)
2.谈一下HashMap的底层原理是什么?
基于hashing的原理,jdk8后采用数组+链表+红黑树的数据结构。我们通过put和get存储和获取对象。当我们给put()方法传递键和值时,先对键做一个hashCode()的计算来得到它在bucket数组中的位置来存储Entry对象。当获取对象时,通过get获取到bucket的位置,再通过键对象的equals()方法找到正确的键值对,然后在返回值对象。
3.谈一下hashMap中put是如何实现的?
1.计算关于key的hashcode值(与Key.hashCode的高16位做异或运算) 2.如果散列表为空时,调用resize()初始化散列表 3.如果没有发生碰撞,直接添加元素到散列表中去 4.如果发生了碰撞(hashCode值相同),进行三种判断 4.1:若key地址相同或者equals后内容相同,则替换旧值 4.2:如果是红黑树结构,就调用树的插入方法 4.3:链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。 5.如果桶满了大于阀值,则resize进行扩容
4.谈一下hashMap中什么时候需要进行扩容,扩容resize()又是如何实现的?
调用场景: 1.初始化数组table(懒加载,一开始是0,第一次初始化为16) 2.当数组table的size达到阙值时即++size > load factor * capacity 时,也是在putVal函数中 实现过程:(细讲) 1.通过判断旧数组的容量是否大于0来判断数组是否初始化过 否:进行初始化 判断是否调用无参构造器, 是:使用默认的大小和阙值 否:使用构造函数中初始化的容量,当然这个容量是经过tableSizefor计算后的2的次幂数 是,进行扩容,扩容成两倍(小于最大值的情况下),之后在进行将元素重新进行与运算复制到新的散列表中
jdk1.7 新数组扩容用的是头插法 重新计算元素在数组中位置的方法是使用类似于取模的方法(其实还是&length-1) jdk1.8 新数组扩容用的是尾插法 重新计算元素在数组中位置的方法是使用 看新增的哪一位(最高有效位)是0还是1,是0的话位置不变,是1的话位置为原来的位置索引+n
概括的讲:扩容需要重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结构,把所有的元素挨个重新hash分配到新结构中去。 PS:可见底层数据结构用到了数组,到最后会因为容量问题都需要进行扩容操作
5.谈一下hashMap中get是如何实现的?
对key的hashCode进行hashing,与运算计算下标获取bucket位置,如果在桶的首位上就可以找到就直接返回,否则在树中找或者链表中遍历找,如果有hash冲突,则利用equals方法去遍历链表查找节点。
6.谈一下HashMap中hash函数是怎么实现的?还有哪些hash函数的实现方式?
对key的hashCode做hash操作,与高16位做异或运算 还有平方取中法,除留余数法,伪随机数法 hashcode值&(数组的长度-1) 分为三步
- 取key的hashcode值
- hashcode值右移16位之后,拿这个值与原先的hashcode值做异或运算 3.用第二步的值与数组的长度减一做与运算 这个算法在数组长度是2^N时与直接取模数组长度等价,但是&预算比取模更快 h & (length-1) == h%length 这个算法在数组长度是2^N时与直接取模数组长度等价,这也是数组的长度必须得是2^N 和 扩容是变为原来的二倍的原因
7.为什么不直接将key作为哈希值而是与高16位做异或运算?
因为数组位置的确定用的是与运算,仅仅最后四位有效,设计者将key的哈希值与高16为做异或运算使得在做&运算确定数组的插入位置时,此时的低位实际是高位与低位的结合,增加了随机性,减少了哈希碰撞的次数。
HashMap默认初始化长度为16,并且每次自动扩展或者是手动初始化容量时,必须是2的幂。
8.为什么是16?为什么必须是2的幂?如果输入值不是2的幂比如10会怎么样?
1.为了数据的均匀分布,减少哈希碰撞。因为确定数组位置是用的位运算,若数据不是2的次幂则会增加哈希碰撞的次数和浪费数组空间。(PS:其实若不考虑效率,求余也可以就不用位运算了也不用长度必须为2的幂次) 2.输入数据若不是2的幂,HashMap通过一通位移运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字
- 效率问题,其实hash取模时,数组的长度是素数更合适,但是选择合数是因为一些考虑。例如计算在数组的位置时用了h&(length-1)只有当length是2的幂时,结果才与h%length相等。但是&运算更快。
9.谈一下当两个对象的hashCode相等时会怎么样?
会产生哈希碰撞,若key值相同则替换旧值,不然链接到链表后面,链表长度超过阙值8就转为红黑树存储
10.如果两个键的hashcode相同,你如何获取值对象?
HashCode相同,通过equals比较内容获取值对象
11."如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
超过阙值会进行扩容操作,概括的讲就是扩容后的数组大小是原数组的2倍,将原来的元素重新hashing放入到新的散列表中去。
12.HashMap和HashTable的区别
相同点:都是存储key-value键值对的 不同点: HashMap允许Key-value为null,hashTable不允许; hashMap没有考虑同步,是线程不安全的。hashTable是线程安全的,给api套上了一层synchronized修饰; HashMap继承于AbstractMap类,hashTable继承与Dictionary类。 迭代器(Iterator)。HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException。 容量的初始值和增加方式都不一样:HashMap默认的容量大小是16;增加容量时,每次将容量变为"原始容量x2"。Hashtable默认的容量大小是11;增加容量时,每次将容量变为"原始容量x2 + 1"; 添加key-value时的hash值算法不同:HashMap添加元素时,是使用自定义的哈希算法。Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。
13.请解释一下HashMap的参数loadFactor,它的作用是什么?
loadFactor表示HashMap的拥挤程度,影响hash操作到同一个数组位置的概率。默认loadFactor等于0.75,当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,在HashMap的构造器中可以定制loadFactor。
14.传统hashMap的缺点(为什么引入红黑树?):
JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。
15. 平时在使用HashMap时一般使用什么类型的元素作为Key?
选择Integer,String这种不可变的类型,像对String的一切操作都是新建一个String对象,对新的对象进行拼接分割等,这些类已经很规范的覆写了hashCode()以及equals()方法。作为不可变类天生是线程安全的,
16 Get方法的流程是怎样的?
先调用Key的hashcode方法拿到对象的hash值,然后用hash值对第一维数组的长度进行取模,得到数组的下标。这个数组下标所在的元素就是第二维链表的表头。然后遍历这个链表,使用Key的equals同链表元素进行比较,匹配成功即返回链表元素里存放的值。
17 请说明一下HashMap扩容的过程
扩容需要重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结构,把所有的元素挨个重新hash分配到新结构中去。这个rehash的过程是很耗时的,特别是HashMap很大的时候,会导致程序卡顿,而2倍内存的关系还会导致内存瞬间溢出,实际上是3倍内存,因为老结构的内存在rehash结束之前还不能立即回收。那为什么不能在HashMap比较大的时候扩容扩少一点呢,关于这个问题我也没有非常满意的答案,我只知道hash的取模操作使用的是按位操作,按位操作需要限制数组的长度必须是2的指数。另外就是Java堆内存底层用的是TcMalloc这类library,它们在内存管理的分配单位就是以2的指数的单位,2倍内存的递增有助于减少内存碎片,减少内存管理的负担。
18 HashMap的长度为什么是2的幂次方?
HashMap为了存取的高效,要尽量减小碰撞冲突,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就是把数据存到哪个链表中的算法;这个算法实际就是取模,hash%length,计算机中直接求余效率不如位移运算,源码中做了优化hash&(length-1),而hash%length==hash&(length-1)的前提是:length是2的幂次方。
19 为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键?
20 Hashmap的结构,1.7和1.8有哪些区别?
JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
扩容后数据存储位置的计算方式也不一样:
在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1)
而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是:扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。
在计算hash值的时候,JDK1.7用了9次扰动处理=4次位运算+5次异或,而JDK1.8只用了2次扰动处理=1次位运算+1次异或。
扩容流程:
添加数据流程:
具体区别:
21 TreeMap和HashMap的区别
Map:在数组中是通过数组下标来对 其内容进行索引的,而Map是通过对象来对 对象进行索引的,用来 索引的对象叫键key,其对应的对象叫值value;
1、HashMap是通过hashcode()对其内容进行快速查找的;HashMap中的元素是没有顺序的;
TreeMap中所有的元素都是有某一固定顺序的,如果需要得到一个有序的结果,就应该使用TreeMap;
2、HashMap和TreeMap都不是线程安全的;
3、HashMap继承AbstractMap类;覆盖了hashcode() 和equals() 方法,以确保两个相等的映射返回相同的哈希值;
TreeMap继承SortedMap类;他保持键的有序顺序;
4、HashMap:基于hash表实现的;使用HashMap要求添加的键类明确定义了hashcode() 和equals() (可以重写该方法);为了优化HashMap的空间使用,可以调优初始容量和负载因子;
TreeMap:基于红黑树实现的;TreeMap就没有调优选项,因为红黑树总是处于平衡的状态;
5、HashMap:适用于Map插入,删除,定位元素;
TreeMap:适用于按自然顺序或自定义顺序遍历键(key);
22 hashmap的线程安全性
- 两个线程同时添加元素,此时都出发了扩容。有可能会导致环形链表的出现。tech.meituan.com/2016/06/24/… 1.7 下 头插法 过程 e = a temp = a.next e头插法插入后,e = temp 在x号桶有AB两个元素x->a->b->null,此时有1,2两个线程过来发生扩容。这时1号线程需要对A重哈希,记下来了e = a temp = a.next = b ;还未进行接下来的时,2号线程过来,对x号桶rehash,申请一下新的数组table2结果是 x->b->a,完成扩容。这时回到1号线程,申请一个新的数组table1,先对a头插,结果是x->a,temp= b,开始对b进行头插,结果是 x->b->a,又要对a进行头插,
1.8 使用尾插法 2号线程rehash结束后,结果为x->a->b;1号线程只是做了一遍遍历rehash,不会形成环。 2. 多个线程对hashmap的size进行++时,会丢数据。 3. 数据丢失问题 尾插法,头插法都会有数据丢失的问题 头插法 temp = x.next x.next = a a.next = temp 线程2在temp = x.next 后插入,则线程1再执行时,会丢失2插入的数据 尾插法 temp = x.next x.next = a a.next = temp 同理
23 Hashmap底层数据结构,以及hash寻址的原理性问题?
hashmap的底层是数组,每个数组位置是一个桶,为了解决冲突,每个桶会再接入链表或者是红黑树的结构。1.7 .1.8 put操作 根据key的32位hashcode右移16位后,再与本身做异或操作,也就是高16位与低16位做异或,之后与table.length-1做与操作,定位到具体的桶。如果key存在,则直接替换value。如果不存在key,1.7会做链表的一个插入操作;1.8会首先判断是链表还是红黑树,如果是链表会判断添加链表后是否会从链表转为红黑树。这里会涉及到hashmap的扩容,根据负载因子来判断是否需要扩容,默认是0.75,如果达到阈值会选择扩容。细节来说,put时会有四个参数,第一个参数hash值,
24 Hashmap高16位与低16位的异或操作的原因?
为了hashcode更加离散化,使得发生hash冲突可能更小 上面那个问题中为什么不用与或非而要用异或?为什么不用与或者或呢 举例子,对于10,11,01,00 这四个值, 如果是与的话,结果是1的概率是0.25,是0的概率是0.75 如果是或的话,结果是1的概率是0.75,是0的概率是0.25 如果是异或的话,结果是1的概率是0.5,是0的概率是0.5 所以从概率学上考虑的话,用异或更好
25 从源码层面阐述Hashmap在JDK1.8时什么条件下会转变为红黑树?
首先要满足第一个条件,链表长度大于八的时候才会尝试去转换。 过程中的还会对hashmap的size去进行判断,如果size超过64,并且链表长度大于8,这时候会去转化为红黑树。 如果size小于64,即使链表长度大于8,也不会去转换,会去选择扩容。
26 ConcurrHashmap在JDK1.7和JDK1.8的区别?
两个读线程访问hashtable时会有冲突 1.7时是使用了两层的嵌套,外层是16个segment,内层其实还是个hashmap,理想情况下可以同时支持16个线程的并发,源码层面使用的是retrainilock来进行加锁。 1.8是调整了一个锁的对象,把对segment的锁调整到锁的是hashmap中的每一个链表或者是红黑树,源码层面,使用了syncionzed和cas的操作,cas虽然会消耗比较多的cpu,但是时间复杂度是ologn ,但实际效果远高于1.7时1并发量的效益