前言
上一篇我们介绍了 ArrayList
,这次,我们再看看一下它的兄弟:LinkedList
。
LinkedList
同样也实现了 List
接口,底层原理是双向链表,那么它又是如何实现的呢?继续来看吧。
源码分析
成员变量
LinkedList
只有三个成员变量:
1 | transient int size = 0; |
size 属性不用说,肯定是表示链表的逻辑长度,first 应该是链表的第一个元素,last 表示最后一个元素。
构造方法
先来看无参构造:
1 | public LinkedList() { |
无参构造没有任何逻辑,那么再来看看其他的构造方法:
1 | public LinkedList(Collection<? extends E> c) { |
这里牵扯到要给 addAll
方法,一会在常用方法里我们会讲到,这里先放一放。
这次我们带上内存图来分析,会更直观一些,首先用无参构造来创建一个对象:
1 | List<Student> list = new LinkedList<>(); |
注:为了节省篇幅,本图省略了一些细节上东西,如常量池,方法区等内容。
常用方法
add
首先是 add
方法:
1 | public boolean add(E e) { |
1 | void linkLast(E e) { |
这里提到了 Node 类,来看看它的定义:
1 | private static class Node<E> { |
原来 Node
类是 LinkedList
的静态内部类,表示链表的一个节点。
那么当我们执行这段代码后,会发送什么呢?
1 | list.add(new Student("张三", 20)); |
那我们再添加一个元素呢?
1 | list.add(new Student("李四", 21)); |
可能看起来有写复杂,其实也不难理解,耐下心对照源码好好看一下,应该就能理解这张图的意思了。
我们再添加两个元素看看效果。
1 | list.add(new Student("王五", 22)); |
remove
添加了这么多,我们删除一个试试,先来看看源码:
1 | // 根据索引删除 |
1 | private void checkElementIndex(int index) { |
1 | E unlink(Node<E> x) { |
可能这里的这 10 和 17 行不太容易理解:
1 | prev.next = next; // 将前驱节点的 next 指向要删除节点的后继节点 |
举个简单的例子:
张三 <==> 李四 <==> 王五
那么要删除李四的话,应该要将张三的 “下一个” 指向王五吧,也就是 prev.next = next;
。同样,应为是双向链表,所以也应该让王五的 “上一个” 指向张三,即 next.prev = prev;
。
这里讲解的是根据索引删除,还有根据元素删除,其实原理是一样的,主要是 unlink
这个方法,先根据传入的参数,找到要删除的元素,然后进行 unlink
方法的逻辑即可,这里就不再展开,如果你看懂了根据索引删除,相信你也能理解根据元素删除。
但是需要注意的是:如果删除的是引用数据类型的话,需要重写 equals 方法,不然可能会无法进行删除操作哦。
其实仔细想想也能理解,既然需要 找到要删除的元素,那么如何判断传入的参数和要删除的是同一个呢?只有 equals
方法了,而默认从 Object
继承的 equals
方法可不一定能满足我们的需求,因为它只比较地址值,所以我们需要重写 equals
方法。
get
1 | public E get(int index) { |
1 | Node<E> node(int index) { |
这里用了一个很巧妙的小算法,既然我们知道链表的长度,那么当要删除的元素 索引 < 长度 / 2
,就从第一个开始找,反之从最后一个开始找,长度 / 2
可以改写为位运算即:size >> 1
,效率更高一些。
先讲这么多,如果你看懂了这些,相信 LinkedList
的其他方法,你也能够轻松的理解。
总结
根据上方的源码分析,我们可以总结出 LinkedList
的一些特性:
LinkedList
底层数据结构是双向链表。- 不能对元素进行随机访问,虽然提供了 get 方法,但这个方法是通过遍历来实现的。
- 删除、添加元素的效率很高,但查找元素的的效率较差。