本文主要对ArrayList和LinkedList中常用的方法结合源码进行了介绍,具体的内容如下图所示

概述
List是Java中非常重要的数据结构,最常见的主要是ArrayList和LinkedList,另外还有Vector,但是使用的不多

其实Vector和ArrayList基本上使用了相同的算法,唯一的区别就是Vector中进行了同步操作,因此支持多线程操作,是线程安全的,而ArrayList在多线程场景下则可能存在安全隐患,其他的基本操作都是一样的,因此下面的总结均以ArrayList为例
初始化
首先从初始化说起,为了能够更加方便的理解,所有的讲解均会附带JDK的源码,并且会把涉及到的部分抽取出来
ArrayList初始化
首先来看一下ArrayList的初始化

其中有几个比较重要的成员变量
DEFAULT_CAPAITY:链表默认容量
EMPTY_ELEMENTDATA:定义的内容为空静态常量
DEFAULTCAPACITY_EMPTY_ELEMENTDATA:定义的内容为空静态常量
elementData:实际存储元素的数组
size:链表的容量
如果使用默认的无参构造函数,会将实际储存元素的数组elementData初始化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。也可以在初始化的同时指定链表的容量,如果传入的是一个正整数,那么就按照传入的长度初始化elementData,如果传入的长度为0,这时候则会将elementData初始化为EMPTY_ELEMENTDATA。
其实根据构造函数可以看出,在使用默认函数进行初始化时,ArrayList被初始化为了一个空数组,而不是常说的默认长度为10
EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA看起来都是静态常量,并且都是一个空数组,那么为什么要定义两个不同的变量呢?这个问题在看完后面的内容就可以得到答案了。
LinkedList初始化
LinkedList使用了双向链表结构,在JDK1.6及之前是循环双向链表。需要注意这两者之间的区别
同样对构造函数进行抽取简化如下

其中中有几个比较成员变量
size:链表当前的容量
first:记录链表的头结点
last:记录链表的尾结点
包含两个构造函数,默认构造函数不进行任何操作,重载的构造函数可以接受一个Collection集合,其中首先调用无参构造函数,然后调用addAll()方法将所有元素添加到链表中
添加元素
ArrayList添加元素
接下来介绍添加元素方法,这又可以分为两种情况,一是将新元素追加到末尾,二是在指定的索引处添加元素,下面还是结合源码分别进行介绍
增加元素到列表末尾
首先来看一下在ArrayList中追加元素到末尾的代码

在向链表末尾追加元素时,调用的是add(E e)方法。在方法中,首先会判断当前链表的容量是否满足添加元素后的最小容量要求,这是通过调用ensureCapacityInternal(int minCapacity)来实现的。在该方法中又会调用calculateCapacity(Object[] elementData, int minCapacity)方法来确定所需要的最小容量。然后调用ensureExplicitCapacity(int minCapacity)来确保容量够用。如果所需的最小容量大于当前数组的长度,则调用grow(int minCapacity)对链表进行扩容,具体的扩容测量在后面也会进行详述。其中的modCount记录对链表元素操作的次数,是从父类继承下来的变量。
这时候需要注意其中的if判断,在if判断中,判断条件是elementData与DEFAULTCAPACITY_EMPTY_ELEMENTDATA是否相等,如果相等,则返回DEFAULT_CAPAITY与minCapacity中的最大值,如果不相等则返回minCapacity。
这时候就可以回答上面构造函数中的问题了,为什么需要定义两个内容一样的空数组了。
- 如果使用默认构造函数,
elementData就是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,那么在首次调用add()方法时,所需的最小容量是1,但是经过计算会直接将容量设置为DEFAULT_CAPAITY也就是10。这应该就是常说的ArrayList的初始容量是10的原因 - 但是,如果在创建
ArrayList时传入的自定义容量是0,虽然也将elementData初始化为空数组,但是在首次添加元素时,返回的最小容量就是1了
也就是,如果使用无参构造函数,添加一个元素之后,链表底层的数组容量为10,如果使用重载的构造函数传入初始化容量为0,那么添加一个元素后,底层的数组容量为1。
增加元素到指定位置
还是先来看源码

添加元素到到指定位置时,首先需要进行的就是判断传入的索引位置是否符合要求,这一步是通过rangeCheckForAdd(int index)来实现的,其实很简单,就是判断索引元素是否小于0或者是否超出了当前链表的长度,如果是则会跑出一个IndexOutOfBoundException。第二步也是判断容量是否满足要求,同上面的一样,就不再进行讲解。
再找到需要插入的具体位置之后,就调用System.arraycopy()复制数组,将待插入位置之后的元素全部向后移动一位(向后复制),然后将指定位置的元素重新设置为待插入元素
扩容策略
ArrayList中非常重要的一点就是扩容,毕竟它的底层是依靠数组来实现的,而数组的本身的容量是固定的,不断添加元素之后就需要扩容,那么ArrayList是怎么进行扩容的呢?
其实非常简单,就是调用grow(int minCapacity)函数就可以了

