集合类面试
1.数组和集合的区别
数组:
- 数组是相同类型数据的有序集合,通过下标对数组元素进行访问。
- 数组可以存储任意类型的数据,包括基本类型和引用类型。
- 数组一旦创建,大小就不会改变。
集合:
- 集合是用于存储数据对象引用的容器。主要包括list、set、map。
- 集合只能存储引用类型数据对象,存储的对象可以是不同数据类型。
- 集合长度是可变的。
2.集合继承关系图
3.Iterator接口
Iterator接口,这是一个用于遍历集合中元素的接口,主要包含hashNext(),next(),remove()三种方法。它的一个子接口LinkedIterator在它的基础上又添加了三种方法,分别是add(),previous(),hasPrevious()。也就是说如果是先Iterator接口,那么在遍历集合中元素的时候,只能往后遍历,被遍历后的元素不会在遍历到,通常无序集合实现的都是这个接口,比如HashSet,HashMap;而那些元素有序的集合,实现的一般都是LinkedIterator接口,实现这个接口的集合可以双向遍历,既可以通过next()访问下一个元素,又可以通过previous()访问前一个元素,比如ArrayList。
4.常用的集合类
- Map接口和Collection接口是所有集合框架的父接口:
2Collection接口的子接口包括:Set接口和List接口
Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
5.集合的快速失败机制 “fail-fast”
-
概述:"fail-fast"是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
-
例子:假设存在两个线程(线程A、线程B),线程A通过Iterator在遍历集合list中的元素,在某个时候线程B修改了集合list的结构(比如调用了remove方法),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
-
原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
-
解决办法:1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
2.使用CopyOnWriteArrayList来替换ArrayList
6.集合的扩容机制
7.集合的遍历方式
- List的四种遍历方式
List<String> list = Arrays.asList("qwe","asd","zxc");
//1. for-in迭代
for(String s:list){
System.out.println(s);
}
//2. 经典循环迭代
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
//3. 迭代器迭代
Iterator<String> it = list.iterator();
while (it.hasNext()){
System.out.println(it.next());
}
//4. jdk1.8+ Lambda表达式循环迭代
list.forEach(s -> System.out.println(s));
- map的四种遍历方式
Map<String,String> map = new HashMap<>();
map.put("a","11");
map.put("b","22");
map.put("c","33");
// 1. 通过keySet的方式
for(String key:map.keySet()){
String value = map.get(key);
System.out.println("key:"+key+" vlaue:"+value);
}
// 2. 通过迭代器的方式
// import java.util.Map.Entry;
Iterator<Entry<String, String>> it = map.entrySet().iterator();
while(it.hasNext()){
Entry<String, String> entry = it.next();
System.out.println("key:"+entry.getKey()+" key:"+entry.getValue());
}
// 3.通过Map.entrySet遍历,尤其是容量大时,推荐
for (Entry<String, String> m : map.entrySet()) {
System.out.println("key:" + m.getKey() + " value:" + m.getValue());
}
// 4.通过Map.values()遍历所有的value,但不能遍历key,只需要获取value时使用
for(String m:map.values()){
System.out.println(m);
}
8 集合与数组之间的转换方式
// 数组转 List:使用 Arrays. asList(array) 进行转换。
Integer[] array = new Integer[]{1,2,3,4};
List<Integer> list = Arrays.asList(array);
// List 转数组:使用 List 自带的 toArray() 方法。
Integer[] arr = (Integer[]) list.toArray();
9 多线程场景下如何使用 ArrayList
ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。
List<String> list = new ArrayList<>();
list.add("qwe");
list.add("asd");
list = Collections.synchronizedList(list);
list.add("ert");
System.out.println(list.toString());
10 HashMap的实现原理
- HashMap对象在实例化后,底层创建了一个长度为16的一维数组 Entry[ ] table
- 执行put(key,value)添加数据,首先利用key的hashCode方法计算key的哈希值,
此哈希值经过底层算法的计算后得出当前对象的元素在数组中的下标。 - 拿到数组下标后,有三种情况。
- 数组下标数据为空,则key-value添加成功;
- 数组下标数据不为空,则比较key的哈希值,哈希值相同,调用key的equals方法,比较key的值,如果key和已存在的key值相同则覆盖原来的value,如果key和原来的key值不同则将当前的key-value放入链表中;
- 数组下标数据不为空,哈希值不相同,则将当前的key-value放入链表中
- 在不断添加数据的过程中,会涉及到数组的扩容问题。当添加数据超出临界值
(且数据要存放的位置非空)时,需要对数组扩容。默认的扩容方式:扩容为原来容量的2倍,并将原来的数据复制过来。 - 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)
11.HashMap JDK1.7 与 JDK1.8比较
不同 | JDK 1.7 | JDK 1.8 |
---|---|---|
存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
初始化方式 | 单独函数:inflateTable() | 直接集成到了扩容函数resize()中 |
hash值计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算 |
存放数据的规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树 |
插入数据方式 | 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) | 尾插法(直接插入到链表尾部/红黑树) |
扩容后存储位置的计算方式 | 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量) |
12.HashMap为什么需要扩容
为了解决hash冲突导致的链化影响查询的效率,扩容会缓解该问题
13. 为什么底层用数组+链表+红黑树进行存储
- 数组是用来确定桶的位置,利用元素的key的hash值对数组长度取模得到.
- 链表是用来解决hash冲突问题,当出现hash值一样的情形,就在数组上的对应位置形成一条链表。
- 红黑树是用来当提升查询查找和插入效率,链表时间复杂度为 O(logN),链表查找效率O(N)
14. 为什么不一开始就用红黑树
红黑树需要进行左旋,右旋,变色来保持平衡,而单链表不需要。
当元素小于8个当时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于8个的时候,此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。