注意
本系列文章是笔者在参考了大量优秀文章的基础上的理解,由于水平有限,难免会出现错误,望批评指正,同时我也会把自己最新的理解及时更新到文章中去。最后,我会在文中注明参考文章,特别感谢这些无私奉献的作者。
在开始研究 LinkedList 之前,强烈建议先阅读廖雪峰老师的下面三篇文章:
1、使用Queue
2、使用Deque
3、使用Stack
1 初始化
LinkList 的属性很少,就三个。
1 | transient int size = 0; |
size 就是当前的元素个数,first、last分别指头尾元素。为什么要分别定义头尾节点呢,这个问题我们先放着,文章结尾有总结。
下面是它的构造方法。
1 | public LinkedList() { |
无参数的方法什么也没做,有参数的调用了addAll方法,并最终调用了addAll(int index, Collection<? extends E> c) ,此时 index==0 。
1 | public boolean addAll(int index, Collection<? extends E> c) { |
从代码看出,这里就是一个依据提供的集合构造双向链表的过程。
2 add(E e)
1 | public boolean add(E e) { |
代码逻辑在注释中已经标出了。
对于 LinkedList 的其它增删操作,原理差不多,这里不再赘述。
3 get(int index)
1 | public E get(int index) { |
node(int index)方法的作用是依据传入的 index 找到对应的 Node 元素。查找的逻辑很巧妙,通过判断 index 处在 LinkedList 的前部分还是后半部分来决定遍历的方向,这有助于减少遍历的次数。
4 set(int index, E element)
1 | public E set(int index, E element) { |
代码结构很清晰,通过 node(int index) 找到对应的 node 替换即可。node(int index) 方法在上面讲过了,这里不再赘述。
5 遍历
同ArrayList一样,LinkedList 也有两种遍历方式。
方式一:
1 | for (int i=0;i<size;i++){ |
方式二:
1 | //LinkedList 的 iterator() 和 listIterator() 返回的对象是一样的。 |
对于方式二,listIterator 是一个 ListItr 对象,在讲 ArrayList 时我们已经介绍过该迭代器了,这里的逻辑和之前的一致,如:fast-fail机制、增删改操作逻辑等。只不过之前是通过外部类 Arraylist 操作数组,现在是通过外部类 LinkedList 操作双向链表。
6 其它操作
从 LinkedList 的继承关系图我们知道,它还实现了 Deque 接口,而 Deque 又继承了 Queue,所以可以用 LinkedList 实现队列\双向队列\栈的功能。
比如,我们要实现一个队列操作:
1 | Queue<String> queue = new LinkedList<>(); |
要实现双向队列、栈的操作:
1 | Deque<String> deque = new LinkedList<>(); |
注意,双向队列和栈的操作都是通过 Deque 来实现的,在使用时分别调用他们各自的方法即可(虽然可以混用,但还是建议按照规范来使用),比如当我们把Deque作为Stack使用时,注意只调用push()/pop()/peek()方法,不要调用addFirst()/removeFirst()/peekFirst()方法,这样代码更加清晰。
7 总结
再来看文章刚开始的问题:为什么要分别定义头尾节点呢?这里有两个原因:
一、查询时可依据 index 在链表中的位置来决定遍历的方向,知道了头尾节点,按照不同的方向遍历也就方便了,提升了查找效率。
二、为 Queue 和 Deque 的实现提供便利。
通过这篇文章的研究,我们知道了LinkedList是基于双向链表实现的,没有扩容操作。同时,Linkedlist 也为 Queue 和 Deque 的实现提供了便利。LinkedList 不适合查询、修改频繁的场景,它适合插入、删除操作频繁的场景(操作指定位置时,需要遍历),而 ArrayList 则适合查询、修改频繁,插入、删除不频繁的场景。