集合(List、Set)
一、典型面试例题及思路分析
问题 1:“ArrayList 和 LinkedList 的的相同点和不同点分别是什么?”
相同点:ArrayList 和 LinkedList 都是 List 接口的实现类,因此都具有 List 的特点,即存取有序,可重复;而且都不是线程安全的。
不同点:ArrayList 基于数组实现,LinkedList 基于双向链表实现。
点评:
这是一个典型的考察集合异同点的面试题。同类面试题还有很多,比如说
数组 (Array) 和列表 (ArrayList) 的差别是什么?
ArrayList 和 Vector 的差别是什么?
…
这类题目考察的仍然是基础,同时还有候选人的知识深度。就参考答案而言,不同点部分抓住了二者最大的不同,即内部数据结构的差异,而相同点部分则从三个方面进行了展开:
“都是 List 接口的实现类”,体现侯选人对 JDK 源码是比较熟悉的;
“因此都具有 List 的特点,即存取有序,可重复”,是在 1 的基础上对 List 的具体阐述;
“都不是线程安全的”,体现的是线程安全方面的考虑。
第 2 点和第 3 点单独拎出来进行说明,是因为其对应的正是另两类关于 List 的高频题,详情分别参见本章节的问题 2 和问题 3,先不展开。这里基于不同点还可以继续追问:
“既然 ArrayList 和 LinkedList 的内部数据结构不一样,那分别适用于什么场景呢?”
不同的内部数据结构适应于不同的应用场景,这点无庸置疑。
ArrayList 基于数组存储数据,因此查询元素时可以直接按照数据下标进行索引,而插入元素时,通常涉及到数据元素的复制和移动,所以查询数据快而插入数据慢;
LinkedList 基于双向链表存储数据,因此查询元素时需要前向或后向遍历,而插入数据时只需要修改本元素的前后项即可,所以查询数据慢而插入数据快。
所以,ArrayList 适合查询多(读多)的场景,LinkedList 适合插入多(写多)的场景。
问题 2:"List、Set、Map 之间的区别是什么?"
List 是有序集合,可以有重复元素;
Set 集合不能包括重复元素,实现类中 LinkedHashSet 按照插入顺序排序,SortedSet 可排序,HashSet 无序;
Map 存放键值对 (key-value pairs) 映射,映射关系可以是一对一或多对一,key 无序且唯一,value 可重复。实现类中 LinkedHashMap 按照插入顺序排序,SortedMap 可排序,HashMap 无序。
点评:
本题属于自由发挥题,主要考察候选人两方面的能力:一是要真正熟悉对应的知识点,二是要有较强的总结和表述能力。如果自己心里明白但表述不清楚,于面试官而言也等同于你不明白。比如说基于参考答案之外,候选人也可以针对这些集合的异同添加自己的理解。
回到题目本身,首先,List 与 Set 具有相似性,都继承共同的 Collection 接口,也都是单列元素的集合。List 的内部是数组,所以不断在数组后面追加元素即可,这是它为什么有序的原因;而 Set 里面不允许有重复的元素,这里的重复是指两个相等 (注意不是相同) 的对象 ,即 equals () 返回 true。如果 Set 集合 s 中有 A 元素,现在再向 s 集合插入 B 元素,此时 B 元素如果与 A 元素相等,则 B 元素存储不进去(add 方法返回 false)。
其次,Map 与 List 和 Set 不同,它是双列的集合,值得注意的是并不继承 Collection。
问题 3:"ArrayList 和 LinkedList 都不是线程安全的,那有线程安全的 List 类吗?"
线程安全 List 类有 Vector 和 CopyOnWriteList。
Vector 是通过在其几乎所有方法前加 synchronized 关键字来保证线程安全性;
CopyOnWriteList 则是通过数组复制的方法来保证线程安全的。
点评:
Vector 和 Collections.synchronizedList (new ArrayList ()) 类似,都是通过 synchronized 来保证集合的安全性。只不过 Vector 的 synchronized 关键字加在方法外面(图 1),Collections.synchronizedList (new ArrayList ()) 的 synchronized 关键字加在方法里面(图 2)。
图 1 Vector 中的方法
图 2 Collections.synchronizedList 中的方法
CopyOnWriteList 是通过缩小锁有范围和数组复制来实现线程安全,看读(add 方法)、写(get 方法)部分的源码:
/**
* 添加元素
**/
public boolean add(E e) {
// 1、加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 2、获取原数组及长度
Object[] elements = getArray();
int len = elements.length;
// 3、复制到新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 4、添加元素到新数组
newElements[len] = e;
setArray(newElements);
return true;
} finally {
// 5、释放锁
lock.unlock();
}
}
/**
* 读取元素
**/
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
//CopyOnWriteList的成员属性,通过private transient volatile来修饰
return array;
}
可以看到写的时候加锁了而读的时候没有加锁。这是因为 CopyOnWriteList 在读的时候读的原数组,而原数组通过 volatile 修饰保证了可见性。这在迭代器上表现得更加明显。
/**重写List的迭代器实现*/
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
... ...
private COWIterator(Object[] elements, int initialCursor) {
//COWIterator的构造方法,cursor和snapshot是迭代器的成员属性
cursor = initialCursor;
snapshot = elements;
}
/**
* 迭代器中hasNext()和next()方法都是基于snapshot数组,而snapshot是传递进来的array原数组
*/
public boolean hasNext() {
return cursor < snapshot.length;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
二、总结
总体而言,List/Set 类的面试题的特点和 Map 类有点类似(深度上可能不及 Map 类),都是属于基础知识的部分,这类试题的答题要点在于:
基础知识的回答要表达清晰,知识准确。
厚积薄发,平常多问问为什么,注意学习源码。
面试技巧:回答时可以结合着源码、或者自己的工程实践展开,向面试官传递出自己热爱技术、勤于思考的形象。
三、扩展阅读
数组(Array)和列表(ArrayList)的差别是什么?
Array可以容纳基本类型和对象,而ArrayList只能容纳对象;
Array 是静态的,一旦创建就无法更改它的大小,ArrayList 是Java集合框架类的一员,可以称它为一个动态数组。
问:ArrayList和Vector有何异同点?
相同点:
(1)两者都是基于索引的,内部由一个数组支持。
(2)两者维护插入的顺序,我们可以根据插入顺序来获取元素。
(3)ArrayList和Vector的迭代器实现都是fail-fast的。
(4)ArrayList和Vector两者允许null值,也可以使用索引值对元素进行随机访问。
不同点:
(1)Vector是同步的,而ArrayList不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用CopyOnWriteArrayList。
(2)ArrayList比Vector快,它因为有同步,不会过载。
(3)ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。
问:EnumSet是什么?
java.util.EnumSet是使用枚举类型的集合实现。当集合创建时,枚举集合中的所有元素必须来自单个指定的枚举类型,可以是显示的或隐示的。EnumSet是不同步的,不允许值为null的元素。
问:Java集合类中的Iterator和ListIterator的区别?
iterator()方法在set和list接口中都有定义,但是ListIterator()仅存在于list接口中(或实现类中);
ListIterator有add()方法,可以向List中添加对象,而Iterator不能;
ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以;
ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能;
都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改;