Java集合面试题总结

1. Java的集合有哪些?

  • 主要有两大类集合都继承自Iterable接口,分别是Collection与Map。
  • 其中Collection下又分为List和Set两个接口,List有序可以重复,Set无序不可重复;
  • List的具体实现类有:ArrayList,LinkedList,Vector;Set的具体实现类有:HashSet,TreeSet;
  • Map接口主要有三个实现类:HashMap,HashTable(线程安全),TreeMap

使不安全的集合线程安全:Collections.synchronizedList();

2. ArrayList是怎么扩容的?

如果不指定ArrayList的容量,默认初始化一个容量为10的数组,当插入第11个元素时就会触发扩容,创建一个长度为原来1.5倍的数组,并将原来的数据拷贝到新数组中,这就是扩容机制。

3. 说一下LinkedList和ArrayList

  • 线程安全:都是线程不安全;

  • 底层数据结构:ArrayList底层采用的是Object数组,LinkedList底层是双向链表;

  • 是否支持随机访问:ArrayList支持,LinkedList不支持。

  • 插入删除元素时:ArrayList采用数组存储,插入和删除元素的时间复杂度受位置影响,时间复杂度最坏为O(n),LinkedList插入或删除的时间复杂度仅为O(1)。

  • 当ArrayList存储的数据过多时,会进行扩容,而LinkeList不需要扩容。

4. 链表和哈希表的区别?

  • 链表是一种物理存储单元上非连续、非顺序的存储结构,链表由一个个节点组成,单向链表中每个节点存放值和下一个节点的地址;

  • 哈希表是根据key值直接访问的数据结构,通过关键码值映射到表中的一个位置来访问记录,以加快查找速度。

5. 说说HashMap的底层

  • JDK1.7之前HashMap底层数据结构是数组+链表,首先通过计算key值的hash值,然后对数组长度取模,判断当前元素应该存放的位置,如果该位置没有元素就直接存入,如果有元素,判断存入元素的hash值是否与该元素相等,相等就覆盖,不相等就采用拉链法解决冲突;

  • JDK1.8后,底层是数组+红黑树, 当拉链的链表长度大于8时,先判断数组长度,如果长度小于64,对数组扩容,如果大于64,就将链表转化为红黑树。

6. hashmap是否为线程安全的,为什么不安全?

线程不安全,在JDK1.7的时候会出现死循环、数据丢失的情况,JDK1.8会出现数据覆盖的情况。
(这几个问题出现的具体原因请参考别的博客,结合源码看才能记得清楚,下面只是我根据源码的简单总结)

  • 在1.7中,插入元素采用的是头插法,HashMap的线程不安全主要发生在扩容时,假设两个线程同时执行扩容,一个进入阻塞状态,另一个线程完成了扩容后,阻塞线程被唤醒,该线程还会继续执行扩容,此时扩容的过程中有可能出现一种情况,比如我们扩容后要插入的元素是A和B,他们原来的hash值就相等,且A指向B,迁移到新散列表的时候A先插入,根据头插法原理B插入后B会指向A,这样就出现死循环;

  • 1.8后摒弃了transfer函数,直接在resize函数中完成数据的迁移,使用的是尾插法,解决了死循环的问题,但是会出现数据覆盖问题,在put操作中有段代码是检查哈希碰撞,如果没有哈希碰撞就直接插入元素,假设此时有两个线程A和B都在执行put操作,并且通过hash函数计算的结果是相同的,线程A执行完检查哈希碰撞操作后因时间片耗尽被挂起,线程B得到时间片后在该处下标进行插入元素,然后线程A获得了时间片,他会直接进行插入操作,这样就把线程B刚插入的数据覆盖了。

7. HashMap扩容

JDK1.8后,底层是数组+红黑树, HashMap 默认的初始化大小为 16,之后每次扩充,容量变为原来的 2 倍。如果初始化时指定容量,HashMap 会将其扩充为 2 的幂次方大小。插入元素时,首先计算hash值……

4.8 为什么每次扩容是2的n次幂

因为为了让HashMap高效,必须减少碰撞,所以必须把数据均匀分布,Java中的hash值范围非常大,远远超过内存,虽然保证了难以出现碰撞,但是内存放不下。所以需要对这个散列值关于数组长度做取模运算,即hash%n,如果n为2的幂次方,那么hash%n=hash&(n-1),与操作的运算效率高于取模运算,所以每次扩容都是2的幂次方。

8. 为什么HashMap的加载因子是0.75?

HashMap的初始容量是16,所以当HashMap的数组长度达到一个临界值的时候才会触发扩容,这个临界值是16*0.75=12;至于0.75这个具体值的由来,这是从统计学中的泊松分布推出来的,取值0.75既可以是为了提高利用率和又可以减少查询成本。

9. HashMap的rehash过程

rehash发生在扩容时,将数组扩容后,对于原来数组中的每个元素,重新计算key的哈希值,然后通过(n-1)&hash判断当前元素存放的位置,如果没有元素就直接插入,如果存在元素用拉链法存入,当链表长度超过8且数组长度超过64时,链表转换成红黑树。

10. Hashmap 发生hash冲突怎么办?

遇到哈希冲突将冲突值首先判断对应的value是否相同,相同就覆盖,不同就加到对应的链表或者红黑树中。

11. 为什么用红黑树

