链表(LinkedList)
数组(array)和数组列表(ArrayList)都有一个重大的缺陷:
从数组的中间位置删除一个元素要付出很大的代价,因为数组中在被删除元素之后的所有元素都要向数组的前端移动一个位置(最坏的情况是:删除数组的第一个元素)。在数组中间的某个位置插入一个元素也是类似的后果(最坏的情况是:在数组的头部插入一个元素)。
Java中链表的实现方案,实现了Iterator接口的LinkedList集合类如何遍历和删除元素呢?不需要像c语言那样使用复杂的指针,非常简单,代码如下:
List<String> staff = new LinkedList<>();
staff.add("Amy");
staff.add("Jim");
staff.add("slepin");
Iterator<String> it = staff.iterator();
String firstEmployee = it.next();
String secondEmployee = it.next();// skip past second element
it.remove();// remove last visited element,the second element
for (String e : staff) {
System.out.println(e);
}
链表是一个有序集合(ordered collection),每个对象的位置十分重要。LinkedList自身的add()方法会把对象添加到链表的尾部。但是,实际的应用场景中常常需要将元素添加到链表的中间,而迭代器正好是用来描述集合中元素的位置的,所以这种依赖于位置的add()方法将由迭代器负责实现。
注意,只有对自然有序的集合使用迭代器添加元素才有实际意义,因为如果是无序集合,元素堆积成一盘散沙,在任何一个位置添加元素,尾部、中间或者头部,效果都一样。尽管所有集合类都实现了Iterator接口,但Iterator接口中没有add()方法,道理很简单,因为不是所有的集合类都是有序的,相反,类库中大部分集合类都是无序的。
但是,这样一来,那些需要依赖迭代器来添加元素的有序集合类该怎么办呢?不要紧,集合类库提供了Iterator接口的子接口ListIterator,它里面就包含了add()方法。我们看看源代码的注释是怎么解释的:
/**
* Inserts the specified element into the list (optional operation).
* The element is inserted immediately before the element that
* would be returned by {@link #next}, if any, and after the element
* that would be returned by {@link #previous}, if any. (If the
* list contains no elements, the new element becomes the sole element
* on the list.) The new element is inserted before the implicit
* cursor: a subsequent call to {@code next} would be unaffected, and a
* subsequent call to {@code previous} would return the new element.
* (This call increases by one the value that would be returned by a
* call to {@code nextIndex} or {@code previousIndex}.)
*
* @param e the element to insert
* @throws UnsupportedOperationException if the {@code add} method is
* not supported by this list iterator
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this list
* @throws IllegalArgumentException if some aspect of this element
* prevents it from being added to this list
*/
void add(E e);
翻译成中文就是:
插入指定的元素到列表中(可选的操作),这个元素立即被插入到将会被next()方法返回的元素的前面,如果有的话;或者立即插入到将会被previous()方法返回的元素的后面,如果有的话。(如果这个列表不包含元素,这个新的元素会成为这个列表里唯一仅有的元素。)这个新元素被插入到隐式游标的前面:一个后来的对next()方法的调用不会被影响,一个后来的对previous()方法的调用将会返回这个新元素。(这个调用会使得调用nextIndex()或previousIndex()方法返回的值增加1)
参数 待插入的元素
抛“未支持操作”异常,如果add()方法不被列表迭代器支持。
抛“类型转换”异常,如果指定的元素的类型阻止它被添加到这个列表。
抛“非法参数”异常,如果这个元素的某些方面阻止它被添加到这个列表。
另外通过仔细地研究类库中LinkedList的源码,我们就会发现,LinkedList并不是直接就实现ListItrator接口,而是让自己的内部类ListItr去实现,这样,每次LinkedList对象调用listIterator()方法后返回的其实是它的内部类ListItr的一个实例,每个实例都会维护一个自己的预期的修改的计数值expectedModCount,在自己的add(),set(),remove()等方法中会检查自己的预期修改计数值expectedModCount是否与集合LinkedList的修改计数值modCount一致。如果不一致,就抛出一个ConcurrentModificationException异常。
下面贴出ListItr的源码:
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;
private Node<E> next;
private int nextIndex;
private int expectedModCount = modCount;
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
public boolean hasPrevious() {
return nextIndex > 0;
}
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex - 1;
}
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++;
}
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (modCount == expectedModCount && nextIndex < size) {
action.accept(next.item);
lastReturned = next;
next = next.next;
nextIndex++;
}
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
LinkedList类的作者对它的描述
* Doubly-linked list implementation of the {@code List} and {@code Deque}
* interfaces. Implements all optional list operations, and permits all
* elements (including {@code null}).
*
* <p>All of the operations perform as could be expected for a doubly-linked
* list. Operations that index into the list will traverse the list from
* the beginning or the end, whichever is closer to the specified index.
*
* <p><strong>Note that this implementation is not synchronized.</strong>
* If multiple threads access a linked list concurrently, and at least
* one of the threads modifies the list structurally, it <i>must</i> be
* synchronized externally. (A structural modification is any operation
* that adds or deletes one or more elements; merely setting the value of
* an element is not a structural modification.) This is typically
* accomplished by synchronizing on some object that naturally
* encapsulates the list.
*
* If no such object exists, the list should be "wrapped" using the
* {@link Collections#synchronizedList Collections.synchronizedList}
* method. This is best done at creation time, to prevent accidental
* unsynchronized access to the list:<pre>
* List list = Collections.synchronizedList(new LinkedList(...));</pre>
*
* <p>The iterators returned by this class's {@code iterator} and
* {@code listIterator} methods are <i>fail-fast</i>: if the list is
* structurally modified at any time after the iterator is created, in
* any way except through the Iterator's own {@code remove} or
* {@code add} methods, the iterator will throw a {@link
* ConcurrentModificationException}. Thus, in the face of concurrent
* modification, the iterator fails quickly and cleanly, rather than
* risking arbitrary, non-deterministic behavior at an undetermined
* time in the future.
*
* <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
* as it is, generally speaking, impossible to make any hard guarantees in the
* presence of unsynchronized concurrent modification. Fail-fast iterators
* throw {@code ConcurrentModificationException} on a best-effort basis.
* Therefore, it would be wrong to write a program that depended on this
* exception for its correctness: <i>the fail-fast behavior of iterators
* should be used only to detect bugs.</i>
*
* <p>This class is a member of the
* <a href="{@docRoot}/../technotes/guides/collections/index.html">
* Java Collections Framework</a>.
翻译成中文如下:
List和Deque接口的”双重链接列表”实现。实现了所有可选的列表操作,并且允许任何元素,包括null。
列表(list)是“列”表,每一列是一个最小单位。
所有的操作会对双重链接列表产生可以被预期的效果。索引这个列表(查找某个元素)的操作将会从头到尾遍历这个列表,无论离索引的位置近不近。
注意:这个实现不是线程同步的!如果不同的线程并发地访问一个链表,或者这些线程中的至少一个修改了这个链表结构,它就必须从外面被同步。结构上的修改指的是任何添加或者删除一个或多个元素的操作;仅仅设置一个元素的值不算是结构上的修改。这被典型地实现,依赖于对自然地封装了这个列表的一些对象的同步。
如果没有这样的对象存在,这个列表将会被用synchronizedList()方法包起来。这是在创建时期最好的做法,为的是阻止对这个列表意外的非同步的访问:
List list = Collections.synchronizedList(new LinkedList(…));
这个类的iterator()和listIterator()方法返回的迭代器是遵循”fail-fast”机制的:如果这个列表在迭代器被创建之后的任何时刻以任何除了通过这个迭代器自己的remove()或者add()方法之外的方式被结构上地修改,这个迭代器将会抛出一个ConcurrentModificationException异常。因此,在面对并发修改时,这个迭代器会干净利索地失败,而不是在将来的某个不确定的时间冒主观上的,不确定行为的风险。
注意,迭代器的”fail-fast”行为不能够确保它一定会凑效,一般来说,在非同步并发修改面前不可能做出任何严格的保证。遵循”fail-fast”机制的迭代器会在付出了最大努力的基础上抛ConcurrentModificationException异常。因此,为了正确性而编写一个依赖于此异常的程序是错误的:迭代器的fail-fast行为应该仅仅被用于debug。
这个类是Java集合框架的成员。
***
我们需要注意的是,add(E e)方法的返回类型是void,并不是boolean,也就是说它假定每次添加操作总会改变链表。如果链表有n个元素,就有n+1个位置可以添加新元素,请自行脑补。
List<String> staff = new LinkedList<>();
staff.add("Amy");
staff.add("Jim");
staff.add("slepin");
ListIterator<String> listIt = staff.listIterator();
String firstEmployee = listIt.next();
String secondEmployee = listIt.next();// skip past second element
listIt.add("king");//now "king" is the third element
for (String e : staff) {
System.out.println(e);
}
console result:
Amy
Jim
king
slepin
实现了ListIterator接口的类还可以反向遍历链表中的元素,还可以替换迭代器当前返回的元素。代码示例如下:
List<String> staff = new LinkedList<>();
staff.add("Amy");
staff.add("Jim");
staff.add("slepin");
ListIterator<String> listIt = staff.listIterator();
String firstEmployee = listIt.next();
String secondEmployee = listIt.next();// skip past second element
String specialEmp = listIt.previous();// skip backward-past second element "Jim"
listIt.set("peak");// replace the element that returned by next() or previous() method
for (String e : staff) {
System.out.println(e);
}
console result:
Amy
peak
slepin
现在我们要考虑一种情形:如果在一个迭代器修改某个有序集合的同时,另一个迭代器对这个集合进行遍历,这时肯定会出现混乱的状况。例如:一个迭代器在删除了由它的next()或previous()方法返回的元素的之后,另一个迭代器依然指向了这个被删除的元素,那么很显然这个有错误指向的迭代器就是无效的,不应该继续被使用。
ListIterator的设计十分优秀,迭代器如果发现它掌控的集合被另一个迭代器修改了,或者被这个集合自身的方法修改了,就会抛出一个Concurrent ModificationException异常。
List<String> staff = new LinkedList<>();
staff.add("Amy");
staff.add("Jim");
staff.add("slepin");
ListIterator<String> listIt1 = staff.listIterator();
ListIterator<String> listIt2 = staff.listIterator();
listIt1.next();
listIt1.remove();//delete first elelment "Amy"
listIt2.next();//want to refer to the deleted element "Amy"
//ConcurrentModificationException 并发修改异常
listIt2检测出这个链表被从外部修改了,所以对listIt2的next()调用抛出了一个ConcurrentModificationException异常。
为了避免发生上面那样的异常,我们最好遵守以下的规则:
可以根据实际业务需要给集合容器附加多个迭代器,但是限制这些迭代器都仅仅进行读取操作;另外,再单独附加一个既能读又能写的迭代器。
Collection接口中声明了很多用于操作链表的有用方法,其中大部分方法都是在LinkedList类的超类AbstractCollection中实现的,下面我们就来分析AbstractCollection类中实现toString()方法的代码。
//AbstractCollection类的toString()方法
public String toString() {
//获取当前集合的迭代器
Iterator<E> it = iterator();
//如果迭代器没有下一个元素可以越过,即集合为空
if (! it.hasNext())
//返回"[]"
return "[]";
//创建一个带缓存的线程危险的相对(StringBuffer)高效的StringBuilder类的实例
StringBuilder sb = new StringBuilder();
//StringBuiler对象的内容变成"["
sb.append('[');
for (;;) {
//越过并返回下一个相邻位置的元素
E e = it.next();
//如果这个元素就是当前集合(集合就只有一个元素),字符串的尾部添加“this collection”,否则,字符串的尾部添 //加这个元素的内容(字面量)
sb.append(e == this ? "(this Collection)" : e);
//如果迭代器到尾部了,没有下一个元素可以越过
if (! it.hasNext())
//在字符串的尾部添加“]”,然后调用这个StringBuiler对象的toString()方法,最后返回的是一个String对象
return sb.append(']').toString();
//如果迭代器下一个相邻位置的元素存在,就在字符串的尾部添加“,”和空格,继续循环
sb.append(',').append(' ');
}
}
@Override
//StringBuilder重载抽象父类AbstractStringBuilder的append(String str)方法
public StringBuilder append(String str) {
super.append(str);
return this;
}
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//StringBuilder重载抽象父类AbstractStringBuilder的append(Object obj)方法
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
public AbstractStringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
//StringBuilder重载抽象父类AbstractStringBuilder的toString()方法
@Override
public String toString() {
// Create a copy, don't share the array 创建一个备份,不分享数组
return new String(value, 0, count);
}
链表并不支持快速随机访问,注意,可以实现访问第i个元素,但是必须从头开始越过i-1个元素,才能到达第i个元素。没有快速的捷径可以走的。由于这个原因,在编程中如果有需要使用整数索引访问元素的情形,通常不应该选用链表。即使这样,LinkedList类还是提供了一个用来访问特定第i个元素的get()方法,哪怕这个方法效率低得惊人。如果你用了,就说明你选了错误的数据结构。
更可怕的是你写出了这样的代码!
for(int i =0;i<list.size();i++)
do something with list.get(i);
首先,我们分析i<list.size()
这条语句的执行情况,我们写一个同样功能的例子:
public static void main(String[] args) {
for (int i = 0; i < Tool.getCompareValue(); i++) {
System.out.println("the " + (i + 1) + "th time loop");
}
}
static class Tool {
static int j = 1;
static int getCompareValue() {
System.out.println("call getCompareValue() method " + j + "th time");
j++;
return 5;
}
}
控制台的输出如下:
call getCompareValue() method 1th time
the 1th time loop
call getCompareValue() method 2th time
the 2th time loop
call getCompareValue() method 3th time
the 3th time loop
call getCompareValue() method 4th time
the 4th time loop
call getCompareValue() method 5th time
the 5th time loop
call getCompareValue() method 6th time
也就是说,每次拿循环变量i和循环控制值比较时,你都额外多余地计算了一次循环控制值,要是在开始循环之前就求出循环控制值,然后循环时直接拿来用该多好呀!!
其次,你本意是希望通过反复调用list.get(i)来遍历list,但是,仔细想想,list.get(3)越过了第1个、第2个元素,list.get(i)越过了第1个,第2个,…,第i-1个元素,这就相当于在for循环中又内嵌了一个for循环,自然效率极低!
另外:实际上,Java迭代器指向的是两个元素之间的位置,所以可以同时产生两个索引:
nextIndex()方法返回下一次调用next方法时返回的元素的整数索引,previousIndex()方法返回下一次调用previous方法时返回的元素的整数索引。这两个方法的效率非常高,因为迭代器总保持着当前位置的计数值。
LinkedList集合类中的
E get(int);
E set(int,E);
void add(int,E);
E remove(int);
ListItrator<E> listIterator(int);
方法都依赖用整数索引来表示链表中的位置,而前面就说过了,LinkedList并不支持快速随机访问,LinkedList对象根本不做任何缓存位置信息的操作,它的索引形同虚设起的作用微乎其微,所以,应该避免使用上述以整数索引表示链表中位置的所有方法。
而通过LinkedList的ListItrator<E> listIterator();
方法(实际上是继承的他的超类AbstractList类的方法)返回的listIterator迭代器却通过private int nextIndex;
成员变量“缓存”了当前位置信息,所以调用迭代器的
void add(E);
void remove();
方法来减少在列表中插入和删除元素的代价是使用链表的唯一理由(不像其它普通列表插入或删除一个元素就必须要其后所有元素都移位,链表只需要断链和建立新链这样极少量的操作);如果元素规模不大,这个优势并不明显,使用ArrayList也是可以的。