首先获取当前的链表长度oldCapacity,新的链表长度就是原来的1.5倍(oldCapacity + oldCapacity / 2,右移操作效率更高),接下来对比新链表长度是否大于所需的最小链表长度,如果还是不满足所需的最小容量,则直接将新容量设置为所需的最小容量。
如果扩容之后的容量大于MAX_ARRAY_SIZE(预先定义好的,具体值为Integer.MAX_VALUE - 8),则调用hugeCapacity,里面会判断是否溢出。
最后调用Arrays.copyOf()将原始数据复制到扩容后的数组并返回。
LinkedList添加元素
LinkedList添加元素也可以分为三种,分别是向末尾添加,向表头添加以及向指定位置添加但是由于LinkedList是通过链式连接的,因此不会涉及到扩容
添加元素到末尾
在LinkedList中,向末尾添加元素的方式有两种,一种是直接调用add(E e)方法,另一种是调用addLast(E e)方法,两个方法的作用是一样的,只不过是返回值不同。具体的可以看下面的源码

add(E e)和addLast(E e)都是通过调用linkLast(e)方法将新元素追加到末尾,只不过add(E e)会有一个bool类型的返回值,而addLast(E e)没有返回值
linkLast(E e)将新元素追加到当前链表的末尾。
在前面曾经说过,LinkedList是一个双向链表,从这里我们就能看到。LinkedList中的每一个链表元素其实都是一个LinkedList.Node对象,它是LinkedList的一个内部类。每一个对象都有一个前驱节点和一个后继节点。
具体来看linkLast(E e)。首先定义一个l指向当前链表的最后一个节点last,然后创建一个新的节点newNode,在LinkedList.Node的构造函数中可以看到,将新节点的prev节点设置为当前链表的尾结点,将next节点设置为null。
然后将last指向新创建的节点表示新的链表的尾结点。如果当前链表是空的则将first节点也只指向新节点,否则l.next = newNode将新节点与链表连接起来,最后将链表的长度和对链表的操作次数分别加1
添加元素到链表头
其实这个过程和添加到末尾基本一致,唯一的区别就是链表在连接的时候将新节点的prev设置为null,将next设置为原始链表的first节点

添加元素到指定位置
在LinkedList中也可以将链表添加到指定位置,首先进行的还是索引的合法性检查,然后将其添加到指定位置,并将原来位置上的元素连接其后

checkPositionIndex(int index)进行下标的合法性检查,如果是负数或者超出当前链表长度就会抛出异常
如果位置是链表长度index == size就直接调用linkLast()添加到链表末尾。这样说来将元素添加到末尾其实应该是有三种方法
linkBefore(E e, LinkedList.Node<E> succ)将新元素插入到指定位置元素之前,也就是将原来该位置的元素连接到新位置元素后
获取元素
ArrayList获取指定位置元素
在ArrayList中获取指定位置的索引非常简单,先判断传入的索引是否超出链表的长度范围,然后在调用底层的数组获取指定的元素并返回

LinkedList获取指定位置元素
在LinkedList中获取指定位置的元素要比ArrarList稍微麻烦一点,当然第一步还是免不了的检查索引的合法性,这里就不在重复讲解了

然后调用node(int index)获取具体的元素。在这个获取过程中,使用了一个小trick,就是会先判断索引和链表长度的一半的关系,如果小于链表长度的一半就从头开始遍历,否则就从后向前遍历
删除元素
ArrayList删除元素
在ArrayList中删除元素也分为两种,删除指定位置元素和删除指定内容的元素
ArrayList删除指定位置元素
删除指定位置元素的第一步还是索引值检查,然后获取位置的索引作为结果返回。
在返回之前会首先将索引位置之后的所有元素全部向前移动一位,然后将原始数组的最后一个元素设置为null以便垃圾回收

ArrayList删除指定元素
由于链表中的元素可以重复,因此根据内容删除元素的时候只会删除第一次出现的指定内容的元素。
用if进行区分的原因是因为null不能使用equals()方法,首先是获取与待删除元素相等的元素的索引,然后调用fastRemove(int index)删除

这一过程与上面的删除指定位置元素的过程基本一样,只是少了一个索引合法性判断而已。
LinkedList删除元素
与ArrayList一样,也是有两种删除
删除指定位置元素
第一步不用说,索引合法性检查