红黑树是一种近似平衡树,虽然查找效率略低于平衡树,但其插入、删除、查找各种操作性能比较稳定。

12. 讲讲HashMap的get操作?

首先根据对象的Hash值进行数组方面的寻找,然后找到这个数组之后,判断key是不是唯一,如果key唯一,则直接返回,如果不唯一,则使用equals进行值的判断,最后返回数据。

13. 说一下hashmap和hashtable

  • 线程安全上:hashmap是线程不安全的,hashtable线程安全

  • 执行效率上:hashmap效率高于hashtable,现在hashtable已经被ConcurrentHashMap取代

  • 扩容机制上:如果创建时不指定容量,hashmap初始容量为16,每次扩容容量变为原来的2倍,hashtable初始容量为11,每次扩容为原来的2倍加1;如果创建时指定了容量初始值,HashMap会将宿主扩充为2的n次幂,而HashTable会使用指定大小。

  • 底层数据结构:JDK1.8后,HashMap底层机构是数组+红黑树,HashTable底层是数组+链表

14. 自定义一个类,需要存放在HashSet/HashMap中,需要注意什么?

需要重写equals方法,也要重写hashCode方法

15. ConcurrentHashMap和Hashtable区别

底层数据结构不同,实现线程安全的方式上不同。

底层:1.7时ConcurrentHashMap是分段数据+链表,1.8时是数据+链表(红黑树),HashTable在1.8之前是数组+链表的形式;

实现线程安全:对于ConcurrentHashMap:1.7的时候对整个桶数组进行分割分段,每把锁只锁住其中一段数据,多线程访问不同数据段的数据,这就不会存在锁竞争,保证了线程安全;在1.8的时候,抛弃了分段的概念,底层也发生了改变,并发控制采用synchronized和CAS操作,例如一个在put的过程中,计算到插入的位置然后只将该位置锁住,但是其他线程能在别的位置插入元素。

对于Hashtable:也使用synchronized来保证线程安全,但效率非常低,因为多个线程访问同步方法时可能会进入阻塞或者轮询状态,例如put元素时,另一个线程不能put也不能get。

在这里插入图片描述

16. ConcurrentHashMap的put过程

  • 1.7的时候

ConcurrentHashMap是由一个Segment数组和多个HashEntry数组组成,其实就是将HashMap分为多个小HashMap,每个Segment元素维护一个小HashMap,目的是锁分离,给每个Segment进行加锁。当⼀个线程占⽤锁访问其中⼀个段数据时,其他段的数据也能被其他线程访问 。

对于插入操作,需要两次Hash映射去定位数据存储位置。首先通过第一次hash过程,定位Segment位置,然后通过第二次hash过程定位HashEntry位置。

因为Segment继承ReentrantLock,所以在数据插入指定HashEntry过程的时候,会尝试调用ReentrantLock的tryLock方法获取锁,如果获取成功就直接插入相应位置,无法获取,当前线程就会以自旋方式去继续调用tryLock方法去获取锁,超过指定次数就挂起,等待唤醒。

在这里插入图片描述

  • 1.8的时候

底层是数组加红黑树,最一开始得检查key或者value是否有空值,如果有就抛出异常,然后根据key计算hash值,初始化数组,计算插入的数组中的位置。

判断该位置有没有元素,没有元素的话,那么就用CAS的方式添加元素,当然有可能失败,有可能其他线程已经抢占先添加,这个过程在一个死循环里,失败了再从头来一次,插入成功后返回;

如果此时对应位置有元素了,那么判断插入位置的第一元素的hash值是否为MOVED,如果是说明正在扩容,那么此线程就会帮助扩容。如果没有在扩容,那么就对第一个元素synchronized上锁,上锁之后还要对桶内第一个元素判断是否发生了改变,发生变化了就再从头再来,如果插入位置的链表长度超过阈值就转换成红黑树。

最后还要判断是否需要进行扩容。

我对再次判断桶内第一个元素的理解:如果在加锁的时候,此时有可能其他线程删除了第一个元素,那么就产生了错误。经历了重重困难终于能插入数据了,此时就和HashMap有点像了,如果第一元素是链表节点,那么遍历链表查询是否key相等,若查到把value更改为我们要put的value值,没找到的话新建节点插入到链表尾部。若第一个节点是树节点,那么就以树的方式插入。

17. 如何计算ConcurrentHashMap的size?

size是调用sumCount函数获取的元素数量,元素数量 = baseCount+ counterCells各个元素值

baseCount是一个volatile类型的变量,用来记录元素的个数,当插入新数据put()或则删除数据remove()时,会通过addCount()方法更新baseCount。

counterCells是个全局的变量,表示的是CounterCell类数组。CounterCell是ConcurrentHashmap的内部类,它就是存储一个值。初始化时counterCells为空,在并发量很高时,如果存在两个线程同时执行CAS修改baseCount值,则失败的线程会使用 counterCells记录元素个数的变化。

所以需要baseCount加上counterCells中所有元素的值,就得到了size。

(继续执行方法体中的逻辑,执行fullAddCount(x, uncontended)方法,这个方法其实就是初始化counterCells,并将x的值插入到counterCell类中,而x值一般也就是1或-1,这可以从put()方法中得知。)

注意:
ConcurrentHashMap如果设置了初始容量,那就是大于该容量的2的最小次幂,比如new ConcurrentHashMap(32),默认容量是64

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值