Java面试—集合篇

1、有哪些常见的集合?

image.png

  • Collection是集合List、Set的父接口
    • List:存储的元素有序,可重复
    • Set:不存的元素无序,不可重复
    • Queue:按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的
  • Map是另外的接口,是键值对映射结构的集合

2、集合框架底层数据结构有哪些?

List

  • ArrayList:Object[] 数组
  • Vector:Object[]数组
  • LinkedList:双向链表

Set

  • HashSet(无序、唯一):基于HashMap实现的,底层使用HashMap来保存元素
  • LinkedHashSet:LinkedHashSet是HashSet的子类,并且其内部都是通过LinkedHashMap来实现的
  • TreeSet(有序、唯一):红黑树

Queue

  • PriorityQueue:Object[] 数组来实现二叉堆
  • ArrayQueue:Object[] 数组+双指针

Map

  • HashMap:JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。Jdk1.8之后当链表长度大于阈值8时,数组大于64时,将链表转换为红黑树
  • LinkedHashMap:LinkedHashMap继承自HashMap,所以它的底层基于拉链式散列结构即由数组和链表或红黑树组成。此外在其基础上,增加了一条双向列表,使得上面的结构可以保持键值对的插入顺序,通过对链表的进行相应的操作,实现了访问顺序相关逻辑。java.util包下的
  • HashTable:数组+链表组成的,数组是HashTable主体,链表是为了解决哈希冲突而存在的
  • TreeMap:红黑树(自平衡的排序二叉树)

3、快速失败(fail-fast)和安全失败(fail-safe)了解吗?

  • 快速失败:是Java集合中等的一种错误检测机制,当用迭代器遍历一个集合对象时,如果线程A遍历过程中,线程B对集合对象的内容进行了修改,则会抛出Concurrent Modification Exception,原理是在遍历过程中使用一个modCount变量,集合在被遍历期间如果内容发生变化,就会改变modCount,每当迭代下一个元素的时候,就会检测modCount是否为expectionCount的值,是的话就遍历,不是的话就抛出异常。java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改。
  • 安全失败:采用安全失败的机制的集合容器,在遍历时不是直接在集合内容上遍历,而是复制原先的集合内容,在拷贝的集合上进行遍历,原理是迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合的修改并不能被迭代器检测到,就不会触发Concurrent Modification Exception,java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。

4、ArrayList,LinkedList ,Vector的区别,包括存储结构,增删改查效率等。

ArrayList LinkedList Vector
数据结构 数组 双向列表 数组
查找 有利于 不利于 有利于
增删 不利于 有利于 不利于
扩容机制 1.5倍扩容 2倍扩容
线程安全 不安全 不安全 安全

5、HashMap的底层数据结构是什么?

  • JDK1.8之前的数据结构是数组+链表,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,数组默认大小是16
  • JDK1.8之后数据结构是数组+链表+红黑树,数组默认大小是16

image.png

  • 通数组是来存储元素的,链表是来解决冲突的,红黑树是用来提高查询效率
  • 数据元素通过映射关系,也就是散列函数,映射到通数组对应索引的位置
  • 如果发生冲突,从冲突的位置拉一个链表,插入冲突的元素
  • 如果链表长度>8并且数组>=64,链表转换为红黑树
  • 如果红黑树的结点<6,转换为链表

6、怎么解决HashMap的hash冲突?

解决hash冲突的方法:

  1. 开放定址法(线性探测法):从发生冲突的那个位置开始,按照一定次序从Hash表找到一个空闲的位置然后把发生冲突的元素放入到这个位置中去。
  2. 链式寻址法:把hash冲突的key,以单向链表来进行存储
  3. 再hash法:通过某个hash函数计算的key,存在冲突的时候,再用另一个hash函数去运算,直到没有冲突即可
  4. 建立公共溢出区:把Hash表分为基本表和溢出表两个部分,凡是存在冲突的元素,一律放到溢出表中去

HashMap在JDK1.8以后使用的是链式寻址法和红黑树来解决Hash冲突的问题,红黑树是为了优化Hash表的链表过长导致时间复杂度增加,增加查询效率。

7、HashMap是线程安全的吗?怎么解决?

HashMap不是线程安全的,会发生死循环和数据丢失的问题(死循环在jdk1.7中存在,在1.8中没有,但是数据丢失在1.7和1.8都存在)。

  • 多线程下扩容死循环:jdk1.7的HashMap使用头插法插入元素,在多线程的环境下,扩容的时候可能导致环形链表的出现,jdk1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题
  • 多线程put可能导致元素的丢失。多线程同时执行put操作,如果计算出来的索引位置是相同的,它会造成前一个key被后一个key覆盖,导致元素的丢失
  • put和get并发时,可能导致get为null,线程1执行put时候,因为元素个数超过threshold而导致rehash,线程2此时执行get,有可能导致这个问题

