Java集合源码学习笔记(三)LinkedList分析

前面学习了ArrayList的源码,
数组是顺序存储结构,存储区间是连续的,占用内存严重,故空间复杂的很大。
但数组的二分查找时间复杂度小,为O(1),数组的特点是寻址容易,插入和删除困难。
今天学习另外的一种常用数据结构LinkedList的实现,
LinkedList使用链表作为存储结构,链表是线性存储结构,在内存上不是连续的一段空间,
占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N),链表的特点是寻址困难,插入和删除容易。
所有的代码都基于JDK 1.6。

>>关于LinkedList

LinkedList继承了AbstractSequentialList,实现了List,Deque,Cloneable,Serializable 接口,

(1)继承和实现

继承AbstractSequentialList类,提供了相关的添加、删除、修改、遍历等功能。
实现List接口,提供了相关的添加、删除、修改、遍历等功能。
实现 Deque 接口,即能将LinkedList当作双端队列使用,可以用做队列或者栈
实现了Cloneable接口,即覆盖了函数clone(),能被克隆复制,
实现java.io.Serializable接口,LinkedList支持序列化,能通过序列化传输。

(2)线程安全

LinkedList是非同步的,即线程不安全,如果有多个线程同时访问LinkedList,可能会抛出ConcurrentModificationException异常。

final void checkForComodification() {
	    if (modCount != expectedModCount)
		throw new ConcurrentModificationException();
	}

  代码中,modCount记录了LinkedList结构被修改的次数。Iterator初始化时,expectedModCount=modCount。任何通过Iterator修改LinkedList结构的行为都会同时更新expectedModCount和modCount,使这两个值相等。通过LinkedList对象修改其结构的方法只更新modCount。所以假设有两个线程A和B。A通过Iterator遍历并修改LinkedList,而B,与此同时,通过对象修改其结构,那么Iterator的相关方法就会抛出异常

>>双向链表结构

和普通的链表不同,双向链表每个节点不止维护指向下一个节点的next指针,还维护着指向上一个节点的previous指针。
(1)内部实现

LinkedList内部使用Entry<E>来封装双向循环链表结点。
LinkedList头结点的定义:

//头结点的定义
	private transient Entry<E> header = new Entry<E>(null, null, null);
	//链表的实际长度
    private transient int size = 0;

  Entry是一个静态内部类,

private static class Entry<E> {
	E element;
	Entry<E> next;
	Entry<E> previous;

	Entry(E element, Entry<E> next, Entry<E> previous) {
	    this.element = element;
	    this.next = next;
	    this.previous = previous;
	}
    }

  

(2)构造函数

LinkedList内部提供了两个构造函数,

 /**
     * 初始化一个空的list,可以理解为一个双向循环链表
     */
    public LinkedList() {
        header.next = header.previous = header;
    }
    /**
     * 使用指定的collection构造一个list
     */
    public LinkedList(Collection<? extends E> c) {
	this();
	addAll(c);
    }

  

>>常用方法

(1)遍历方式

LinkedList支持多种遍历方式。建议不要采用随机访问的方式去遍历LinkedList,而采用逐个遍历的方式。
(01) 第一种,通过迭代器遍历。即通过Iterator去遍历。

for(Iterator iter = list.iterator(); iter.hasNext();)
    iter.next();

(02) 通过快速随机访问遍历LinkedList

int size = list.size();
for (int i=0; i<size; i++) {
    list.get(i);
}

(03) 通过另外一种for循环来遍历LinkedList

for (Integer integ:list)
    ;

(04) 通过pollFirst()来遍历LinkedList

while(list.pollFirst() != null)
    ;

(05) 通过pollLast()来遍历LinkedList

while(list.pollLast() != null)
    ;

(06) 通过removeFirst()来遍历LinkedList

try {
    while(list.removeFirst() != null)
        ;
} catch (NoSuchElementException e) {
}

(07) 通过removeLast()来遍历LinkedList

try {
    while(list.removeLast() != null)
        ;
} catch (NoSuchElementException e) {
}

