Java集合框架源码分析(2)LinkedList

时间:2022-09-02 15:43:14

链表(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也是可以的。

Java集合框架源码分析(2)LinkedList的更多相关文章

  1. 【java集合框架源码剖析系列】java源码剖析之LinkedList

    注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 在实际项目中LinkedList也是使用频率非常高的一种集合,本博客将从源码角度带领大家学习关于LinkedList的知识. ...

  2. 【java集合框架源码剖析系列】java源码剖析之ArrayList

    注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 本博客将从源码角度带领大家学习关于ArrayList的知识. 一ArrayList类的定义: public class Arr ...

  3. 【java集合框架源码剖析系列】java源码剖析之TreeSet

    本博客将从源码的角度带领大家学习TreeSet相关的知识. 一TreeSet类的定义: public class TreeSet<E> extends AbstractSet<E&g ...

  4. 【java集合框架源码剖析系列】java源码剖析之HashSet

    注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本.本博客将从源码角度带领大家学习关于HashSet的知识. 一HashSet的定义: public class HashSet&l ...

  5. 【java集合框架源码剖析系列】java源码剖析之TreeMap

    注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本.本博客将从源码角度带领大家学习关于TreeMap的知识. 一TreeMap的定义: public class TreeMap&l ...

  6. 【java集合框架源码剖析系列】java源码剖析之HashMap

    前言:之所以打算写java集合框架源码剖析系列博客是因为自己反思了一下阿里内推一面的失败(估计没过,因为写此博客已距阿里巴巴一面一个星期),当时面试完之后感觉自己回答的挺好的,而且据面试官最后说的这几 ...

  7. Java集合框架源码(二)——hashSet

    注:本人的源码基于JDK1.8.0,JDK的版本可以在命令行模式下通过java -version命令查看. 在前面的博文(Java集合框架源码(一)——hashMap)中我们详细讲了HashMap的原 ...

  8. JDK集合框架源码分析 - 简单概要

    1.类继承体系 在集合框架的类继承体系中,最顶层有两个接口Collection.Map: Collection 表示一组纯数据 Map 表示一组key-value对 Collection的类继承体系: ...

  9. 【java集合框架源码剖析系列】java源码剖析之java集合中的折半插入排序算法

    注:关于排序算法,博主写过[数据结构排序算法系列]数据结构八大排序算法,基本上把所有的排序算法都详细的讲解过,而之所以单独将java集合中的排序算法拿出来讲解,是因为在阿里巴巴内推面试的时候面试官问过 ...

随机推荐

  1. java入门笔记(1)

    上图表达的是我们写的java程序是怎么在电脑上运行并算出结果的.编译器判断语法是否正确,如果错误,不能生成.class文件. JVM(Java Virtual Machine)是java虚拟机. JV ...

  2. hanio 塔和递规的理解。

    //递规很好理解,但是初看hanoi的时候,总没有理所当然的感觉.//那应该是对递规根本还没理解吧.仔细想了下.有点总结. 后来翻到 <<数据结构>> 112页,原来hanio ...

  3. Centos 添加SWAP&lpar;交换分区&rpar;

    一般情况下,内存过小时,可以增加 swap,大小为内存的2倍为宜,具体设置如下: 1.进入目录cd /var/ 2.获取要增加的SWAP文件块(这里以1GB为例)dd if=/dev/zero of= ...

  4. tcpdump 使用

    例子: 首先切换到root用户 tcpdump -w  aaa.cap   -i eth7   -nn -x  'port  9999'  -c  1 以例子说明参数: -w:输出到文件aaa.cap ...

  5. 7&period;Django CSRF 中间件

    CSRF 1.概述 CSRF(Cross Site Request Forgery)跨站点伪造请求,举例来讲,某个恶意的网站上有一个指向你的网站的链接,如果某个用户已经登录到你的网站上了,那么当这个用 ...

  6. ABP&plus;AdminLTE&plus;Bootstrap Table权限管理系统第十一节--Bootstrap Table用户管理列表以及Module Zero之用户管理

    返回总目录:ABP+AdminLTE+Bootstrap Table权限管理系统一期 用户实体 用户实体代表应用的一个用户,它派生自AbpUser类,如下所示: public class User : ...

  7. bzoj5293&colon; &lbrack;Bjoi2018&rsqb;求和

    题目链接 bzoj5293: [Bjoi2018]求和 题解 暴力 对于lca为1的好坑啊.... 代码 #include<cmath> #include<cstdio> #i ...

  8. Sublime Text3 使用总结

    一.简介: Sublime Text 3是一款强大而精巧的文本编辑器 [点击下载].它的界面友好.功能非凡.性能极佳可令代码高亮.语法提示.自动完成更重要的是,它支持众多插件扩展——锦上添花.强之又强 ...

  9. hdu4901The Romantic Hero

    #include<iostream> #include<map> #include<string> #include<cstring> #include ...

  10. 随手练——洛谷-P1151(枚举与暴力搜索)

    枚举 #include <iostream> using namespace std; int main() { ; cin >> k; ; i < ; i++) { ) ...