LinkedList源码分析(超详细)
Posted 算不出来没办法
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LinkedList源码分析(超详细)相关的知识,希望对你有一定的参考价值。
目录
addFirst(E e) 和 addLast(E e) 方法
addAll(int index, Collection c) 方法
问:LinkedList 中的 add() 方法采用的是头插法还是尾插法?效率如何?
问:LinkedList 采用的是链表来实现,那其 get(int index) 方法是如何实现查找数据的呢?
LinkedList简介
LinkedList 底层基于双向链表实现,所以其插入和删除的效率高,但也正因为使用链表实现,所以其查找元素的效率比较低。又因为链表在内存中存放的地址是不连续的,所以其遍历效率没有ArrayList 高。当我们需要频繁的插入和删除元素,而较少查询元素时,可以考虑使用 LinkedList 。如果你对链表的操作很熟练的话,那么你将会很容易的理解 LinkedList 的实现原理。
LinkedList 还实现了 Deque 接口,所以可当作栈或队列来使用。
在这里附上 ArrayList 的源码分析链接。ArrayList源码分析
成员变量
作用:list中数据的个数。
作用:双向链表的头结点。
作用:双向链表的尾节点。
作用:作用:记录对 List 操作的次数,主要使用是在 Iterator,是防止在迭代的过程中集合被修改。该变量定义在AbstractList中,被ArrayList继承。思考一下,如果我们在遍历集合时,对集合进行了修改,比如说删除了某一个元素,那么很容易导致结果出错或者下标越界。
内部节点类
LinkedList 内部使用泛型设计了一个节点类,其存放了当前数据、前一个节点和后一个节点的信息。
构造函数
LinkedList 有两个构造函数,其中一个为空构造,另一个则可传入集合。可以发现,传入集合的构造方法其实是先调用空构造,然后使用 addAll 方法将集合的数据添加进链表中。对于 addAll 方法的分析,接下来会讲到。
增加元素
LinkedList 较为常用的添加元素的方法如下,接下来我们将一一讲解。
add(E e) 方法
add 方法调用了 linkLast 方法,该方法是将元素添加到双向链表的尾部。
linkLast 方法其实很简单,就是将数据插入到链表的尾部(先获取尾部节点 last,然后创建一个节点类存放新的数据,将其前一个节点设置为 last,由于该节点是放置在尾部,所以后一个节点直接设置为 null 即可。然后将 last 指向新创建的节点,这样就插入了一个节点)。值得注意的是,如果这是第一个插入的元素,即 l = last = null,那么这个节点既是尾节点,也是头节点。
add(int index, E element) 方法
该方法首先检查数组插入位置是否越界,若越界则抛出异常。接下来的工作就很简单了,判断插入的位置是否是尾部,如果是尾部直接调用 linkLast 方法,若不是则调用 linkBefore 方法。而 linkLast 方法已经在上文分析过了。 值得注意的是,对于插入位置是头结点的情况,LinkBefore 方法中进行了判断。
这里我们先来看 node(index) 方法,该方法是找到插入位置的元素。可以很清楚的看到,该方法对index 进行了比较,如果小于 size 的一半(size >> 1 右移一位,相当于除2),那么是从头往后查找元素,否则从尾部往前查找元素。然后返回这个元素。
linkBefore 方法的代码也很好理解,插入数据到找到的元素的前一个位置。这里还是得判断 pred 是否为空,如果为空,那么插入的位置就是第一个位置。
addFirst(E e) 和 addLast(E e) 方法
这两个方法一个插入到头部,一个插入到尾部。代码很容易理解这里就不赘述了。其代码如下:
addAll(int index, Collection<? extends E> c) 方法
这里我们先来看这一个方法,其全部代码如下,大致思路可分为三步:
第一步:首先判断开始插入的位置是否越界,然后判断插入的集合长度是否为 0 ,如果为 0 ,直接返回。若不为 0,找到插入位置的节点。(node 方法上文已经分析过了)
第二步:循环插入节点,链表的基础操作。
第三步:插入完成之后将节点进行连接。
上述三步图例如下:
到这里这个方法的思路就很清晰了,接下来我们再看 addAll(Collection<? extends E> c) 方法。
addAll(Collection<? extends E> c) 方法
该方法实际上调用的还是 addAll(int index, Collection<? extends E> c) 方法,只是传入的 index 为 size,表示插入到最后。
删除元素
LinkedList 的 remove 方法如下,这里只选取几个来分析,其他的方法思路和这几个方法类似,重点是对链表的操作,只要对链表十分的熟悉,那么 LinkedList 实现思路将会变得十分的简单。
removeFirst() 方法
移除第一个元素,这里将 item 和 next 置空是让垃圾回收器回收这部分内存。而 removeLast 方法的代码和 removeFirst 的代码十分相似,这里就不赘述了。
remove() 方法
其内部实现就是调用 removeFirst() 方法来移除第一个元素。
remove(Object o) 方法
如果查看过 ArrayList.remove(Object o) 的源码,会发现这两个方法极其相似。都是遍历数组查找到要删除的元素进行删除。而通过这里对空值的判断,可以推断出 LinkedList 也是支持存放空值的。
其中的 unlink 代码如下,删除时会判断这个节点是否是头尾节点,头尾节点特殊处理,其他位置的节点正常处理。
E unlink(Node<E> x)
// assert x != null;
final E element = x.item;
final LinkedList.Node<E> next = x.next;
final LinkedList.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;
remove(int index) 方法
看到这里,remove(int index) 方法的代码思路就很清晰了,node 方法获取要删除位置的节点,unlink 删除这个节点。(其中的 node 方法和 unlink 方法上文都已经分析过)
获取元素
对于 LinkedList 而言,其提供了如下三个方法来获取元素。
因为 LinkedList 中定义了头节点和尾节点,所以其中 getFirst 和 getLast 方法就是直接返回头结点和尾节点,时间复杂度为O(1),其代码如下:
而对于 get(int index) 方法而言,其需要遍历数组来获取对应下标位置的信息,所以其查找效率比较低。
有关栈和队列的方法
由于 LinkedList 实现了 Deque 接口,所以其中也包含了有关栈和队列的操作方法。其原理基本上都依托于上面所介绍到的方法。下面给出常用的操作,其余操作实现
常见问题
问:LinkedList 和 ArrayList 的区别?
答:①ArrayList 和 LinkList 都是线程不安全的。
②ArrayList 其底层用数组实现所以查找元素速度快,但新增和删除由于要在数组中移动元素,所以效率低。而 LinkedList 的查找元素速度慢,但新增和删除速度快。
③ArrayList需要一份连续的内存空间,LinkedList不需要连续的内存空间,但是 LinkedList 的遍历效率没有 ArrayList 高。
问:LinkedList 底层是用什么数据结构实现的?
答:是用双向链表实现的,node 节点类中分别定义了当前节点的数据、前一个节点和后一个节点。
问:LinkedList 中的 add() 方法采用的是头插法还是尾插法?效率如何?
答:add() 方法采用的是尾插法,由于 LinkedList 中定义了尾节点来保存当前链表的尾节点,所以其插入时间复杂度为O(1),效率高。
问:LinkedList 采用的是链表来实现,那其 get(int index) 方法是如何实现查找数据的呢?
答:其查找数据主要依托于 node 方法,接收下标位置,判断下标与当前链表的长度关系来选择是从头部开始遍历还是尾部开始遍历。
以上是关于LinkedList源码分析(超详细)的主要内容,如果未能解决你的问题,请参考以下文章