首页 > 代码库 > Java集合(二):List列表
Java集合(二):List列表
在上一节中,介绍了Java集合的总体情况。从这节開始,将介绍详细的类。这里不单单介绍类的使用方法。还会试图从源代码的角度分析类的实现。这一节将介绍List接口及实现类。即列表中的链表LinkedList和数组列表ArrayList。
1 List接口及抽象类
List接口扩展自Collection接口,这个接口设计了一些适合列表操作的方法。List是一个有序集合。元素能够加入到容器中某个特定的位置。
使用javac编译List.java源代码后,能够使用javap反编译源代码获得接口的详细信息。例如以下是调用后的结果:
Compiled from "List.java" public interface java.util.List<E> extends java.util.Collection<E> { public abstract int size(); public abstract boolean isEmpty(); public abstract boolean contains(java.lang.Object); public abstract java.util.Iterator<E> iterator(); public abstract java.lang.Object[] toArray(); public abstract <T> T[] toArray(T[]); public abstract boolean add(E); public abstract boolean remove(java.lang.Object); public abstract boolean containsAll(java.util.Collection<?>); public abstract boolean addAll(java.util.Collection<? extends E>); public abstract boolean addAll(int, java.util.Collection<? extends E>); public abstract boolean removeAll(java.util.Collection<?>); public abstract boolean retainAll(java.util.Collection<?>); public void replaceAll(java.util.function.UnaryOperator<E>); public void sort(java.util.Comparator<? super E>); public abstract void clear(); public abstract boolean equals(java.lang.Object); public abstract int hashCode(); public abstract E get(int); public abstract E set(int, E); public abstract void add(int, E); public abstract E remove(int); public abstract int indexOf(java.lang.Object); public abstract int lastIndexOf(java.lang.Object); public abstract java.util.ListIterator<E> listIterator(); public abstract java.util.ListIterator<E> listIterator(int); public abstract java.util.List<E> subList(int, int); public java.util.Spliterator<E> spliterator(); }List接口提供了这些方法,大部分是Abstract的,但也有一部分不是,这部分方法是JDK 1.8 新增的default方法。比方sort方法。
List接口提供了随机訪问方法,比方get(int)方法,可是List并无论这些方法都某个特定的实现是否高效。
为了避免运行成本较高的随机訪问操作,Java SE 1.4 引入了一个标记接口RandomAccess。
这个接口没有不论什么方法,但能够用来检測一个特定的集合是否支持高效的随机訪问:
if(c instanceof RandomAccess) { use random access algorighm } else { use sequential access algorithm }ArrayList就实现了这个接口。
List接口中的例行方法在抽象类AbstractList中实现了,这样就不须要在详细的类中实现,比方isEmpty方法和contains方法等。
这些例行方法比較简单。含义也明显。对于随机訪问元素的类(比方ArrayList),优先继承这个抽象类。
在AbstractList抽象类中。有一个重要的域。叫modCount:
protected transient int modCount = 0;
这个域能够用来跟踪列表结构性改动的次数,什么是结构性改动呢?就是改变列表长度的改动,比方添加、删除等。
对于仅仅改动某个节点的值不算结构性改动。
这个域在后面的迭代器中很实用。
迭代器能够使用这个域来检測并发改动问题,这个问题会在LinkedList类中介绍。
抽象类AbstractSequentialList实现了List接口中的一些方法,对于顺序訪问元素的类(比方LinkedList),优先继承这个抽象类。
2 链表:LinkedList
链表是一个大家很熟悉的数据结构。
链表攻克了数组列表插入和删除元素效率太低的问题,链表的插入和删除就很高效。
链表将每一个对象存放在独立的节点中。
Java中的LinkedList链表,每一个节点除了有后序节点的引用外,另一个前序节点的引用,也就是说。LinkedList是一个双向链表。
LinkedList类有三个域,各自是大小、头结点和尾节点:
transient int size; transient Node<E> first; transient Node<E> last;
还有两个构造器,一个无參构造器和一个含參构造器:
public java.util.LinkedList(); public java.util.LinkedList(java.util.Collection<? extends E>);
当中无參构造器构造一个空的链表,含參构造器依据传进来的一个集合构造一个链表。
2.1 Node<E>内部类
LinkedList类中,定义了一个Node<E>内部类来表示一个节点。
这个类的定义例如以下:
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; } }这是一个静态内部类,也没有对外部的引用。这个类有三个域:值。前序节点的引用,后序节点的引用,也有一个构造方法。定义非常easy。
假设要创建一个Node节点,能够这样:
Node<E> node=new Node<>(pre,item,next);
当中,pre和next各自是前序节点和后序节点的引用。
2.2 链表操作的基本方法
既然是链表。就少不了链表节点的加入与删除。
在LinkedList类中,提供了六个主要的链表操作的方法。这些方法都对链表的结构进行改动,因此会改变AbstractList类中的modCount域,这六个方法例如以下:
private void linkFirst(E);//在链表头部加入给定值的节点作为头结点 void linkLast(E);//在链表尾部加入一个给定值的节点作为尾节点 void linkBefore(E, java.util.LinkedList$Node<E>);//在给定的节点前插入一个节点 private E unlinkFirst(java.util.LinkedList$Node<E>);//删除头结点。并返回头结点的值 private E unlinkLast(java.util.LinkedList$Node<E>);//删除尾节点,并返回尾节点的值 E unlink(java.util.LinkedList$Node<E>);//删除给定的节点这些方法都是私有的(或包内私有的)。因此能够称为工具方法,LinkedList类中的全部结构性改动操作都是基于这六个方法实现的。
这六个方法都是链表的基本操作。代码比較简单。只是给出实现能够看看源代码实现者的写法,对于自己编程还是有帮助的:
/** * Links e as first element. */ 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++; } /** * 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++; } /** * Inserts element e before non-null Node succ. */ void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; } /** * Unlinks non-null first node f. */ 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; } /** * Unlinks non-null last node l. */ private E unlinkLast(Node<E> l) { // assert l == last && l != null; final E element = l.item; final Node<E> prev = l.prev; l.item = null; l.prev = null; // help GC last = prev; if (prev == null) first = null; else prev.next = null; size--; modCount++; return element; } /** * Unlinks non-null node x. */ E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; }
2.3 列表迭代器:ListIterator接口
链表是一个有序集合。每一个对象的位置十分重要。LinkedList.add方法仅仅是将节点加到尾部,然而对于链表的操作还有非常大一部分须要将节点加入到链表中间。
因为迭代器是秒数集合中的位置的。所以这样的依赖位置的加入方法将由迭代器负责。
仅仅有对自然有序的集合使用迭代器加入元素才有意义。
比方,对于无序的集合set,在Iterator接口中就没有add方法。相反的,在集合类库中提供了ListIterator接口,当中就有add方法:
interface ListIterator<E> extends Iterator<E> { void add(E element); ... }与Collection接口中的add方法不同,这种方法不返回boolean类型的值。由于它假定加入操作总是改变链表。
另外。除了hasNext和next方法,ListIterator接口还提供了以下的两个方法:
E previous(); boolean hasPrevious();这两个方法用来反向遍历链表。previous也像next一样,返回越过的对象。
LinkedList类的listIterator方法返回一个迭代器对象:
ListIterator<String> iter=list.listIterator();在介绍接口时我们知道。不能实例化一个接口对象。但能够声明一个接口对象然后引用一个实现了该接口的类的实例。那么listIterator方法返回的就必定是一个类的实例,而这个类也必定实现了这个接口,问题是。这个类是什么?
这个类事实上是LinkedList的一个内部类,即ListItr:
Compiled from "LinkedList.java" class java.util.LinkedList$ListItr implements java.util.ListIterator<E> { private java.util.LinkedList$Node<E> lastReturned; private java.util.LinkedList$Node<E> next; private int nextIndex; private int expectedModCount; final java.util.LinkedList this$0; java.util.LinkedList$ListItr(java.util.LinkedList, int); public boolean hasNext(); public E next(); public boolean hasPrevious(); public E previous(); public int nextIndex(); public int previousIndex(); public void remove(); public void set(E); public void add(E); public void forEachRemaining(java.util.function.Consumer<? super E>); final void checkForComodification(); }上面也是使用javap反编译的结果。能够看到。这个内部类实现了ListIterator接口,并实现了这个接口的方法。
这正是理解迭代器的关键。我们知道,迭代器能够看做是一个位置。这个位置在两个节点的中间,也就是说,对于一个大小为n的链表,迭代器的位置有n+1个:
| a | b | ...| z |
在这个样例中。链表表示26个字母,迭代器的位置就有27个。
这里也是把迭代器形象化为光标。next方法就是光标移到下一个位置,饭后返回刚刚越过的元素,同理previous也是一样。仅仅只是是左移一个位置。然后返回刚刚越过的元素。以下是这两个方法的代码:
public E next() { checkForComodification(); if (!hasNext()) throw new NoSuchElementException(); lastReturned = next; next = next.next; nextIndex++; return lastReturned.item; } public E previous() { checkForComodification(); if (!hasPrevious()) throw new NoSuchElementException(); lastReturned = next = (next == null) ? last : next.prev; nextIndex--; return lastReturned.item; }这两个方法首先调用checkForComodifcation方法检查并发改动问题。前面说过,AbstractList的modCount记录了链表的改动次数,而每个迭代器都通过以下的字段维护一个独立的计数器:
private int expectedModCount = modCount;这个域初始化为类的modCount改动次数。而checkForComodification检查迭代器自己维护的计数器是否和类的modCount相等,假设不等。就会抛出一个ConcurrentModificationException。
并发改动检查通过后。会调用hasNext或hasPrevious方法检查是否有待訪问的元素。ListItr类有一个nextIndex域:
private int nextIndex;这个域维护迭代器的当前位置。当然,对于LinkedList来说,因为迭代器指向两个元素中间,所以能够同一时候产生两个索引:nextIndex方法返回下一次调用next方法时返回元素的整数索引。previousIndex返回下一次调用previous方法时返回元素的索引,这个索引比nextIndex小1。
hasNext和hasPrevious方法就是检查nextIndex和previousIndex是否在正确范围来确实是否有待訪问元素的。
ListItr类还有两个域:
private Node<E> lastReturned; private Node<E> next;lastReturned用来保存上次返回的节点,next就是迭代器位置的下一个元素。也能够看做光标的下一个元素(下一个元素总是光标的右面那个元素)。调用next方法后,光标右移一位。越过next域保存的节点,然后更新这两个域的值,即刚才的next变为lastReturned,next就是再下一个元素,然后nextIndex增1。
previous相对于next操作来说相当于光标左移一位,在更新lastReturned和next时,须要考虑next是否为null。假设next为null,说明在没运行previous时。迭代器在最后一个位置,所以运行previous后。next应该是链表的尾节点last。假设next不是null,那么next更新为next的前序节点。而lastReturned为光标刚越过的元素。即如今的next节点,这时,lastReturned和next节点指向同一个元素。
ListItr类有三个能够改动链表的方法:add、remove和set。当中add和remove会改变迭代器的位置。由于这两个方法改动了链表的结构;而set方法不会改动迭代器的位置。由于它不改动链表的结构。
这三个方法的代码例如以下:
public void remove() { checkForComodification(); if (lastReturned == null) throw new IllegalStateException(); Node<E> lastNext = lastReturned.next; unlink(lastReturned); if (next == lastReturned) next = lastNext; else nextIndex--; lastReturned = null; expectedModCount++; } public void set(E e) { if (lastReturned == null) throw new IllegalStateException(); checkForComodification(); lastReturned.item = e; } public void add(E e) { checkForComodification(); lastReturned = null; if (next == null) linkLast(e); else linkBefore(e, next); nextIndex++; expectedModCount++; }值得注意的是remove方法。
在每次调用remove方法后,都会将lastReturned置为null。也就是说。假设连续调用remove方法,第二次调用就会抛出一个IllegalStateException异常。
因此。remove操作必须跟在next或previous操作之后。
如今已经介绍了ListIterator接口的基本方法,能够从前后两个方向遍历链表中的元素,并能够加入、删除元素。
记住一点:链表的任何位置加入与删除节点的操作是ListIterator迭代器提供的,类本身的add方法仅仅能在结尾加入。
2.4 随机訪问
在Java类库中,还提供了很多理论上存在一定争议的方法。链表不支持高速随机訪问。假设要查看链表中的第n个元素,就必须从头開始。越过n-1个元素,没有捷径可走。鉴于这个原因。在程序须要採用整数索引訪问元素时。一般不选用链表。
虽然如此,LinkedList类还提供了一个用来訪问某个特定元素的get方法:
LinkedList<String> list=...; String s=list.get(n);当然,这种方法的效率不太高。
绝不应该使用这样的让人误解的随机訪问方法来遍历链表。以下的代码效率极低:
for(int i=0;i<list.size();i++) { dosomething with list.get(i); }
每次查找一个元素都要从头開始又一次搜索。LinkedList对象根本不做不论什么缓存位置信息的处理。
事实上。在LinkedList类中,get方法会推断当前的位置距离头和尾哪一端更近,然后推断从左向右遍历还是从右向左遍历。
2.5 样例
以下的代码演示了LinkedList类的基本操作。
它简单的创建两个链表,将它们合并在一起,然后从第二个链表中每间隔一个元素删除一个元素。最后測试removeAll方法:
import java.util.*; public class LinkedListTest { public static void main(String[] args) { List<String> a=new LinkedList<>(); a.add("A"); a.add("C"); a.add("E"); List<String> b=new LinkedList<>(); b.add("B"); b.add("D"); b.add("F"); b.add("G"); ListIterator<String> aIter=a.listIterator(); Iterator<String> bIter=b.iterator(); while(bIter.hasNext()){ if(aIter.hasNext())aIter.next(); aIter.add(bIter.next()); } System.out.println(a); bIter=b.iterator(); while(bIter.hasNext()){ bIter.next(); if(bIter.hasNext()){ bIter.next(); bIter.remove(); } } System.out.println(b); a.removeAll(b); System.out.println(a); } }结果例如以下:
3 数组列表:ArrayList
前面介绍了List接口和实现了这个接口的LinkedList类。List接口用于描写叙述一个有序集合,而且集合中每一个元素的位置十分重要。
有两种訪问元素的协议:一种是用迭代器。还有一种使用get和set方法随机訪问每一个元素。
后者不适用于链表,但对数组非常实用。集合类库提供了一个大家非常熟悉的ArrayList类,这个类也实现了List接口。ArrayList类封装了一个动态再分配的对象数组。
Java集合类库中另一个动态数组:Vector类。只是这个类的全部方法是同步的,能够由两个线程安全的訪问一个Vector对象。
可是,假设一个线程訪问Vector。代码要在同步上消耗大量的时间。
而ArrayList方法不是同步的,因此。假设不须要同步时使用ArrayList。
在ArrayList具体解释中具体介绍了类的实现及方法的使用。
Java集合(二):List列表