JAVA源码学习-LinkedList
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了JAVA源码学习-LinkedList,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含12481字,纯文字阅读大概需要18分钟。
内容图文
![JAVA源码学习-LinkedList](/upload/InfoBanner/zyjiaocheng/716/cf86757c8d7a41048ddd949278332712.jpg)
很多公司的面试题都会问到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?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的增删查方法,队列的增删查方法和双端队列的增删查方法,太多了,这里不一一列举了。
转载于:https://my.oschina.net/u/2610176/blog/605859
内容总结
以上是互联网集市为您收集整理的JAVA源码学习-LinkedList全部内容,希望文章能够帮你解决JAVA源码学习-LinkedList所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。