【面试题】Java集合面试题整理


1.讲一下集合是什么/怎么理解集合的

集合就是一个放数据的容器,准确的说是放数据对象引用的容器。
集合类存放的都是对象的引用,而不是对象的本身。
集合类型主要有 3 种: set(集)、 list(列表)和 map(映射) 。
一类是实现Collection接口;另一类是实现Map接口。

2.set、list、map的区别?

  • List
    (1)可以允许重复的对象;
    (2)可以插入多个null元素;
    (3)是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序;
  • Set
    (1)不允许重复的对象;
    (2)无序容器,你无法保证每个元素的存储顺序。
    (3)只允许一个null元素;
    (4)HashSet是基于HashMap实现的
  • Map
    (1)Map不是collection的子接口或者实现类,Map是一个接口;
    (2)HashMap底层是数据+链表的组成,是无序的,通过hashCode()方法计算索引值后再存储或查找元素;链表长度大于8时转换成数组+红黑树结构;
    (3)Map的每个Entry都持有俩个对象,一个键一个值,可能会持有相同的值对象但键对象必须是唯一的(key相同会覆盖之前相同的key值);
    (4)Map里你可以拥有任意个null的value值,但只能有一个null的key键;

3.集合的底层数据结构

  • List
    • ArrayList Object[]数组
    • Vector Object[]数组
    • LinkedList 双向链表(jdk1.6之前是循环链表,1.7取消了循环)
  • Set
    • HashSet(无序、唯一) 底层采用HashMap来保存元素
    • TreeSet(有序、唯一) 底层采用红黑树(自平衡的二叉排序树)实现
  • Queue
    • ArrayQueue Object[]数组 + 双指针
    • PriorityQueue Object[]数组来实现二叉堆
  • Map
  • HashMap: jdk1.8之前由数组+链表组成,数组是HashMap的主体,而链表主要是为了解决哈希冲突。jdk1.8之后引入了红黑树。当链表长度大于等于8且数组长度大于等于64,链表就会转化为红黑树。如果只满足一个条件,那么会优先选择数组扩容。
  • TreeMap: 红黑树(自平衡的二叉排序树),底层是基于TreeSet实现的。
  • HashTable: 数组+链表组成,数组是HashTable的主题,链表则是主要是为了解决哈希冲突而存在的。

4.如何选取集合结构?

  • 如果我们需要根据键值来获取元素值,就可以选用Map接口下的集合。
  • 需要排序就选用TreeMap,不需要排序就选用HashMap,需要保证线程安全就选用ConcurrentHashMap;
  • 如果我们只需要存放元素值,就选择实现Collection接口的集合。
  • 需要保证元素唯一时选择Set接口下的集合,比如TreeSet或者是HashSet,不需要就选用List接口下的集合,比如ArrayList或者LinkedList。

5.说一下阻塞队列的实现原理

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:

  • 在队列为空时,获取元素的线程会等待队列变为非空。
  • 当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
在这里插入图片描述


6.List

ArrayList扩容

在 Java 中,ArrayList是一个动态数组,它可以根据需要自动增长。当 ArrayList 中的元素数量超过其初始容量时,它会重新分配一个更大的内部数组,然后将现有元素复制到新数组中。这个过程称为扩容。

list和Arrays排序方法

Collection.sort是对list进行排序,Arrays.sort是对数组进行排序。

数组和List相互转换

List.toArray();
Arrays.asList(new Integer[]{1,2,3,4,5})

Arraylist和Linkedlist相互转换

//方法一
ArrayList arrayList = new ArrayList();
LinkedList linkedList = new LinkedList(arrayList);

LinkedList linkedList = new LinkedList(); 
ArrayList<String> arrayList = new ArrayList(linkedList); 
//方法二
ArrayList<String> arrayList = Arrays.asList(str);
LinkedList<String> list = arrayList.stream().collect(Collectors.toCollection(LinkedList::new));

Arraylist和Linkedlist相互转换

  • ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
  • 随机访问List(get和set操作)时,ArrayList比LinkedList的效率更高,
    相对于ArrayList,LinkedList的插入,添加,删除操作速度更快。因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。
  • LinkedList比ArrayList更占内存,因为LinkedList每一个节点存储了两个指向上下的引用

Arraylist和数组的区别

  • ArrayList是动态扩容的,数组无法进行动态扩容。
  • 迭代方式不同,数组只能通过循环,通过元素的索引下标来达到一一遍历的目的,而 ArrayList 不仅可以使用循环来遍历,还可以通过Iterator来进行遍历
  • 数组中的元素可以是基本数据类型或者是引用类型,而 ArrayList 只支持引用类型,ArrayList 中对于基础数据类型的储存先要将其自动装箱成对应的包装类,取出的时候也是相同的,自动拆箱成基础数据类型。
  • 数组在构建的时候需要声明容纳元素的类型,而 ArrayList 则不需要声明容纳元素的类型

