前言:因为最近要重新找工作,Collection(集合)是面试中出现频率非常高的基础考察点,所以好好恶补了一番。
复习过程中深感之前的学习不系统,而且不能再像刚毕业那样死背面试题,例如:String是固定长度的,StringBuffer和StringBuilder的长度是可以变化的。如果一旦问得深入一点,问为什么有这样的区别就傻眼了,只能一脸呆萌地看着面试官。
因此想要通过写文章的形式,系统地总结学习的内容,例如Collection架构是怎样的、有哪些相关的继承和接口实现,这样才能了解什么时候应该用哪个类,以及类之间要如何搭配合作,才知道出了问题应该如何解决。这一系列文章适用于Java技术岗的应聘者、高校计算机专业的学生以及培训机构学习Java的初学者阅读。
1.1 认识Collection架构
我们都使用过ArrayList类收集对象,例如add()方法新增对象,remove()方法移除对象,这些都不会陌生。但是这些方法是怎么来的呢?下图是该类的继承架构图:
ArrayList一个类就这么复杂,如果要把全部Collection架构表现在一张图上,那估计就跟蜘蛛网一样纠缠不清。简化一下,忽略一些不那么重要的接口和实现类,我们可以得到以下这张架构图。
从图上可知,Colletion是一个接口,实现了另一个接口Iterable。Collection下面有三个接口直接实现了它,分别是List、Set和Queue。List下面有两个实现类,分别是ArrayList和LinkedList;Set的常用实现类是TreeSet和HashSet;Queue下面有Deque接口实现,再下面是实现类ArrayDeque.
这张图将会是这一系列文章的核心,之后会反复提及,不妨称之为Collection架构图,每一篇文章都是介绍其中的一部分。熟悉这张图,不仅有助于理解学习,还可以帮助记忆。至于详细周全的继承关系和实现架构,到底有哪些类实现了哪些接口、继承了哪些类,可以自行在API说明文档中查询。
1.2 具有索引的List
List实现了Collection接口,所以我们可以说List是一种Colletion,作用就是收集对象,特点是以索引的方式记录所收集的对象顺序。List中常见的实现类是刚才所提及架构图中的ArrayList,忘了的读者可以翻到前面对照着看。
1 /**
2 * ArrayList的实验用例
3 */
4
5 import java.util.*;
6
7 public class Student {
8 public static void main(String[] args) {
9 List list = new ArrayList(); //使用JavaSE的List和ArrayList
10 Scanner scanner = new Scanner(System.in);
11 String name;
12 while(true) {
13 System.out.print("学生签到:");
14 name = scanner.nextLine();
15 if(name.equals("quit")) {
16 break;
17 }
18 list.add(name); //实用Add()方法收集对象
19 }
20 System.out.println("今天上来上课的学生名单:");
21 foreach(list);
22 }
23
24 private static void foreach(List list) {
25 for(int i = 0; i < list.size(); i++) {
26 String student = (String) list.get(i); //使用get()方法依据索引取得收集的对象
27 System.out.println(student);
28 }
29 }
30 }
以上是ArrayList类的一个简单使用例子,模拟的是学生上课签到的情景。强烈建议读者跟我一样自己试着写一个简单用例,尤其是之前很少使用ArrayList的初学者,单纯的看和读跟实际敲代码产生的效果完全不一样。也可以照着我给出的例子敲,偷懒一点的话可以直接复制在机器上跑一遍。
从Collection架构图中可知,LinkedList同样也实现了List接口。就算只是把上面那个实验中的ArrayList全部改为LinkedList,程序照样可以运作,而且效果看起来完全相同。那么问题来了,我们什么时候应该使用ArrayList,什么时候又应该使用LinkedList呢?
1.2.1 ArrayList的特性
卡车和轮船都可以运送货物,我们可以根据不同的情况选择不同的运输方式。如果时间紧、运输量小,而且两个地点都在陆地上(例如北京到南京),那么我们可以使用汽车;如果时间多、运输量大,出发地和目的之间隔着海洋(例如大连到纽约),那么用船运是更好的选择。
刚毕业那会要找工作,为了面试背过“ArrayList像数组,读取速度快,但是需要调整索引的话表现很差;LinkedList像链表,调整索引的表现非常好,但是随机读取的速度比较慢”。那么我们可以问深一句,为什么会这样呢?不妨从源代码中找寻答案。
1 public boolean add(E e) {
2 ensureCapacityInternal(size + 1); // Increments modCount!!
3 elementData[size++] = e;
4 return true;
5 }
上面这一段是JavaSE的源代码,我们可以看到ArrayList中的add()方法非常简单,跟我们平时使用数组一样。查看源代码中更多内容你会发现,ArrayList内部就是使用Object数组来保存所收集的对象,这就是为什么说“ArrayList就像数组”的原因。在考虑是否使用ArrayList的时候,我们可以相当于考虑是否要使用数组的特性。
1.2.2 LinkedList的特性
在学习Collection架构的时候,我们不妨可以多看源代码,看的时候优先比较几个基本方法的实现,例如add()、remove()等。从这些方法的实现,我们就可以看到不同实现类的特性。
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* Links e as last element.
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
看到LinkdedList.add()的源代码,我们会发现其实现方式跟链表的实现如出一辙。如果last结点为null,那么说明链表为空,所以新添加的结点为头结点。如果last结点不等于null,那么把新添加的结点设为last的下一个结点,作为新的尾结点。
根据链表的特性,我们可以很快总结两点点特性:1.想要指定索引随机存取时,链接方式都得使用从第一个元素开始查找下一个元素的方式,效率比较糟糕;2.链接的每个元素都会参考下一个元素,这有利于调整索引顺序。
1.2.3 List总结
作为Collection三大阵营之一的List,最大的特点就是索引,我们可以通过索引做到随机存取。
List中常用的实现有ArrayList和LinkedList,各自的特性可以分别参考数组和链表。在比较它们之间区别的过程中,我们看了源代码,提倡在比较同一接口不同实现类时重点查看它们共同需要实现的方法,例如Collection中规定的add(),remove()等。
面试中常见的List实现类其实还有Vector,其特性与ArrayList相同。不同在于Vector具有线程安全的特性,性能开销比较大,具体的内容会放在以后关于多线程的文章里。
在总结过程中,给初学者提供两个建议,一是做实验,通过写一些demo来熟悉所学内容;二是在力所能及的情况下多看源代码,知其然也要知其所以然。
如果你喜欢我的文章,可以扫描关注我的个人公众号“李文业的思考笔记”。
不定期地会推送我的原创思考文章。