测试这些遍历方式效率的代码如下

  1 import java.util.List;
  2 import java.util.Iterator;
  3 import java.util.LinkedList;
  4 import java.util.NoSuchElementException;
  5
  6 /*
  7  * @desc 测试LinkedList的几种遍历方式和效率
  8  *
  9  * @author skywang
 10  */
 11 public class LinkedListThruTest {
 12     public static void main(String[] args) {
 13         // 通过Iterator遍历LinkedList
 14         iteratorLinkedListThruIterator(getLinkedList()) ;
 15
 16         // 通过快速随机访问遍历LinkedList
 17         iteratorLinkedListThruForeach(getLinkedList()) ;
 18
 19         // 通过for循环的变种来访问遍历LinkedList
 20         iteratorThroughFor2(getLinkedList()) ;
 21
 22         // 通过PollFirst()遍历LinkedList
 23         iteratorThroughPollFirst(getLinkedList()) ;
 24
 25         // 通过PollLast()遍历LinkedList
 26         iteratorThroughPollLast(getLinkedList()) ;
 27
 28         // 通过removeFirst()遍历LinkedList
 29         iteratorThroughRemoveFirst(getLinkedList()) ;
 30
 31         // 通过removeLast()遍历LinkedList
 32         iteratorThroughRemoveLast(getLinkedList()) ;
 33     }
 34
 35     private static LinkedList getLinkedList() {
 36         LinkedList llist = new LinkedList();
 37         for (int i=0; i<100000; i++)
 38             llist.addLast(i);
 39
 40         return llist;
 41     }
 42     /**
 43      * 通过快迭代器遍历LinkedList
 44      */
 45     private static void iteratorLinkedListThruIterator(LinkedList<Integer> list) {
 46         if (list == null)
 47             return ;
 48
 49         // 记录开始时间
 50         long start = System.currentTimeMillis();
 51
 52         for(Iterator iter = list.iterator(); iter.hasNext();)
 53             iter.next();
 54
 55         // 记录结束时间
 56         long end = System.currentTimeMillis();
 57         long interval = end - start;
 58         System.out.println("iteratorLinkedListThruIterator:" + interval+" ms");
 59     }
 60
 61     /**
 62      * 通过快速随机访问遍历LinkedList
 63      */
 64     private static void iteratorLinkedListThruForeach(LinkedList<Integer> list) {
 65         if (list == null)
 66             return ;
 67
 68         // 记录开始时间
 69         long start = System.currentTimeMillis();
 70
 71         int size = list.size();
 72         for (int i=0; i<size; i++) {
 73             list.get(i);
 74         }
 75         // 记录结束时间
 76         long end = System.currentTimeMillis();
 77         long interval = end - start;
 78         System.out.println("iteratorLinkedListThruForeach:" + interval+" ms");
 79     }
 80
 81     /**
 82      * 通过另外一种for循环来遍历LinkedList
 83      */
 84     private static void iteratorThroughFor2(LinkedList<Integer> list) {
 85         if (list == null)
 86             return ;
 87
 88         // 记录开始时间
 89         long start = System.currentTimeMillis();
 90
 91         for (Integer integ:list)
 92             ;
 93
 94         // 记录结束时间
 95         long end = System.currentTimeMillis();
 96         long interval = end - start;
 97         System.out.println("iteratorThroughFor2:" + interval+" ms");
 98     }
 99
100     /**
101      * 通过pollFirst()来遍历LinkedList
102      */
103     private static void iteratorThroughPollFirst(LinkedList<Integer> list) {
104         if (list == null)
105             return ;
106
107         // 记录开始时间
108         long start = System.currentTimeMillis();
109         while(list.pollFirst() != null)
110             ;
111
112         // 记录结束时间
113         long end = System.currentTimeMillis();
114         long interval = end - start;
115         System.out.println("iteratorThroughPollFirst:" + interval+" ms");
116     }
117
118     /**
119      * 通过pollLast()来遍历LinkedList
120      */
121     private static void iteratorThroughPollLast(LinkedList<Integer> list) {
122         if (list == null)
123             return ;
124
125         // 记录开始时间
126         long start = System.currentTimeMillis();
127         while(list.pollLast() != null)
128             ;
129
130         // 记录结束时间
131         long end = System.currentTimeMillis();
132         long interval = end - start;
133         System.out.println("iteratorThroughPollLast:" + interval+" ms");
134     }
135
136     /**
137      * 通过removeFirst()来遍历LinkedList
138      */
139     private static void iteratorThroughRemoveFirst(LinkedList<Integer> list) {
140         if (list == null)
141             return ;
142
143         // 记录开始时间
144         long start = System.currentTimeMillis();
145         try {
146             while(list.removeFirst() != null)
147                 ;
148         } catch (NoSuchElementException e) {
149         }
150
151         // 记录结束时间
152         long end = System.currentTimeMillis();
153         long interval = end - start;
154         System.out.println("iteratorThroughRemoveFirst:" + interval+" ms");
155     }
156
157     /**
158      * 通过removeLast()来遍历LinkedList
159      */
160     private static void iteratorThroughRemoveLast(LinkedList<Integer> list) {
161         if (list == null)
162             return ;
163
164         // 记录开始时间
165         long start = System.currentTimeMillis();
166         try {
167             while(list.removeLast() != null)
168                 ;
169         } catch (NoSuchElementException e) {
170         }
171
172         // 记录结束时间
173         long end = System.currentTimeMillis();
174         long interval = end - start;
175         System.out.println("iteratorThroughRemoveLast:" + interval+" ms");
176     }
177
178 }

