链表(List)是最为常用的一种数据结构。链表是一种有序容器(Ordered Collection),有时又被称为序列(sequence)。在链表中,元素之间首尾相连,组成了一条长长的元素链。为了向开发人员提供链表功能,Java标准库也提供了链表相关的接口和类。
java.util.List表示的是链表接口;它继承自java.util.Collection接口,所以List对象也是集合对象。List接口包含了所有链表操作的接口。
java.util.AbstractList是一个抽象的链表类。它提供了需要实现List接口的必要内容。如果开发人员需要实现一个随机访问的链表类的话,可以考虑继承自AbstractList类,因为它能帮助减少开发工作量。如果开发人员需要实现一个顺序访问的链表类的话,可以考虑继承自java.util.AbstractSequentialList类。AbstractSequentialList继承自AbstractList;在随机访问的基础上,AbstractSequentialList实现了顺序访问的一些必要功能。
Java标准库中包含的List相关类的结构如下图所示。
图一 List相关类结构图
List接口提供了add()和addAll()接口,用于向链表添加新元素。add(index, element)方法可以将新元素放置在指定的位置。例如:
List<Integer> aListOfInteger = new LinkedList<Integer>();
aListOfInteger.add(1); // 在链表尾部添加新元素1
aListOfInteger.add(0, 2); // 在链表的第一个位置,即头部,添加新元素2
List<Integer> secondList = new LinkedList<Integer>();
secondList.addAll(aListOfInteger); // 将aListOfInteger中的元素添加入secondList
remove()方法可以删除指定的元素或者删除指定位置的元素。clear()方法则删除链表中的所有元素。如果需要一次删除多个元素的话,可以使用removeAll()方法。
aListOfInteger.remove(0); // 删除链表中第一个元素
aListOfInteger.remove(Integer.valueOf(2)); // 删除链表中值为2的元素
aListOfInteger.clear(); // 删除链表中所有的元素
isEmpty()可以用于判断链表是否为空链表。size()方法返回链表中元素的个数。
List<Integer> aListOfInteger = new LinkedList<Integer>();
aListOfInteger.add(1); // 在链表尾部添加新元素1
aListOfInteger.add(2); // 在链表尾部添加新元素2
System.out.println(aListOfInteger.isEmpty()); // 打印false,当前链表不为空
System.out.println(aListOfInteger.size()); // 打印2,当前链表的元素个数
get()方法允许开发人员根据元素所在位置获取元素。iterator()方法返回一个Iterator对象,提供依次访问链表元素的接口。
System.out.println(aListOfInteger.get(0)); // 打印链表中第一个元素
// 打印链表中每一个元素
Iterator<Integer> it = aListOfInteger.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
listIterator()方法也提供了类似的功能。只不过,listIterator()方法可以接受一个参数,指定迭代器的起始位置。
// 从第二个元素开始遍历链表
ListIterator<Integer> it = aListOfInteger.listIterator(1);
while (it.hasNext()) {
System.out.println(it.next());
}
contains()和containsAll()方法在链表对象中查找一个或者多个元素。这两个方法仅返回容器是否包含给定的元素。如果需要查找并获取元素时,可使用indexOf()和lastIndexOf()方法。indexOf()方法从前向后查找给定的元素;lastIndexOf()方法从后向前查找指定的元素。这两个方法的返回值表示的是查找元素在链表中的位置。如果链表中不包含该元素的话,则返回-1。
List<Integer> aListOfInteger = new LinkedList<Integer>();
aListOfInteger.add(1); // 在链表尾部添加新元素1
aListOfInteger.add(2); // 在链表尾部添加新元素2
System.out.println(aListOfInteger.contains(1)); // 返回true,因为链表包含元素1
System.out.println(aListOfInteger.indexOf(1)); // 返回0,因为元素1所在的位置是0
List接口还提供了多个of()静态方法,用于创建不可变更的链表对象。例如,如下的代码示例创建了包含一个元素和两个元素的链表对象。
List<Integer> list1 = List.of(1);
List<Integer> list2 = List.of(1, 2);
List接口覆盖了equals()方法,定义了链表比较的逻辑。只有当两个链表包含相同个数的元素,并且在相同位置处的两个元素"相同"时,List类的equals()方法才返回true;否则返回false。两个元素相同是指在这两个元素上调用Objects.equals()方法的结果为true。
List<Integer> list1 = List.of(1, 2);
List<Integer> list2 = List.of(1, 2);
System.out.println(list1.equals(list2)); // 打印true
sort()方法能够将链表中的元素按照指定的顺序排序。List类的sort()方法的功能与java.util.Collections类的sort()方法的功能类似。
subList()方法可以从一个链表对象中创建出一个子链表。第一个参数和第二个参数设置了一个起始位置和终止位置。在这个区间内的元素将包含在子链表中(在终止位置的元素不包含在子链表中)。
toArray()方法可将链表的所有元素按照顺序导入到一个数组对象中。
List<Integer> aListOfInteger = new LinkedList<Integer>();
aListOfInteger.add(2); // 在链表尾部添加新元素2
aListOfInteger.add(1); // 在链表尾部添加新元素1
aListOfInteger.sort(); // 排序
List<Integer> subList = aListOfInteger.subList(0, 1);
Integer[] anArray = subList.toArray(Integer[]::new);
java.util.Collections类中也提供了一些链表的操作方法。rotate()方法可以循环移动链表中的元素。如果把链表首尾相连的话,rotate()方法能将链表中的元素向尾部平移。例如,当链表的元素为[1, 2, 3]时,在调用rotate(list, 1)之后,链表的元素会变为[3, 1, 2]。平移的距离也可以是负数。当平移距离是负数时,则向链首平移。例如,当链表为[1, 2, 3]时,在调用rotate(list, -1)之后,链表变为[2, 3, 1]。
Collections.swap()方法可以用于交换链表中的两个元素。例如,下面的代码交换链表的第一个和第二个元素。
List<Integer> aList = ...;
Collections.swap(aList, 0, 1);
Collections.shuffle()方法能够随机打乱链表中元素的次序。在使用完shuffle()方法之后,各种元素的排列的概率基本相同。
Collections.replaceAll()方法可将链表中给定值替换为另一个值。例如,下面的代码将链表中值为1的元素替换为值为2的元素。
List<Integer> aList = ...;
Collections.replaceAll(aList, 1, 2);
Collections.copy()方法将源链表中的元素拷贝到目标链表之中。目标链表的长度不能短于源链表。目标链表中超出源链表长度部分的元素保持不变。
我们将在下面介绍了两个链表实现ArrayList和LinkedList都不提供并发保护。如果希望将ArrayList或者LinkedList对象加上并发保护的话,可以使用Collections.synchronizedList()方法。
List<Integer> syncrhonizedArrayList = Collections.synchronizedList(new ArrayList<Integer>());
List<Integer> syncrhonizedLinkedList = Collections.synchronizedList(new LinkedList<Integer>());
Java标准库中提供了多种链表的实现。其中,ArrayList和LinkedList是最为常见的两种。我们将在本小节中介绍它们的实现细节。
ArrayList类使用了数组(Array)来实现链表。链表中的所有元素保存在一个数组中。当数组被填满时,这个数组能够自动増长。因为所有的元素都是存放在数组中的,所以,方法size()、isEmpty()、get()、set()、iterator()都可以在常量时间内完成。
我们再来看看OpenJDK 15如何实现ArrayList。ArrayList类放置在java.util包中。它包含了两个重要的成员变量。elementData是保存元素的数组;size表示当前链表包含的元素个数。因此,size()方法和isEmpty()方法可以直接使用成员变量size。
// OpenJDK 15
package java.util;
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...
transient Object[] elementData;
private int size;
...
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
...
}
图二 ArrayList内部结构图
因为所有的元素都包含在数组之中,所以get()和set()方法直接使用了数组的访问方法。checkIndex()方法检查参数index是否越界。
// OpenJDK 15
package java.util;
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...
E elementData(int index) {
return (E) elementData[index];
}
public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index);
}
public E set(int index, E element) {
Objects.checkIndex(index, size);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
...
}
当添加新元素时,首先判断数组elementData是否已填满。如果未填满,则直接放置在数组中。如果已填满,则申请一个更大的数组空间,将已有的元素全部拷贝到新的数组中,然后再填入新元素。在grow()方法中,ArrayList类创建了一个更大的数组,并使用了Arrays.copyOf()方法来将元素拷贝到新数组中。
// OpenJDK 15
package java.util;
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));
}
private Object[] grow() {
return grow(size + 1);
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
...
}
所以,当移除元素时,ArrayList也需要向前平移删除位置之后的元素。元素平移是调用System.arraycopy()方法完成的。
// OpenJDK 15
package java.util;
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...
public E remove(int index) {
Objects.checkIndex(index, size);
final Object[] es = elementData;
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
fastRemove(es, index);
return oldValue;
}
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
...
}
在查找元素时,沿着数组从前向后或者从后向前逐个比较,完成查找。
// OpenJDK 15
package java.util;
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...
public int indexOf(Object o) {
return indexOfRange(o, 0, size);
}
int indexOfRange(Object o, int start, int end) {
Object[] es = elementData;
if (o == null) {
for (int i = start; i < end; i++) {
if (es[i] == null) {
return i;
}
}
} else {
for (int i = start; i < end; i++) {
if (o.equals(es[i])) {
return i;
}
}
}
return -1;
}
...
}
另一个使用数组来实现链表接口的类是java.util.Vector。Vector的实现方式与ArrayList非常相似。但是Vector有着两个显著的区别。第一,开发人员可以为Vector设置自动增长的步长。步长保存在成员变量capacityIncrement中。第二,Vector为每个方法增加了并发保护。每个方法都被保护在synchronized之中。我们仅在下面列出Vector实现的不同之处。有兴趣的读者可以自行查看其实现的代码。
// OpenJDK 15
package java.util;
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
protected Object[] elementData;
protected int elementCount;
protected int capacityIncrement;
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
public synchronized int size() {
return elementCount;
}
public synchronized boolean add(E e) {
modCount++;
add(e, elementData, elementCount);
return true;
}
public boolean remove(Object o) {
return removeElement(o);
}
public synchronized void removeElementAt(int index) {
...
}
}
LinkedList是一个双向链表的实现。在LinkedList链表中,元素存放在节点上,节点之间首尾相连,形成链表。当需要使用索引来操作链表时,首先需要从表头或者表尾开始遍历链表,查找到索引指向的位置,然后再处理相应的操作。
在OpenJDK 15中,LinkedList类放置在java.util包中。LinkedList类包含了三个重要的成员变量;size表示链表的长度;first和last分别表示链表的头节点和尾节点。
// OpenJDK 15
package java.util;
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
...
}
图三 LinkedList内部结构图
每个节点是一个Node对象。Node是LinkedList类的内部类。Node类由三个成员组成,分别为item、next和prev。它们分别表示链表中的元素和指向前驱、后继节点的引用。
// OpenJDK 15
package java.util;
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
...
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
...
}
因为LinkedList类的first和last分别指向链首和链尾,所以,它能直接返回第一个元素和最后一个元素。
// OpenJDK 15
package java.util;
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
...
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
...
}
当从链首插入新元素时,新节点的next指向当前的链首,并且将first指向新节点。然后再更新链尾。我们省略了从链尾插入新元素的代码,读者如果感兴趣的话,可自行查看。
// OpenJDK 15
package java.util;
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
...
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
public void addFirst(E e) {
linkFirst(e);
}
...
}
当从链首删除元素时,首先将first指向第一个元素的next指向的元素,然后更新成员变量last。
// OpenJDK 15
package java.util;
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
...
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
...
}
当查找元素时,indexOf()方法从链首开始遍历链表,直到到达链尾或者找到目标元素。lastIndexOf()方法则从链尾开始向前查找。我们在这里省略了lastIndexOf()方法的实现。
// OpenJDK 15
package java.util;
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
...
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
...
}
本章介绍了Java标准库中的链表的接口与两个最常用的实现。ArrayList使用数组来实现链表;而LinkedList则是一个双向链表的实现。ArrayList的特点是空间使用率较高,每个元素紧紧的排列在一起,遍历每个元素的速度较快。但是,当需要动态增长时,ArrayList需要复制所有的元素到新的数组中。LinkedList的动态性更好。从链首或者链尾删除元素较快。但是,当遍历链表时,LinkedList只能沿着每个节点,一步一步的前进。Vector与ArrayList非常相似。Vector为开发人员提供了灵活的动态增长步长和并发保护。
注册用户登陆后可留言