JAVA源码学习-LinkedList
2016-01-20 00:00
579 查看
摘要: 很多公司的面试题都会问到ArrayList和LinkedList的区别,在这里我先稍微做下总结。他们都是List的实现类,实现方式上ArrayList是用数组,LinkedList是用双向链表,性能上ArrayList随机访问效率高,但插入和删除效率低,而LinkedList随机访问效率低,但插入和删除效率高,这样的性能又决定了他们的使用场景不同。那么是什么导致这哥俩性能那么互补呢?希望这篇对LinkedList的源码解读能提供这个问题的答案。
很多公司的面试题都会问到ArrayList和LinkedList的区别,在这里我先稍微做下总结。他们都是List的实现类,实现方式上ArrayList是用数组,LinkedList是用双向链表,性能上ArrayList随机访问效率高,但插入和删除效率低,而LinkedList随机访问效率低,但插入和删除效率高,这样的性能又决定了他们的使用场景不同。那么是什么导致这哥俩性能那么互补呢?希望这篇对LinkedList的源码解读能提供这个问题的答案。
按惯例先上LinkedList类的定义:
跟ArrayList继承自AbstractList不同,LinkedList继承自AbstractSequentiaList,与ArrayList一样都实现了List,Cloneable和Serializable接口,都可以克隆并序列化,不一样的是实现了Deque接口,这个接口定义了跟双端队列相关的操作。
LinkedList通过链表实现,那么肯定以一个链表节点作为基础数据单元,跟ArrayList的数组不一样,这个链表节点的定义如下:
每个节点都保存了存储的元素,前一个节点,后一个节点,即只能看到跟自己相邻的节点。
LinkedList之中定义了三个属性:
size属性记录了LinkedList中存储的元素个数,first和last分别是LinkedList的头节点和尾节点。
LinkedList的构造方法有两个
ArrayList中的无参构造方法(默认构造函数)还默认初始化一个长度为10的数组,LinkedList中的无参构造方法啥也没有,这该如何理解?从这里看,用无参构造函数初始化的时候貌似什么也没做,其实不然,仔细看上面提到的Node类,它通过静态加载方式声明,即在工程加载时变初始化了Node类,并包含了节点,前驱,后继三个属性;在初始化LinkedList时,又将这三个属性赋给first和last,并且都为null,即得到了一个空链表。构建对象较完整的顺序是:
先在工程加载时初始化静态代码(Node类),new LinkedList()时,先执行父类的非静态代码初始化父类成员变量,然后执行父类构造方法,接着执行子类成员变量,最后才执行子类构造方法,继而完成了对象的创建。
带参数的构造方法里有一个addAll(Collection), 即将Collection转换为LinkedList所能接受的数据(节点),将这些节点追加到linkedList中。
addAll(int, Collection),重载addAll(Collection),在指定位置插入Collection到linkedList中
构造方法调用的addAll最终会将size作为index参数调用addAll(int index, Collection<? extends E> c),怎么实现的等看懂了以后再写。
接下来就是几个比较重要的私有方法,LinkedList的大多数公有方法里都会出现他们的身影。
linkFirst(E e),将e插入为第一个元素
这里我将first,last理解为C语言里的指针,将first指向的元素复制给Node f,声明一个newNode节点,使其前驱为null,节点元素为e,后继为f(此时的f为原来链表里第一个元素),此时已经完成了头结点->newNode->f的链接,在将first的指针位置更新到newNode,是newNode成为链表第一个元素。如果链表第一个元素为null,即空链表,则将last也指向newNode;否则,使f的前驱指向newNode,建立从f-》newNode的链接。同时将LinkedList的size加一。
linkLast(E e),将e插入为最后一个元素
过程跟linkFirst类似。先将LinkedList最后一个元素拷贝至l,初始化一个newNode使其前驱为l,元素为e,后继为null,此时完成从newNode->l的链接;更新last指针为newNode;如果原LinkedList最后一个节点为null(即为空链表),将first也指向newNode;否则,将原最后一个节点的后继指向newNode,完成l->newNode的链接,同时将LinkedList的size加一。
linkBefore(E e, Node<E> succ),插入一个元素到succ之前,如果succ的前置为null(即succ为第一个节点),则同linkFirst方法。
流程是:先将succ的前驱元素复制给pred,初始化newNode节点使其前驱为pred,元素为e,后继为succ,此时建立了从newNode->succ的前置元素pred,newNode到succ的链接;建立succ->newNode的链接;如果succ的前置元素为null,则令newNode为一个元素;否则令succ前置元素的后继指向newNode,建立pred->newNode的链接,同时将LinkedList的size加一。
为什么没有类似linkAfter的方法,个人理解这跟linkBefore功能上重合,ORACLE的程序猿们懒得写~
以上linkFirst,linkLast,linkBefore可视作LinkedList的插入方法,过程总结如下:
先找出待插入位置的元素,另存副本A;新建node节点B与A链接,并调整收尾指针first和last,最后size加1。按照这个思路在比较这三个方法,发现linkFirst对应了linkBefore的一种特殊情况,即linkBefore的succ元素的前驱为null,即succ为链表第一个元素。
接下来我们看一组unlink方法,unlink方法顾名思义就是将LinkedList中的某个元素移除,一般思路是先将这个元素的左右链接断开,然后将这个元素的前驱后继元素链接上。
unlinkFirst(Node<E> f)
既然是移除linkedList的第一个元素,那么这个方法在供其他方法使用的时候,f肯定会是first指针指向的元素。
备份第一个元素f的item和后继元素,然后将f的item和后继指针指向空,即从该链表中断开链接。将first指针指向原有链表第二个元素,即f的后继。如果f的后继也为null,那说明移除后成了空链表,尾指针last也指向null,否则将f的后继元素的前驱指向null,同时size减一,返回删除节点的元素值。
unlinkLast(Node<E> l)
类似的,unlinkLast的入参l肯定会是last指向的尾节点
备份尾节点的元素值及其前驱节点,然后将l的item和前驱指针指向空,让此节点从LinkedList中断开,将last指向l的前驱节点。如果前驱节点为空,说明移除后成了空链表,则让first也指向null,否则l前置节点的后继指向空,同时将size减一。
unlink(Node<E> x):删除LinkedList中任意非空节点x
先备份x的元素值,前驱节点和后继节点;如果x的前驱为null(即x为链表第一个节点),则让first指向x的后继节点,否则x前驱节点的后继指向x的后继节点,并将x的前驱断开;如果x的后继节点为null(即x为链表最后一个元素),则让last指向x的前驱节点,否则x的后继节点的前驱指向x的前驱节点,并将x的后继断开;同时将x的元素值置空,size减一并返回移除的节点元素值。
从上面的插入删除方法可以看到,链表的插入删除操作是比ArrayList效率要高的。那为什么链表的查找效率比ArrayList低,那是因为ArrayList支持随机访问而链表不能,那链表是如何查找的呢?看下面的方法:
node(int index)
根据提供的元素序号index,使用二分法在链表中循环遍历对应的元素,因为链表是无序集合,只能按照每个节点的后继去挨个查找。就是说每次对LinkedList做查找,排除最好的情况,LinkedList都得或多或少把自个元素挨个点个到,这种查找方式由链表的特性决定,实属无奈之举,所以效率没ArrayList高。
理解了上面几个方法,那么下面的方法就容易多了,无非是对以上方法的封装,增加一些校验而已,这里就简写了。
getFirst(),getLast():返回链表第一个元素值和最后一个元素值,不是节点。
removeFirst(),removeLast():分别调用unlinkFirst和unlinkLast移除第一个和最后一个节点,返回节点值。
addFirst(E),addLast(E):分别调用linkFirst和linkLast增加第一个节点和最后一个节点,返回节点值。
contains(Object o):分别调用indexOf(o)对LinkedList进行遍历查找对应的元素o,没有用到node(int index)方法。
add(E):调用linkFirst第一个节点,返回true。
remove(Object):遍历LinkedList,找到Object对应的节点,使用unlink方法移除,移除成功返回true,没有该元素则返回false。
等等,分别实现由List的增删查方法,队列的增删查方法和双端队列的增删查方法,太多了,这里不一一列举了。
很多公司的面试题都会问到ArrayList和LinkedList的区别,在这里我先稍微做下总结。他们都是List的实现类,实现方式上ArrayList是用数组,LinkedList是用双向链表,性能上ArrayList随机访问效率高,但插入和删除效率低,而LinkedList随机访问效率低,但插入和删除效率高,这样的性能又决定了他们的使用场景不同。那么是什么导致这哥俩性能那么互补呢?希望这篇对LinkedList的源码解读能提供这个问题的答案。
按惯例先上LinkedList类的定义:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
跟ArrayList继承自AbstractList不同,LinkedList继承自AbstractSequentiaList,与ArrayList一样都实现了List,Cloneable和Serializable接口,都可以克隆并序列化,不一样的是实现了Deque接口,这个接口定义了跟双端队列相关的操作。
LinkedList通过链表实现,那么肯定以一个链表节点作为基础数据单元,跟ArrayList的数组不一样,这个链表节点的定义如下:
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; } }
每个节点都保存了存储的元素,前一个节点,后一个节点,即只能看到跟自己相邻的节点。
LinkedList之中定义了三个属性:
transient int size = 0; /** * Pointer to first node. * Invariant: (first == null && last == null) || * (first.prev == null && first.item != null) */ transient&n 7fe0 bsp;Node<E> first; /** * Pointer to last node. * Invariant: (first == null && last == null) || * (last.next == null && last.item != null) */ transient Node<E> last;
size属性记录了LinkedList中存储的元素个数,first和last分别是LinkedList的头节点和尾节点。
LinkedList的构造方法有两个
/** * Constructs an empty list. */ public LinkedList() { } /** * Constructs a list containing the elements of the specified * collection, in the order they are returned by the collection's * iterator. * * @param c the collection whose elements are to be placed into this list * @throws NullPointerException if the specified collection is null */ public LinkedList(Collection<? extends E> c) { this(); addAll(c); }
ArrayList中的无参构造方法(默认构造函数)还默认初始化一个长度为10的数组,LinkedList中的无参构造方法啥也没有,这该如何理解?从这里看,用无参构造函数初始化的时候貌似什么也没做,其实不然,仔细看上面提到的Node类,它通过静态加载方式声明,即在工程加载时变初始化了Node类,并包含了节点,前驱,后继三个属性;在初始化LinkedList时,又将这三个属性赋给first和last,并且都为null,即得到了一个空链表。构建对象较完整的顺序是:
先在工程加载时初始化静态代码(Node类),new LinkedList()时,先执行父类的非静态代码初始化父类成员变量,然后执行父类构造方法,接着执行子类成员变量,最后才执行子类构造方法,继而完成了对象的创建。
带参数的构造方法里有一个addAll(Collection), 即将Collection转换为LinkedList所能接受的数据(节点),将这些节点追加到linkedList中。
addAll(int, Collection),重载addAll(Collection),在指定位置插入Collection到linkedList中
public boolean addAll(Collection<? extends E> c) { return addAll(size, c); } public boolean addAll(int index, Collection<? extends E> c) { checkPositionIndex(index);//如果index小于0且大于size,则抛出IndexOutOfBoundsException异常。 Object[] a = c.toArray();//将Collection转换为对象数组 int numNew = a.length; if (numNew == 0)//如果待插入对象为空,则直接返回。这里为什么不直接使用a!=null?原因我的理解是执行效率问题和不容易出错 return false; Node<E> pred, succ;//根据index查找对应节点信息,以确定Collection的始末位置。pred为起始位置,succ为结尾位置,可将其看作两个指针。 if (index == size) {//如果index==size,即在链表末尾添加元素,则Collection的起始位置为链表的last,结尾位置为null succ = null; pred = last; } else {//否则,结尾位置为index对应的那个节点succ,起始位置为succ的前驱节点。 succ = node(index); pred = succ.prev; } for (Object o : a) {//对Collection中每一个元素创建节点newNode,使其前驱为pred,后继null @SuppressWarnings("unchecked") E e = (E) o; Node<E> newNode = new Node<>(pred, e, null); if (pred == null)//如果pred==null,则说明为第一个元素,first指向newNode first = newNode; else //否则,起始位置pred的后继节点为newNode pred.next = newNode; pred = newNode;,//变更新的起始位置为newNode }//该for循环执行之后会将确定好的起始位置的节点,Collection中每个元素创建的节点连接起来,此时pred存放着所有Collection添加完成后的末尾位置 if (succ == null) {//如果结尾位置(即index对应的那个节点)为null,说明是在链表结尾添加,则将last指针指向succ的前置 last = pred; } else { //否则将所有Collection添加完成后的末尾位置pred的后继指向index对应的节点,index节点的前驱指向pred pred.next = succ; succ.prev = pred; } size += numNew;//链表长度增加Collection的长度 modCount++; return true; }
构造方法调用的addAll最终会将size作为index参数调用addAll(int index, Collection<? extends E> c),怎么实现的等看懂了以后再写。
接下来就是几个比较重要的私有方法,LinkedList的大多数公有方法里都会出现他们的身影。
linkFirst(E e),将e插入为第一个元素
/** * 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++; }
这里我将first,last理解为C语言里的指针,将first指向的元素复制给Node f,声明一个newNode节点,使其前驱为null,节点元素为e,后继为f(此时的f为原来链表里第一个元素),此时已经完成了头结点->newNode->f的链接,在将first的指针位置更新到newNode,是newNode成为链表第一个元素。如果链表第一个元素为null,即空链表,则将last也指向newNode;否则,使f的前驱指向newNode,建立从f-》newNode的链接。同时将LinkedList的size加一。
linkLast(E e),将e插入为最后一个元素
/** * 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++; }
过程跟linkFirst类似。先将LinkedList最后一个元素拷贝至l,初始化一个newNode使其前驱为l,元素为e,后继为null,此时完成从newNode->l的链接;更新last指针为newNode;如果原LinkedList最后一个节点为null(即为空链表),将first也指向newNode;否则,将原最后一个节点的后继指向newNode,完成l->newNode的链接,同时将LinkedList的size加一。
linkBefore(E e, Node<E> succ),插入一个元素到succ之前,如果succ的前置为null(即succ为第一个节点),则同linkFirst方法。
/** * 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++; }
流程是:先将succ的前驱元素复制给pred,初始化newNode节点使其前驱为pred,元素为e,后继为succ,此时建立了从newNode->succ的前置元素pred,newNode到succ的链接;建立succ->newNode的链接;如果succ的前置元素为null,则令newNode为一个元素;否则令succ前置元素的后继指向newNode,建立pred->newNode的链接,同时将LinkedList的size加一。
为什么没有类似linkAfter的方法,个人理解这跟linkBefore功能上重合,ORACLE的程序猿们懒得写~
以上linkFirst,linkLast,linkBefore可视作LinkedList的插入方法,过程总结如下:
先找出待插入位置的元素,另存副本A;新建node节点B与A链接,并调整收尾指针first和last,最后size加1。按照这个思路在比较这三个方法,发现linkFirst对应了linkBefore的一种特殊情况,即linkBefore的succ元素的前驱为null,即succ为链表第一个元素。
接下来我们看一组unlink方法,unlink方法顾名思义就是将LinkedList中的某个元素移除,一般思路是先将这个元素的左右链接断开,然后将这个元素的前驱后继元素链接上。
unlinkFirst(Node<E> f)
/** * 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; }
既然是移除linkedList的第一个元素,那么这个方法在供其他方法使用的时候,f肯定会是first指针指向的元素。
备份第一个元素f的item和后继元素,然后将f的item和后继指针指向空,即从该链表中断开链接。将first指针指向原有链表第二个元素,即f的后继。如果f的后继也为null,那说明移除后成了空链表,尾指针last也指向null,否则将f的后继元素的前驱指向null,同时size减一,返回删除节点的元素值。
unlinkLast(Node<E> l)
/** * 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; }
类似的,unlinkLast的入参l肯定会是last指向的尾节点
备份尾节点的元素值及其前驱节点,然后将l的item和前驱指针指向空,让此节点从LinkedList中断开,将last指向l的前驱节点。如果前驱节点为空,说明移除后成了空链表,则让first也指向null,否则l前置节点的后继指向空,同时将size减一。
unlink(Node<E> x):删除LinkedList中任意非空节点x
/** * 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; }
先备份x的元素值,前驱节点和后继节点;如果x的前驱为null(即x为链表第一个节点),则让first指向x的后继节点,否则x前驱节点的后继指向x的后继节点,并将x的前驱断开;如果x的后继节点为null(即x为链表最后一个元素),则让last指向x的前驱节点,否则x的后继节点的前驱指向x的前驱节点,并将x的后继断开;同时将x的元素值置空,size减一并返回移除的节点元素值。
从上面的插入删除方法可以看到,链表的插入删除操作是比ArrayList效率要高的。那为什么链表的查找效率比ArrayList低,那是因为ArrayList支持随机访问而链表不能,那链表是如何查找的呢?看下面的方法:
node(int index)
/** * Returns the (non-null) Node at the specified element index. */ Node<E> node(int index) { // assert isElementIndex(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; } }
根据提供的元素序号index,使用二分法在链表中循环遍历对应的元素,因为链表是无序集合,只能按照每个节点的后继去挨个查找。就是说每次对LinkedList做查找,排除最好的情况,LinkedList都得或多或少把自个元素挨个点个到,这种查找方式由链表的特性决定,实属无奈之举,所以效率没ArrayList高。
理解了上面几个方法,那么下面的方法就容易多了,无非是对以上方法的封装,增加一些校验而已,这里就简写了。
getFirst(),getLast():返回链表第一个元素值和最后一个元素值,不是节点。
removeFirst(),removeLast():分别调用unlinkFirst和unlinkLast移除第一个和最后一个节点,返回节点值。
addFirst(E),addLast(E):分别调用linkFirst和linkLast增加第一个节点和最后一个节点,返回节点值。
contains(Object o):分别调用indexOf(o)对LinkedList进行遍历查找对应的元素o,没有用到node(int index)方法。
add(E):调用linkFirst第一个节点,返回true。
remove(Object):遍历LinkedList,找到Object对应的节点,使用unlink方法移除,移除成功返回true,没有该元素则返回false。
等等,分别实现由List的增删查方法,队列的增删查方法和双端队列的增删查方法,太多了,这里不一一列举了。
相关文章推荐
- Java Integer值的比较需注意
- java常用夹包
- 学习代码,java包应用
- java中的Static class
- Java随机生成验证码
- 为新手准备的30个JAVA程序问题和好的练习
- 关于Java多态
- Java动态代理
- Myeclipse报错:Could not open the editor: Invalid thr
- Lucene:基于Java的全文检索引擎简介
- java视频教程之数据库连接池配置的两种方法
- spring SchedulerFactoryBean 没有创建 Scheduler的实现类bea
- springjdbc+mysql判断数据库以及数据库表是否存在
- 浅谈 JAVA中的IO流
- Java 连接池的工作原理
- Java反射讲解-实例(1)
- Java反射讲解-实例(2)
- 轻松搞定java高薪
- [Vert.x Core手册 for Java]-了解Vert.x
- springmvc配置