7.Map

Hashset和Hashmap区别

  • HashSet 集合不允许存储相同的元素, 它底层实际上使用 HashMap 来存储元素的, 数据添加到key, 所有value元素默认为static final修饰的Object类对象。
  • HashMap实现了Map接口,HashSet实现了Set接口
  • HashMap储存键值对,HashSet存储对象
  • 使用put()方法将元素放入map中,使用add()方法将元素放入set中

HashSet如何检查重复?

当把对象加入到HashSet中时,HashSet会先计算对象的hashCode值来判断对象加入的下标位置,同时也会与其他的对象的hashCode进行比较。

  • 如果没有相同的,就直接插入数据;
  • 如果有相同的,就进一步使用equals来进行比较对象是否相同,如果相同,就不会加入成功。

HashMap的内部数据结构

Hashmap7是数组+链表,Hashmap8是数组+链表+红黑树(链表大于8时)

为什么HashMap中链表转红黑树的阀值是8?

因为泊松分布的原则(超过8的话的概率非常非常小),且必须是2的幂次方的要求,所取得的最合适的值。

HashMap中put方法过程

先对key求hash值,如果没有碰撞,直接放入哈希桶中,如果碰撞了(哈希冲突),以链表的方式链接到后面。
如果链表长度超过阀值8,就把链表转成红黑树,如果节点已经存在就替换旧值

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

  1. 为了数据的均匀分布,减少哈希碰撞
    因为确定数组位置是用的位运算,若数据不是2的次幂则会增加哈希碰撞的次数和浪费数组空间。(PS:其实若不考虑效率,求余也可以就不用位运算了也不用长度必需为2的幂次)
  2. 手动容量时输入数据若不是2的幂,HashMap通位移运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字。

Hashmap怎么扩容?

当向容器添加元素的时候会判断 当前元素个数 > 数组的长度乘以加载因子(默认0.75)的值的时候,就要自动扩容为原先的两倍。

为什么Hashmap扩容后是原先的两倍?

因为HashMap的初始容量是2的n次幂,扩容也是2倍的形式进行扩容,会减少哈希碰撞,避免形成链表的结构。

Hashmap存在什么安全问题,怎么解决

  • 因为HashMap是线程不安全,所以在多线程的情况下会导致数据的覆盖。
    同时1.8之前的话扩容可能会发生死循环。
  • 可以使用ConcurrentHashMap

HashMap多线程下操作导致死循环问题

主要原因是并发下的Rehash会造成元素之间形成一个循环链表。多线程下HashMap会存在数据丢失的问题,并发环境下推荐使用ConcurrentHashMap

ConcurrentHashMap 的实现原理是什么?

  • JDK1.7 中的 ConcurrentHashMap 是由 Segment 数组结构(外层)和 HashEntry 数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成(HashEntry内部相当于HashMap里面的Entry对象)。
    Segment继承了ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。Segment 默认为 16,也就是并发度为 16。

  • JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+(链表长度大于8时)红黑树结构;
    在锁的实现上,抛弃了原有的 Segment 分段锁,采用 Synchronized来控制对桶的操作,同时结合了 CAS(Compare And Swap)和 Node 数据结构来实现并发操作。
    将锁的级别控制在了更细粒度的哈希桶数组元素级别,只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。而且是只有put和remove才会上锁,get是不需要上锁

  • 1.8实现原理:会先进行cas操作,如果失败,会判断是不是有其他线程在扩容,如果还不是的话会使用synchronized 锁插入元素

ConcurrentHashMap和Hashtable

  • ConcurrentHashMap 的效率要高于 Hashtable。
    因为 Hashtable 给整个哈希表加了一把大锁从而实现线程安全。
    而ConcurrentHashMap 的锁粒度更低,在 JDK1.7 中采用分段锁实现线程安全,在 JDK1.8 中采用CAS+synchronized实现线程安全。

JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock?

因为在 JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态。
假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。只有链表的头节点(红黑树的根节点)需要同步

JDK1.7 与 JDK1.8 中ConcurrentHashMap 的区别?

  • 数据结构:JDK1.8取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
  • 保证线程安全机制:
    JDK1.7 采用 Segment 的分段锁机制实现线程安全,其中Segment 继承自 ReentrantLock 。
    JDK1.8 采用CAS+synchronized保证线程安全。
  • 锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8 调整为对每个数组元素加锁(Node)。

ConcurrentHashMap 的 get 方法是否要加锁,为什么?

get 方法不需要加锁。因为 Node 的元素 value 和指针 next 是用volatile修饰的,在多线程环境下线程A修改节点的 value 或者新增节点的时候是对线程B可见的。


  • 57
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值