上一节咱们手写实现了单链表和双链表,本节咱们来看看源码是如何实现的而且对比手动实现有哪些可优化的地方。html
经过上一节咱们对双链表原理的讲解,同时咱们对照以下图也可知道双链表算法实现有以下特色。java
一、链表中的每一个连接都是一个对象(也称为元素,节点等)。
二、每一个对象都包含一个引用(地址)到下一个对象的位置。
三、链表中前驱节点指向null表示链表的头,链表中的后继节点指向null,表示链表的尾。
四、连接列表能够在运行时(程序运行时,编译后)动态增加和缩小,仅受可用物理内存的限制。node
咱们首先看看LinkedList给咱们提供了哪些可操做的方法,以下:算法
LinkedList<String> list = new LinkedList(); //添加元素到尾结点 list.add("str"); //添加元素到指定索引节点 list.add(1,"str"); //添加元素到头节点 list.addFirst("first"); //添加元素到尾节点 list.addLast("last"); //返回指定索引元素 list.get(1); //返回头节点元素 list.getFirst(); //返回尾节点元素 list.getLast(); //添加元素到尾结点 list.offer("str"); //添加元素到头节点 list.offerFirst("str"); //添加元素到尾节点 list.offerLast("str");
首先咱们来看看LinkedList中定义了哪些变量。首先对节点元素的操做在该类中定义了节点内部类,由于其余地方用不到该节点类,这点和咱们上一节所定义的节点类没有什么差别。数组
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; } }
接下来咱们再来看看对链表的定义,以下:安全
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; //构造函数 public LinkedList() { } //构造函数传入集合进行转换 public LinkedList(Collection<? extends E> c) { this(); addAll(c); } }
上述咱们所定义的双链表类继承了对应的类,实现了对应的接口,咱们经过一张图来进行概括,以下:dom
紧接着经过如上一张图以及上一节咱们对双链表的基本原理讲解,咱们首先给出源码中双链表特色,以下:函数
一、能够用做队列,双端队列或栈。由于已实现了List可做为队列,同时也实现了Queue和Deque接口可做为双端队列或栈。
二、容许全部元素且可包含重复和NULL。
三、LinkedList维护元素的插入顺序。
四、非线程安全。 若是多个线程同时访问链表,而且如有一个线程在结构上修改了列表,则必须在外部进行同步,使用Collections.synchronizedList(new LinkedList())获取同步的链表。
五、迭代器支持快速失败(Fail-Fast),可能会抛出ConcurrentModificationException。
六、未实现RandomAccess接口。因此它不支持随机访问元素,咱们只能按顺序访问元素。源码分析
因为上一节咱们实现了其基本原理,而在java中实现双链表只不过是给咱们提供了更多可操做的方法,好比在头结点、尾节点插入元素是同样的,这里咱们就不一一贴出源码了,针对在指定索引插入元素这点比咱们处理的好,上一节咱们对指定索引插入元素采起循环遍历的方式,同时咱们只用到了后继节点,而在java中,它是如何处理的呢,咱们下面来分析其源码:性能
//指定索引插入元素 public void add(int index, E element) { //检查插入索引位置是否超过边界,不然抛出异常 checkPositionIndex(index); //若是插入索引和链表长度相等则插入尾节点 if (index == size) linkLast(element); else //不然在指定索引插入元素 linkBefore(element, node(index)); }
接下来咱们继续看看若不是插入尾节点,那么插入指定索引位置,它是如何处理的呢?
//在节点succ后插入元素e void linkBefore(E e, Node<E> succ) { //定义succ节点的前驱节点 final Node<E> pred = succ.prev; //实例化要插入元素的节点 final Node<E> newNode = new Node<>(pred, e, succ); //插入元素的前驱节点即为succ succ.prev = newNode; if (pred == null) //若是succ的前驱为null,说明为头节点则插入元素的节点为头节点 first = newNode; else //不然待插入指定索引的节点的后继节点为插入元素的节点 pred.next = newNode; size++; modCount++; } //获取指定索引的节点 Node<E> node(int index) { //若索引位置小于链表长度的一半,则从头节点指向后继节点循环查找索引元素 if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { //不然从尾节点指向前驱节点循环查找索引元素 Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
由上看出咱们上一节手动指定索引插入元素,在java中处理的更为巧妙且性能更好,由于经过指定索引和链表的长度的二分之一做为判断依据能更快查找到指定索引节点。经过查看源码咱们会发现add对应offer方法都是插入到尾节点,而addFirst对应offerFirst都是插入到头节点,addLast对应offerLast都是插入到尾节点。详细区别请参考园友文章,这里我就不列举了。http://www.javashuo.com/article/p-epzovtkp-hd.html。只要咱们了解了算法基本原理,再去看看源码其实很简单,只不过是对照着看看是否有更好的实现方式而已,关于源码分析咱们就到此为止。这里咱们须要强调下双链表的特色有一个是快速失败机制及Fail-Fast,这里咱们经过例子来讲明。
快速迭代失败 - 当咱们尝试在迭代时修改集合的内部结构时,它会抛出ConcurrentModificationException,是失败的快速迭代器。让咱们来看以下例子
LinkedList<String> listObject = new LinkedList<>(); listObject.add("ram"); listObject.add("mohan"); listObject.add("shyam"); listObject.add("mohan"); listObject.add("ram"); Iterator it = listObject.iterator(); while (it.hasNext()) { listObject.add("raju"); System.out.println(it.next()); }
在进一步讨论以前,咱们分析为何会出现此异常,咱们注意到从next()方法开始的异常堆栈问题。 在next()方法中,还有另外一个方法checkForComodification(),它根据某些条件抛出了ConcurrentModificationException异常,接下来 让咱们看看next()和checkForComodification()方法中发生了什么。
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
在checkForComodification()方法内部,若是modcount不等于expectedModCount则抛出ConcurrentModificationException(扩展RuntimException)。根据上述状况,当咱们在循环中执行listObject.add(“raju”)时,modCount的值变为6可是expectedCount的值仍然是5,这就是咱们获得ConcurrentModificationException的缘由。因此看来咱们迭代时在next()方法中遇到了问题,显然这种处理方式毫无疑问是正确的, 若是咱们不使用next方法打印元素,咱们使用加强的for循环试试。
LinkedList<String> listObject = new LinkedList<>(); listObject.add("ram"); listObject.add("mohan"); listObject.add("shyam"); listObject.add("mohan"); listObject.add("ram"); for (String s : listObject ){ listObject.add("raju"); System.out.println(s); }
经过使用加强的for循环咱们仍然获得ConcurrentModificationException异常,这是为何呢?咱们没有使用Iterator进行迭代啊,经过如上堆栈信息咱们知道,即便咱们使用加强的for循环,内部的next()方法也会被调用。在JDK1.5中,一些新类(CopyOnWriteArrayList,CopyOnWriteArraySet,ConcurrentHashMap等)不会抛出ConcurrentModificationException,即便咱们在迭代时修改List,Set或Map的结构。 这些类用于克隆或复制集合Object。
List<String> listObject = new CopyOnWriteArrayList<>(); listObject.add("ram"); listObject.add("mohan"); listObject.add("shyam"); listObject.add("mohan"); listObject.add("ram"); Iterator it = listObject.iterator(); while (it.hasNext()) { listObject.add("raju"); System.out.println(it.next()); }
接下来咱们总结出Fail-Fast和Fail Safe的区别所在,以下:
序号 | Fail Fast | Fail Safe |
---|---|---|
1. | Fail fast应用于集合对象 | 应用于克隆或集合对象的副本。 |
2. | 当迭代时,咱们对集合元素进行修改时将抛出ConcurrentModificationException异常 | 不会抛出任何异常 |
3. | 须要更少的内存 | 须要额外的内存 |
4. | ArrayList,HashSet, HashMap等等被使用时 | CopyOnWriteArrayList,ConcurrentHashMap等等类被使用时 |
咱们来看看在java中是如何将数组转换为LinkedList或将Linkedlist如何转换为数组的呢。
LinkedList<String> linkedList = new LinkedList<>(); linkedList.add("A"); linkedList.add("B"); linkedList.add("C"); linkedList.add("D"); //1. LinkedList to Array String array[] = new String[linkedList.size()]; linkedList.toArray(array); System.out.println(Arrays.toString(array)); //2. Array to LinkedList LinkedList<String> linkedListNew = new LinkedList<>(Arrays.asList(array)); System.out.println(linkedListNew);
咱们可使用Collections.sort()方法对连接列表进行排序,如果对于对象的自定义排序,咱们可使用Collections.sort(连接的List,比较器)方法。以下:
LinkedList<String> linkedList = new LinkedList<>(); linkedList.add("A"); linkedList.add("C"); linkedList.add("B"); linkedList.add("D"); //Unsorted System.out.println(linkedList); //1. Sort the list Collections.sort(linkedList); //Sorted System.out.println(linkedList); //2. Custom sorting Collections.sort(linkedList, Collections.reverseOrder()); linkedList.sort(Collections.reverseOrder()); //Custom sorted System.out.println(linkedList);
在Java LinkedList类中,操做很快,由于不须要发生转换,基本上,全部添加和删除方法都提供了很是好的性能O(1)。LinkedList经常使用方法时间复杂度总结以下:
- add(E element) :O(1).
- get(int index) 和add(int index, E element) : O(N).
- remove(int index) :O(N).
- Iterator.remove() : O(1).
- ListIterator.add(E element) : O(1).
前面咱们也详细分析了ArrayList源码,以下咱们详细分析比较下LinkedList和ArraryList,以下:
一、ArrayList使用动态扩容来调整数组大小。而LinkedList使用双向链表实现。
二、ArrayList容许随机访问它的元素,而LinkedList则不容许,只能按顺序访问链表中的节点,所以访问特定节点会很慢。
三、LinkedList也实现了Queue接口,它添加了比ArrayList更多的方法,例如offer,peek,poll等。
四、与LinkedList相比,ArrayList在添加和删除方面较慢,但在get中更快,由于若是在LinkedList中数组已满,则无需调整数组大小并将内容复制到新数组。
五、LinkedList比ArrayList具备更多的内存开销,由于在ArrayList中,每一个索引仅保存实际对象,但在LinkedList的状况下,每一个节点都保存后继节点和前驱节点的数据和地址。
综上所述LinkedList貌似毫无用武之地,那么咱们究竟何时能够用LinkedList呢?我认为可从如下三方面考虑
一、不须要随机访问任何特定元素。
二、须要频繁进行插入和删除。
三、不肯定链表中有多少项。
本节咱们稍微分析了下LinkedList源码,而后对比了ArrayList和LinkedList优缺点,以及咱们应当何时用LinkedList,除开极少数状况,大部分状况下都不太会用到LinkedList,好了本节咱们到这里为止,下一节咱们进入Hashtable的学习。感谢您的阅读,下节见。