执行结果

iteratorLinkedListThruIterator:8 ms
iteratorLinkedListThruForeach:3724 ms
iteratorThroughFor2:5 ms
iteratorThroughPollFirst:8 ms
iteratorThroughPollLast:6 ms
iteratorThroughRemoveFirst:2 ms
iteratorThroughRemoveLast:2 ms

遍历LinkedList时,使用removeFist()或removeLast()效率最高。但用它们遍历时,会删除原始数据;若单纯只读取,而不删除,应该使用第3种遍历方式。
无论如何,千万不要通过随机访问去遍历LinkedList!

(2)常用方法(从API摘的)

boolean add(E e)
将指定元素添加到此列表的结尾。
void add(int index, E element)
在此列表中指定的位置插入指定的元素。
boolean addAll(Collection<? extends E> c)
添加指定 collection 中的所有元素到此列表的结尾,顺序是指定 collection 的迭代器返回这些元素的顺序。
boolean addAll(int index, Collection<? extends E> c)
将指定 collection 中的所有元素从指定位置开始插入此列表。
void addFirst(E e)
将指定元素插入此列表的开头。
void addLast(E e)
将指定元素添加到此列表的结尾。
void clear()
从此列表中移除所有元素。
Object clone()
返回此 LinkedList 的浅表副本。
boolean contains(Object o)
如果此列表包含指定元素,则返回 true。
Iterator<E> descendingIterator()
返回以逆向顺序在此双端队列的元素上进行迭代的迭代器。
E element()
获取但不移除此列表的头(第一个元素)。
E get(int index)
返回此列表中指定位置处的元素。
E getFirst()
返回此列表的第一个元素。
E getLast()
返回此列表的最后一个元素。
int indexOf(Object o)
返回此列表中首次出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。
int lastIndexOf(Object o)
返回此列表中最后出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。
ListIterator<E> listIterator(int index)
返回此列表中的元素的列表迭代器(按适当顺序),从列表中指定位置开始。
boolean offer(E e)
将指定元素添加到此列表的末尾(最后一个元素)。
boolean offerFirst(E e)
在此列表的开头插入指定的元素。
boolean offerLast(E e)
在此列表末尾插入指定的元素。
E peek()
获取但不移除此列表的头(第一个元素)。
E peekFirst()
获取但不移除此列表的第一个元素;如果此列表为空,则返回 null。
E peekLast()
获取但不移除此列表的最后一个元素;如果此列表为空,则返回 null。
E poll()
获取并移除此列表的头(第一个元素)
E pollFirst()
获取并移除此列表的第一个元素;如果此列表为空,则返回 null。
E pollLast()
获取并移除此列表的最后一个元素;如果此列表为空,则返回 null。
E pop()
从此列表所表示的堆栈处弹出一个元素。
void push(E e)
将元素推入此列表所表示的堆栈。
E remove()
获取并移除此列表的头(第一个元素)。
E remove(int index)
移除此列表中指定位置处的元素。
boolean remove(Object o)
从此列表中移除首次出现的指定元素(如果存在)。
E removeFirst()
移除并返回此列表的第一个元素。
boolean removeFirstOccurrence(Object o)
从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)。
E removeLast()
移除并返回此列表的最后一个元素。
boolean removeLastOccurrence(Object o)
从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)。
E set(int index, E element)
将此列表中指定位置的元素替换为指定的元素。
int size()
返回此列表的元素数。

>>双端队列

双端队列是一个限定插入和删除操作的数据结构,具有队列和栈的性质,
双端队列与栈或队列相比,是一种多用途的数据结构,在容器类库中有时会用双端队列来提供栈和队列两种功能。
查看源码的实现,可以发现作为队列和栈时的相关方法。

(1)作为队列使用