node(int index)方法找到指定索引位置的元素
然后使用unlink(LinkedList.Node<E> x)执行删除过程,LinkedList的删除就是将元素从链表中脱离,然后将其前后的链表连接。但是需要考虑特殊情况的处理,就是元素时链表头或者链表尾。然后将链表的长度自减,操作次数自增,并返回删除的元素
删除指定内容元素
也是删除第一次出现的元素,其他的也是和ArrayList基本一致,只不过是循环判断的方式不同。找到之后调用unlink(LinkedList.Node<E> x)执行删除过程

为什么for...each删除会报错
接下来讲一下在为什么使用for...each遍历过程中删除元素会报错
举个栗子
public class LinkedListTest {
public static void main(String[] args) {
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("你好");
linkedList.add("你好1");
linkedList.add("你好2");
for (String s : linkedList) {
if ("你好".equals(s)){
linkedList.remove(s);
}
}
}
}
这样调用会出现

我们可以看一下编译后的代码
public class LinkedListTest {
public static void main(String[] args) {
LinkedList<String> linkedList = new LinkedList();
linkedList.add("你好");
linkedList.add("你好1");
linkedList.add("你好2");
Iterator var2 = linkedList.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
if ("你好".equals(s)) {
linkedList.remove(s);
}
}
}
}
编译器自动为我们创建了Iterator,这样看起来应该不会有问题,那么为什么还会报错呢。这种方式和我们使用Iterator删除的唯一区别就是我们调用的是iterator.remove()。而且如果你把代码修改为删除的元素是你好2的话居然是不会报错的,这就很神奇了。
下面我们就来揭秘这个的原因,我们在调用iterator()时会获取到一个LinkedList.ListItr它是定义在LinkedList内的一个内部类,为了能够更加清晰的说明问题,就只列举了一些涉及到的方法和属性值

可以看上面的异常信息栈,就发生在checkForComodification()方法中,这里面抛出异常的可能原因就只有一个那就是modCount != expectedModCount,在上面我们说明modCount记录的是对链表操作的次数。expectedModCount是ListItr的一个属性,由于是内部类,所以在创建的时候赋值等于modCount
在调用next()以及remove()方法时都会调用checkForComodification(),并且再看一下上面的异常栈发现,异常是在next()方法时抛出的
在remove()方法中,在得到指定元素后,都会调用unlink()方法删除,我们在上面的unlink()中可以看到,在删除元素之后modCount会自增。
如果我们直接使用for...each循环删除元素,编译器虽然会调用iterator,但是删除元素还是调用LinkedList.remove()这时候modCount自增了,在下次调用next()方法时,还是会首先调用checkForComodification(),这时候modCount != expectedModCount就变成了True,异常就被抛出。但是如果是通过iterator.remove()的话,在调用unlink()之后,expectedModCount也会执行自增操作,这时候下次在调用next()方法时就不会出现异常了。这就是使用for...each删除元素出现异常崩溃的根本原因
遍历元素
ArrayList和LinkedList都有两种遍历方法,分别是for...each和iterator,在上一节中也知道了其实for...each也被编译器优化为了iterator所以就不在进行过多的介绍了,只需要牢记如果在遍历过程中要对链表进行删除就不能使用for...each只能使用iterator
其他方法
ArrayList的其他方法
主要是几个可能比较常用的方法
- contains():判断是否包含指定元素
- indexOf():获得指定元素的索引
- isEmpty():链表是否为空
- toArray():将链表转换为数组
其实都非常简单,不需要做太多的介绍,需要注意的一点其实就是contains()其实也是通过调用indexOf()方法实现的

LinkedList的其他方法
同样是上面四个方法

contains()、indexOf()和isEmpty()基本都和ArrayList一样,只有toArray()有所不同,在ArrayList中是通过调用Arrays.copyOf()实现的,而LinkedList则是自己创建一个数组直接赋值
异同点及使用场景
ArrayList和LinkedList的异同点
ArrayList和LinkedList都是链表,他们最大的区别就是底层的数据结构不同,ArrayList的底层是一个数组,而LinkedList则是链表,这也就导致了他们在不同的场景下具有不同的性能优势
使用场景
由于ArrayList是依靠数组实现的,因此在顺序访问时的效率非常高,但是如果插入删除元素就会耗费较大的性能,一方面是在插入元素时可能会频繁的扩容,然后导致需要重复的复制原来的数组到新的数组中,删除元素的话则必定会涉及到数组的复制
而LinkedList由于是通过索引连接的,因此在添加和删除时的效率非常高,但是如果需要通过索引来获取或者删除元素的话效率必然是不如ArrayList的
Arrays.asList()和ArrayList的区别
我们知道在Arrays类中有一个asList()方法会返回一个ArrayList对象,但是这个对象却并不能进行增删操作。其实这是因为这个ArrayList并不是我们上面说的那个,而是Arrays的一个内部类

这个Arrays.ArrayList对象与上面所说的ArrayList除了增删元素操作之外,其他的方法基本都是相同的