Java基础03——集合

59 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Collection接口

Collection接口包含三个子接口SetListQueue

Set接口:HashSet类(无序不重复),TreeSet类(有序不重复,底层是红黑树,自平衡的排序二叉树)

List接口:ArrayList类(数组,可随机访问,访问效率高,增删效率低)初始容量是10,右移扩容,约1.5倍

LinkedList类(链表,不能随机访问,访问效率低,增删效率高)

线程不安全

Queue接口:LinkedList类(链表,双端队列)

HashMap

基础

HashMap实现了Map接口,采用key-value形式存储,初始容量是16

JDK1.7版本

采用数组+链表的形式存储;

使用头插法,在并发情况下会产生环形链表数据覆盖的情况,get时产生死循环

JDK1.8版本 (改进红黑树的存储,尾插法,扩容时移动元素的方式)

采用数组+链表+红黑树的形式存储;

使用**尾插法,在并发情况下会产生数据覆盖以及A线程扩容会使得B线程get失败**,并引入了红黑树加快检索速度(复杂度变成O(logn))。

插入数据时,若数组长度大于64且链表长度大于阈值8,链表会进化为红黑树,移除数据时,链表长度低于阈值6,红黑树会退化为链表

下标计算

会用扰动函数进行二次hash,再计算下标

key对象的hashcode值异或hashcode的高16位获得一个hash值【(h = key.hashCode()) ^ (h >>> 16)】 ,再用hash值&数组长度-1【 (capacity -1) & hash

优点:

通过混合hashcode值的高位与低位增加低位的随机性,减少哈希碰撞。因为数组位置的计算是&运算,仅仅最后四位有效(15~001111)。

数组的长度是 2 的幂

(1)长度是 2 的幂才适用于位运算计算下标,位运算更高效。【 (n - 1) & hash

(2)为了数据的均匀分布减少哈希碰撞,(n-1)&hash位运算中,数组长度为2的倍数,减1后二进制全是1坐标的分布是否均匀就完全取决于hash值的分布是否均匀

PS:若不考虑效率,可使用求余法,长度也不用为2的幂。

比如HashTable数组长度是质数(初始11,扩容len*2+1),使用求余法hashcode%len获取下标。

(3)扩容时扩大2倍也是为了使用位运算的优化(PS:扩容为4倍、8倍也可以)。

扩容resize()

数组的size达到阙值时即 ++size > loadFactor * capacity 时,需要进行扩容。

扩容操作是新建原数组2倍大的新数组,并将原表结点拷贝到新表中,对原表无修改或修改很小。当原表所有结点都已被拷贝到新表中后,原表会被垃圾回收。

JDK1.7

计算新下标时,先申请一个更大的数组,然后将原数组里面的每一个元素重新 hash后再计算新下标9次扰动),然后进行移动。

JDK1.8

优化计算下标,不需要每个元素重新用【hash&(n-1)获得下标2次扰动),将key对象的hash值【hash&oldCapacity】获取之后&旧数组的长度来判断下标,遍历完链表或红黑树后,位置不变的结点存入低链表位置改变的结点存入高链表,原链表基本均分在高低链表中且顺序与原来相同,将低链表结点放入到新数组的 【旧下标】 位置,将高链表结点放入到新数组 【旧下标+旧数组长度】 位置。

原因

数组的长度是 2 的 n 次方,所以假设以前的数组长度(16)二进制表示是 010000,那么新数组的长度(32)二进制表示是 100000

它们之间的差别就在于高位多了一个 1,而我们通过 key 的 hash 值(由hashcode相关计算得出)计算在数组中下标的方法(数组长度-1) & hash。我们还是拿 16 和 32 长度来举例:

16-1=15,二进制为 001111

32-1=31,二进制为 011111

所以重点就在 key 的 hash 值的从右往左数第五位是否是 1,如果是 1 说明需要搬迁到新位置,且新位置的下标就是原下标+16(原数组大小) ,如果是 0 说明吃不到新数组长度的高位,那就还是在原位置,不需要迁移。

所以,我们刚好拿老数组的长度(010000)来判断高位是否是 1,这里只有两种情况,要么是 1 要么是 0 。

链表的数据是一次性计算完,然后一堆搬运的,因为扩容时候,节点的下标变化只会是原位置,或者原位置+老数组长度,不会有第三种选择。

总结一下,1.8 的扩容不需要每个节点重算 hash 算下标,而是通过和老数组长度的&计算是否为 0 ,来判断新下标的位置。

其他

HashMap的底层原理是什么