可以使用HashTable、Collections.synchronizedMap、ConcurrentHashMap实现线程安全的Map

  • HashTable是直接在操作方法上加synchronized关键字,锁住整个table数组,粒度比较大,效率较低
  • Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个synchronizedMap对象,内部定义一个对象锁,方法内通过对象锁实现
  • ConcurrentHashMap在1.7中使用分段锁,在jdk1.8中使用CAS+synchronized

8、Hash的扩容策略是什么?

       为了减少哈希冲突发生的概率,在当前HashMap的元素个数达到一个临界值的时候,就会触发扩容机制,把所有元素rehash之后再放到扩容后的容器中(1.7时是这样的),扩容因子默认是0.75。

      HashMap在jdk1.8中是基于数组+链表和红黑树实现的,但用于存放key值的桶数组的长度是固定的,由初始化参数确定,随着插入的数据增多和负载因子的作用下,需要扩容来存放更多的数据。在jdk1.8中做了优化,可以不用重新计算每一个元素的哈希值,因为HashMap的初始容量是 2的次幂,扩容之后的长度是原来的2倍,新的容量也是2的次幂,所以元素,要么在原来的位置,要么在原位置的再移动2的次幂

9、JDK1.8对HashMap主要做了哪些优化?为什么?

  1. 数据结构:数组+链表 变成了 数组+链表+红黑树

发生hash冲突,元素会存入链表,链表过长的转换为红黑树

  1. 链表的插入方式:从头插法改为尾插法

在多线程环境下可能会产生死循环

  1. 扩容rehash:1.7需要对原数组中的元素重新hash定位到新数组的位置,1.8不需要重新通过哈希函数计算位置,新的位置不变或索引+新增容量大小

提高扩容的效率,更快的扩容

  1. 扩容时机:1.7先判断是否要扩容,再插入;1.8先进行插入,插入完再判断是否需要扩容
  2. 散列函数:1.7做了四次移位和四次异或,1.8只做了一次

做4次的话,边际效用也不大,改为一次,提升效率

10、HashMap的put和get流程是什么?

put流程:

image.png

  1. 首先进行hash的扰动,获取一个新的hash值。
  2. 判断table是否为空或者长度为0,如果是就做扩容操作
  3. 根据哈希值计算下标,如果对应小标正好没有存放数据,则往下走即可,否则就是覆盖了
  4. 判断table[i]是否是树节点,是就往树中插入,否则就插入到链表中
  5. 如果链表中插入节点的时候,如果达到了树化的阈值,就要把链表转换为红黑树
  6. 最后所有元素处理完成后,判断是否超过阈值,超过则扩容

get流程:
image.png

  1. 使用扰动函数(降低哈希碰撞的概率),获取新的哈希值
  2. 计算数组下标,获取节点
  3. 当前节点和key匹配,直接返回
  4. 否则,当前节点是否为树节点,查找红黑树
  5. 否则,遍历链表查找

11、ConcurrentHashMap底层具体实现?1.7和1.8有什么不同?

1.8之前:
image.png
这里的ConcurrentHashMap是由SegmentHashEntry数组结构组成。

Segment继承了ReentrantLock,所以Segment是一种可重入锁,它里面的每一段配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

一个ConcurrentHashMap中包含了一个Segment数组,Segment的个数一旦初始化就不能改变。Segment数组的大小默认是16,最多支持16个线程并发。

1.8之后:
image.png
这里ConcurrentHashMap是由Node+CAS+synchronized组成

数据结构和HashMap1.8结构类似。链表长度超过了一定阈值的时候,就会转换为红黑树。

1.8版本中,锁粒度更细,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,不会影响其它Node的读写,效率提升。

1.7和1.8有什么不同呢?

  • 线程安全实现方式:jdk1.7采用Segment分段锁来保证安全,Segment是继承自ReentrantLock,jdk1.8放弃了Segment分段锁的设计,采用Node + CAS + synchronized保证线程安全,锁的粒度更细,synchronized只锁定当前链表或红黑二叉树的首节点。

  • Hash碰撞解决方法:jdk1.7采用拉链法,jdk1.8采用拉链法结合红黑树

  • 并发度:jdk1.7最大并发度是Segment的个数,默认是16,jdk1.8最大并发度是Node数组的大小,并发度更大。

12、Set 和 Map的区别,List 和 Set 的区别?

Set和Map

List Set Map
继承接口 Collection Collection
常见实现接口 ArrayList、LinkedList、Vector HashSet、LinkedHashSet、TreeSet HashMap、HashTable
常见方法 add()、remove()、size()… add()、remove()、size()… put()、get()、remove()、clear()、values()、size()…
是否可重复
线程安全 Vector安全 HashTable安全

猜你喜欢

转载自blog.csdn.net/weixin_52487106/article/details/130924997