add(e) 内部实现是addLast(e)
offer(e) 入队,直接返回add()
remove() 获取并移除列表第一个元素,和poll()相同,内部调用removeFirst()
poll() 出队 获取并移除队头元素 内部实现是调用removeFirst()
element() 返回列表第一个元素但不移除,内部调用getFirst()
peek() 返回列表第一个元素但不移除,和element()相同,内部调用getFirst()

(2)作为栈使用

push(e) 入栈 即addFirst(e)
pop() 出栈 即removeFirst()
peek() 获取栈顶元素但不移除 即peekFirst()

>>源码分析

(1)主要的节点更新操作

源码中的两个私有方法addBefore和remove是维护了节点更新的主要操作,
这一部分主要是数据结构中对链表的操作,理解起来比较简单,
大部分的add、remode等操作都可以通过这两个方法实现。

 /**
     * 在传入节点之前插入新的节点元素
     */
    private Entry<E> addBefore(E e, Entry<E> entry) {
    //构造一个新的节点
	Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
	//调整新节点前后节点的指向
	newEntry.previous.next = newEntry;
	newEntry.next.previous = newEntry;
	size++;
	modCount++;
	return newEntry;
    }

	 /**
     * 在链表中删除这个节点
     */
    private E remove(Entry<E> e) {
    	//e等于初始化时的空节点,抛出异常
	if (e == header)
	    throw new NoSuchElementException();
        E result = e.element;
    	/**
    	 * 删除操作就是调整前后结点指针的指向,绕过传入节点
    	 * 然后再把传入节点的前后指针以及value都置为null
    	 */
	e.previous.next = e.next;
	e.next.previous = e.previous;
        e.next = e.previous = null;
        e.element = null;
	size--;
	modCount++;
        return result;
    }

(2)List迭代器 通过ListItr内部类实现

  private class ListItr implements ListIterator<E> {
	private Entry<E> lastReturned = header;
	private Entry<E> next;
	private int nextIndex;
	private int expectedModCount = modCount;

	ListItr(int index) {
	    if (index < 0 || index > size)
		throw new IndexOutOfBoundsException("Index: "+index+
						    ", Size: "+size);
	    if (index < (size >> 1)) {
		next = header.next;
		for (nextIndex=0; nextIndex<index; nextIndex++)
		    next = next.next;
	    } else {
		next = header;
		for (nextIndex=size; nextIndex>index; nextIndex--)
		    next = next.previous;
	    }
	}

	public boolean hasNext() {
	    return nextIndex != size;
	}

	public E next() {
	    checkForComodification();
	    if (nextIndex == size)
		throw new NoSuchElementException();

	    lastReturned = next;
	    next = next.next;
	    nextIndex++;
	    return lastReturned.element;
	}

	public boolean hasPrevious() {
	    return nextIndex != 0;
	}

	public E previous() {
	    if (nextIndex == 0)
		throw new NoSuchElementException();

	    lastReturned = next = next.previous;
	    nextIndex--;
	    checkForComodification();
	    return lastReturned.element;
	}

	public int nextIndex() {
	    return nextIndex;
	}

	public int previousIndex() {
	    return nextIndex-1;
	}

	public void remove() {
            checkForComodification();
            Entry<E> lastNext = lastReturned.next;
            try {
                LinkedList.this.remove(lastReturned);
            } catch (NoSuchElementException e) {
                throw new IllegalStateException();
            }
	    if (next==lastReturned)
                next = lastNext;
            else
		nextIndex--;
	    lastReturned = header;
	    expectedModCount++;
	}

	public void set(E e) {
	    if (lastReturned == header)
		throw new IllegalStateException();
	    checkForComodification();
	    lastReturned.element = e;
	}

	public void add(E e) {
	    checkForComodification();
	    lastReturned = header;
	    addBefore(e, next);
	    nextIndex++;
	    expectedModCount++;
	}

	final void checkForComodification() {
	    if (modCount != expectedModCount)
		throw new ConcurrentModificationException();
	}
    }

  

 

>>fail-fast机制

fail-fast,快速失败是Java集合的一种错误检测机制。
当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。
记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

>>ArrayList和LinkedList的区别

ArrayList是一个基于数组的结构,里面的节点相互之间没有特别的联系,默认的大小是10,最大大小是Integer.MAX_VALUE - 8。
当大小不够时会自动增长,它可以通过get,set来直接获取、修改某一个节点数据。

LinkedList是一个基于双向链表的结构,每一个节点都有两个指针来分别指向上一个节点和下一个节点。它是可变长度的。
这两个实现类的区别在于,ArrayList的get()/set()效率比LinkedList高,而LinkedList的add()/remove()效率比ArrayList高。