jdk8后采用数组+链表+红黑树的数据结构。通过put和get存储和获取对象,用键进行二次hash计算来得到它在bucket数组中的位置来存储Entry对象。当获取对象时,通过get获取到bucket的位置,再通过键对象的equals()方法找到正确的键值对,然后在返回值对象。

15. 平时在使用HashMap时一般使用什么类型的元素作为Key?

选择IntegerString这种不可变的类型,这些类已经很规范的覆写了hashCode()以及equals()方法。作为不可变类天生是线程安全的。

string 类型hashcode的计算方法,每个字符的AscII码加权求和得到,第i个字符乘31的(n-i+1)次幂

image-20220321154454903

为什么权是31?satckoverflow有讨论:

主要是因为31是一个奇质数,所以【31* i=32 * i-i=(i<<5)-i】,这种位移与减法结合的计算相比一般的运算快很多。

红黑树与链表转换的阈值如何确定的?

Hashmap中的链表大小超过八个时会自动转化为红黑树,当删除小于六时重新变为链表,为啥呢?

根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。

3.谈一下hashMap中put是如何实现的?

1.使用hash()方法,计算关于key的hashcode()值再与Key.hashCode的高16位做异或运算

hash() 的返回值*(h = key.hashCode()) ^ (h >>> 16)*

2.如果散列表为空时,调用resize()初始化散列表,默认初识容量(16)和默认加载因子(0.75)

3.如果没有发生碰撞,直接添加元素到散列表中去

4.如果发生了碰撞(hashCode值相同),进行三种判断

4.1:若equals后内容相同,则替换旧值

4.2:如果是红黑树结构,就调用树的插入方法

4.3:链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。

5.如果桶满了大于阀值,则resize进行扩容

5.谈一下hashMap中get是如何实现的?

对key的hashCode进行hashing得到hash值【hashcode^(hashcode>>>16)】,与运算计算下标获取bucket位置【hash&(capa-1)】,如果在桶的首位上就可以找到就直接返回,如果没有找到则利用equals方法去树中找或者链表中查找节点。

8.为什么是16?为什么必须是2的幂?如果输入值不是2的幂比如10会怎么样?

blog.csdn.net/sidihuo/art…

blog.csdn.net/eaphyy/arti…

1.为了数据的均匀分布减少哈希碰撞存取高效,因为用位运算确定数据下标位置若数据不是2的次幂则会增加哈希碰撞的次数和浪费数组空间。(PS:其实若不考虑效率,求余也可以就不用位运算了也不用长度必需为2的幂次)。

2.输入数据若不是2的幂,HashMap通过一通位移运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字。

9.谈一下当两个对象的hashCode相等时会怎么样?

获取对象:HashCode相同,通过equals比较内容获取值对象

放入对象:会产生哈希碰撞,若key值相同则替换旧值,不然链接到链表后面,链表长度超过阙值8就转为红黑树存储

7.为什么不直接将key作为哈希值而是与高16位做异或运算?

因为数组位置的确定用的是与运算,仅仅最后四位有效,设计者将key的哈希值与哈希值的高16为做异或运算,使得在做&运算确定数组的插入位置时,低位实际是高位与低位的结合增加了随机性,减少了哈希碰撞的次数

HashMap默认初始化长度为16,并且每次自动扩展或者是手动初始化容量时,必须是2的幂

HashMap和HashTable的区别

  • HashMap允许Key-value为null(hash值为0),hashTable不允许(报错);
  • hashMap线程不安全。hashTable线程安全,方法有synchronized修饰,效率低;
  • HashMap初始容量是16,扩容时为"原始容量x2",下标为二次哈希hash&(n-1) ;Hashtable初始容量是11,扩容时为"原始容量x2 + 1",下标为key的hashCode()%n
  • HashMap继承于AbstractMap类,hashTable继承与Dictionary类。

ConcurrentHashMap

jdk1.7:使用分段锁技术,使用Segment数组,其中Segment继承于 ReentrantLock,segment元素包含一段Entry数组,不同segment的数据段不会存在锁竞争,提高并发访问率。

  • put操作,找到数组下标后会获取锁,获取锁失败会自旋获取锁,自旋到一定次数会阻塞
  • get操作valuevolatile 修饰,不需要加锁,get高效

jdk1.8:使用数组+链表+红黑树的结构,没有使用Segment,使用synchronized+CAS降低锁粒度,控制并发,ReentrantLock会锁住一定范围的Entry数组synchronized只锁住一个位置的数组元素

  • put操作,找到数组下标,为null则利用CAS写入,不为null则使用synchronized同步代码块写入,若链表数量大于阈值,则转换为红黑树。(synchronized在jdk1.6已经引入了锁升级策略
  • get操作,引入红黑树后查找效率更高。