java集合面试题
- 一、集合容器概述
- 二、Collection接口
- 1、List接口
- 1. 迭代器 Iterator 是什么?
- 2. Iterator 怎么使用?有什么特点?
- 3. 如何边遍历边移除 Collection 中的元素?
- 4. Iterator 和 ListIterator 有什么区别?
- 5. 遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List遍历的最佳实践是什么?
- 6. 说一下 ArrayList 的优缺点
- 7. 如何实现数组和 List 之间的转换?
- 8. ArrayList 和 LinkedList 的区别是什么?
- 9. ArrayList 和 Vector 的区别是什么?
- 10. 插入数据时,ArrayList、LinkedList、Vector谁速度较快
- 11. 多线程场景下如何使用 ArrayList?
- 2、Set接口
- 三、Map接口
- 四、并发集合
一、集合容器概述
1. 什么是集合
- 集合是存放数据的容器,准确的说是存放数据对象引用的容器
- 集合类存放的都是对象的引用,而不是对象的本身
- 集合主要分为:Collection和Map两个接口
2. 集合和数组的区别
- 数组是固定长度的;集合的长度是可变的。
- 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
- 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。
3. 常用的集合类有哪些?
Map接口和Collection接口是所有集合框架的父接口:
- Collection接口的子接口包括:Set接口和List接口
- Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
- List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
- Map接口的实现类主要有:HashMap、TreeMap、Hashtable等
4. List,Set,Map三者的区别?
- List:一个有序容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有ArrayList、LinkedList 和 Vector。
- Set:一个无序容器,不可以存储重复元素,只允许存入一个null元素,须保证元素唯一性。Set 接口常用实现类有HashSet、LinkedHashSet 以及TreeSet。
- Map是一个key-value集合,存储键与值之间的映射。Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap
5. 集合框架底层数据结构
-
Collection
- List
- Arraylist: Object数组
- Vector: Object数组
- LinkedList: 双向循环链表
- Set
- HashSet:基于 HashMap 实现的,底层采用 HashMap 来保存元素
- LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过LinkedHashMap 来实现的。
- TreeSet: 红黑树(自平衡的排序二叉树。)
-
Map
- HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。JDK1.8以后当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
- LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
- Hashtable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
- TreeMap: 红黑树(自平衡的排序二叉树)
6. 哪些集合类是线程安全的?
- Vector:方法上添加synchronized关键字以保证线程安全,但因为效率较低,已不建议使用。
- Hashtable:方法上添加synchronized关键字以保证线程安全,但因为效率较低,已不建议使用。
- ConcurrentHashMap:是Java5中支持高并发、高吞吐量的线程安全HashMap实现。
7. Java集合的快速失败机制 “fail-fast”?
- fail-fast是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。
- 例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出ConcurrentModificationException 异常,从而产生fail-fast机制。
- 原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
- 解决办法:使用synchronized关键字或使用CopyOnWriteArrayList来替换ArrayList
8. 怎么确保一个集合不能被修改?
可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出java.lang.UnsupportedOperationException 异常。
二、Collection接口
1、List接口
1. 迭代器 Iterator 是什么?
- Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。
- 所有Collection接继承了Iterator迭代器
2. Iterator 怎么使用?有什么特点?
List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
String obj = it. next();
System. out. println(obj);
}
Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。
3. 如何边遍历边移除 Collection 中的元素?
边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
*// do something*
it.remove();
}
4. Iterator 和 ListIterator 有什么区别?
- Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
- Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
- ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
5. 遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List遍历的最佳实践是什么?
- 遍历方式有以下几种:
-
for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
-
迭代器(Iterator)遍历。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
-
foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。
-
6. 说一下 ArrayList 的优缺点
- ArrayList的优点如下:
- ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了RandomAccess 接口,因此查找的时候非常快。
- ArrayList 在顺序添加一个元素的时候非常方便。
- ArrayList 的缺点如下:
- 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
- 插入元素的时候,也需要做一次元素复制操作,缺点同上。
- ArrayList 比较适合顺序添加、随机访问的场景。
7. 如何实现数组和 List 之间的转换?
- 数组转 List:使用 Arrays. asList(array) 进行转换。
- List 转数组:使用 List 自带的 toArray() 方法。
8. ArrayList 和 LinkedList 的区别是什么?
-
底层数据结构:ArrayList采用动态数组的数据结构实现,而LinkedList是采用双向链表的数据结构实现。
-
随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
-
增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为ArrayList 增删操作要影响数组内的其他数据的下标。
-
内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
-
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。
9. ArrayList 和 Vector 的区别是什么?
这两个类都实现了 List 接口,他们都是有序集合
- 线程安全:Vector 使用了Synchronized 来实现线程同步,是线程安全的,而 ArrayList是非线程安全的。
- 性能:ArrayList 在性能方面要优于 Vector。
- 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
10. 插入数据时,ArrayList、LinkedList、Vector谁速度较快
-
ArrayList和Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。
-
Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较ArrayList差。
-
LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 LinkedList 插入速度较快。
11. 多线程场景下如何使用 ArrayList?
ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (String s : synchronizedList) {
System.out.println(s);
}
2、Set接口
1. HashSet 的实现原理?
HashSet 是基于HashMap 实现的,HashSet的值存放z于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层HashMap 的相关方法来完成,HashSet 不允许重复的值。
2. HashSet如何检查重复?HashSet是如何保证数据不可重复的?
- HashSet 中的add ()方法会使用HashMap 的put()方法。
- HashMap 的 key 是唯一的。 HashSet 添加进去的值就是作为 HashMap 的key,并且在HashMap中如果Key相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复(HashMap 比较key是否相等是先比较hashcode 再比较equals )。
3. HashSet与HashMap的区别
HashMap | HashSet |
---|---|
实现了Map接口 | 实现Set接口 |
存储键值对 | 仅存储对象 |
调用put()向map中添加元素 | 调用add()方法向Set中添加元素 |
HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false |
HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 | HashSet较HashMap来说比较慢 |
三、Map接口
1、前提知识
1. 什么是Hash算法
哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。
2. 什么是链表
链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查等功能。
链表大致分为单链表和双向链表
- 单链表:每个节点包含两部分,一部分存放数据变量的data,另一部分是指向下一节点的next指针
- 双向链表:除了包含单链表的部分,还增加的pre前一个节点的指针
3. 链表的优点
- 插入删除速度快,因为有next指针指向其下一个节点,通过改变指针的指向可以方便的增加删除元素
- 内存利用率高,不会浪费内存,可以使用内存中细小的不连续空间,并且在需要空间的时候才创建空间。大小没有固定,拓展很灵活。
4. 链表的缺点
不能随机查找,必须从第一个开始遍历,查找效率低
2、HashMap
1. HashMap是什么
HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
2. HashMap的数据结构
在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。HashMap 基于 Hash 算法实现的
3. HashMap在JDK1.7和JDK1.8中有哪些不同?
-
存储方式
JDK1.7采用的是数组+链表的形式,而JDK1.8在数组容量大于64且链表长度大于8的情况下会使用红黑树。 -
初始化方式
在JDK1.7中,table数组默认值为EMPTY_TABLE,在添加元素的时候判断table是否为EMPTY_TABLE来调用inflateTable。在构造HashMap实例的时候默认threshold阈值等于初始容量。当构造方法的参数为Map时,调用inflateTable(threshold)方法对table数组容量进行设置。而在JDK1.8中初始化的过程则是直接集成到了resize()函数中
-
扰动函数(获取hashCode的方法)
//JDK1.7 final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
//JDK1.8 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
-
链表插入数据的方式
在1.7中使用的是头插法,1.8改成了尾插法,为了避免头插法导致的在多线程的情况下hashmap在put元素时产生的环形链表的问题,但1.8仍然存在数据覆盖的问题,所以仍然不是线程安全的 -
扩容后存储位置的计算方式
JDK1.7中借助transfer()方法(jdk1.8中已移除),在扩容的时候会重新计算threshold,数组长度并进行rehash,这样的效率是偏低的而JDK1.8中通过高位的值进行判断存放的位置,当高位是0时,新索引的位置就是原索引的位置,当高位是1时,新索引位置为原索引位置+旧数组容量
3. HashMap的put()方法
4. HashMap的resize()方法
5.HashMap的get()方法
5. 使用HashMap需要注意的地方
HashMap可以使用任意对象(不可以使用基本数据类型)作为key,但使用时需要注意必须重写hashCode和equals方法。这样可以减少hash碰撞的几率,同时推荐使用String、Integer等不可变类作为key进行使用
6. HashMap与 Hashtable的区别
-
线程安全
HashMap 是非线程安全的,Hashtable是线程安全的。Hashtable内部的方法基本都经过 synchronized 修饰。 -
效率:
因为线程安全的问题,HashMap 要比 Hashtable效率高一点 -
Key的值
HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。 -
初始容量大小和扩充容量大小
在创建时如果不指定容量初始值,Hashtable默认的初始大小为11,之后每次扩充容量变为原来的2n+1。HashMap默认的初始化大小为16,之后每次扩充容量变为原来的2倍。
在创建时如果给定了容量初始值,那么Hashtable会直接使用指定大小,而HashMap会将其扩充为2的幂次方大小 -
底层数据结构
jdk1.8中HashMap采用数组+链表+红黑树的结构,而Hashtable采用的仍是数组+链表的结构