具体来说:数组申请一块连续的内存空间,是在编译期间就确定大小的,运行时期不可动态改变,但为什么ArrayList可以改变大小呢,
因为在add如果超过了设定的大小就会创建一个新的更大的(增长率好像是0.5)ArrayList,
然后将原来的list复制到新的list中,并将地址指向新的list,旧的list被GC回收,显然这是非常消耗内存而且效率非常低的。
但是由于它申请的内存空间是连续的,可以直接通过下标来获取需要的数据,时间复杂度为O(1),而链表则是O(n),
而链表结构不一样,它可以动态申请内存空间。在需要加入的节点的上一个节点的指针解开并指向新加节点,再将新加节点的指针指向后一个节点即可。速度很快的。
所以说ArrayList适合查询,LinkedList适合增删。

参考 Java 集合系列05之 LinkedList详细介绍(源码解析)和使用示例

时间: 2024-10-18 11:57:08

Java集合源码学习笔记(三)LinkedList分析的相关文章

Java集合源码学习笔记(二)ArrayList分析

Java集合源码学习笔记(二)ArrayList分析 >>关于ArrayList ArrayList直接继承AbstractList,实现了List. RandomAccess.Cloneable.Serializable接口,为什么叫"ArrayList",因为ArrayList内部是用一个数组存储元素值,相当于一个可变大小的数组,也就是动态数组. (1)继承和实现继承了AbstractList,实现了List:ArrayList是一个数组队列,提供了相关的添加.删除.修

Java集合源码学习笔记(四)HashMap分析

ArrayList.LinkedList和HashMap的源码是一起看的,横向对比吧,感觉对这三种数据结构的理解加深了很多. >>数组.链表和哈希表结构 数据结构中有数组和链表来实现对数据的存储,这两者有不同的应用场景,数组的特点是:寻址容易,插入和删除困难:链表的特点是:寻址困难,插入和删除容易:哈希表的实现结合了这两点,哈希表的实现方式有多种,在HashMap中使用的是链地址法,也就是拉链法.看下面这张流传很广的图, 拉链法实际上是一种链表数组的结构,由数组加链表组成,在这个长度为16的数

Java项目源码学习笔记(三):PathMatcher

先来看看内部类Node节点: private static class Node{ private HashMap<String, Node> mMap; private int mKind = NOT_FOUND; Node addChild(String segment){ if(null == mMap){ mMap = new HashMap<String, Node>(); }else{ Node node = mMap.get(segment); if(node !=

Java项目源码学习笔记(二):Path

import java.lang.ref.WeakReference; import java.util.ArrayList; public class Path{ private static Path sRoot = new Path(null, "ROOT"); private final Path mParent; private final String mSegment; private WeakReference<Object> mObject; privat

Java项目源码学习笔记(一):IdentityCache

import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.Set; public class IdentityCache<K, V>{ private final HashMap<K, Entry<K,V>> mWeakMap = new Ha

yii2源码学习笔记(三)

组件(component),是Yii框架的基类,实现了属性.事件.行为三类功能,如果需要事件和行为的功能,需要继承该类. yii\base\Component代码详解 <?php /** * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ namespace yii\base;

Java多线程之JUC包:ReentrantLock源码学习笔记

若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/go2sea/p/5627539.html ReentrantLock是JUC包提供的一种可重入独占锁,它实现了Lock接口.与Semaphore类似,ReentrantLock也提供了两种工作模式:公平模式&非公平模式,也是通过自定义两种同步器FairSync&NonfairSync来实现的. 源代码: /* * ORACLE PROPRIETARY/CONF

Java集合源码分析(三)LinkedList

LinkedList简介 LinkedList是基于双向循环链表(从源码中可以很容易看出)实现的,除了可以当做链表来操作外,它还可以当做栈.队列和双端队列来使用. LinkedList同样是非线程安全的,只在单线程下适合使用. LinkedList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了Cloneable接口,能被克隆. LinkedList源码 以下是linkedList源码(加入简单代码注释): /* * @(#)LinkedList.java 1.6

Java多线程之JUC包:ReentrantReadWriteLock源码学习笔记

若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/go2sea/p/5634701.html ReentrantLock提供了标准的互斥操作,但在应用中,我们对一个资源的访问有两种方式:读和写,读操作一般不会影响数据的一致性问题.但如果我们使用ReentrantLock,则在需要在读操作的时候也独占锁,这会导致并发效率大大降低.JUC包提供了读写锁ReentrantReadWriteLock,使得读写锁分离,在上述情