目录
(1)LinkedList概述
LinkedList底层是通过双向链表
实现的。除了可以作为链表,还可以当做栈、普通队列和双端队列来使用。
在早期,LinkedList底层是双向循环链表。但在java7、8时,改成了双向链表。即头结点的prev和尾结点的next都为null
。
不支持随机访问,支持克隆、序列化。(可以通过遍历来实现顺序访问,效率比较低)
线程不安全。
为什么不循环了
JDK 1.7的实现思想是,设置两个哨兵节点,分别指向头部和尾部,所以不用循环也能快速找到头尾节点,进行正向遍历或反向遍历。
(2)LinkedList特点
LinkedList底层数据结构是双向链表,查询慢,增删快
链式存储底层是用一个个节点(Node)链接而成的,每个节点都存储着一个对象值和下一个节点的位置(或上一个节点的位置)
(3)LinkedList特有方法
方法名 | 说明 |
---|---|
public void addFirst(E e) | 在该列表开头插入指定的元素 |
public void addLast(E e) | 将指定的元素追加到此列表的末尾 |
public E getFirst() | 返回此列表中的第一个元素 |
public E getLast() | 返回此列表中的最后一个元素 |
public E removeFirst() | 从此列表中删除并返回第一个元素 |
public E removeLast() | 从此列表中删除并返回最后一个元素 |
(4)ArrayList与LinkedList对比
Arraylist:
- 优点:ArrayList是实现了基于数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
- 缺点:因为地址连续, ArrayList要移动数据,所以插入和删除操作效率比较低。
LinkedList:
-
优点:LinkedList基于链表结构,地址是任意的,所以在开辟内存空间的时候不需要一个连续的地址,而且增删效率高,只需要移动指针
-
缺点:因为LinkedList要移动指针来挨个遍历元素,所以查询操作性能比较低。
虽然LinkedList作为链表,增删效率很高,但是需要遍历才能查找到要操作的元素,遍历的效率很低。
适用场景分析:
- 当需要对数据进行多次随机访问的情况下选用ArrayList
- 当需要对数据进行多次增加删除修改时采用LinkedList。
(5)LinkedList源码分析
LinkedList的亮点:
-
由于它允许存null值,也允许按值查找,所以查找时做了特殊判断,如果为null,使用==查找。如果不为null,使用equals查找
-
它提供了一个根据下标查找元素的方法,其实就是遍历链表时维护了一个计数器。
它做了优化:如果要查找的下标大于链表长度的一半,即更靠近尾部,就从末尾向前查找。如果更靠近头部,就从头部向后查找
-
其他的就是普通的链表实现
(1)成员变量
size:当前集合中存放元素的个数。链表不存在容量上限
first:头结点
last:尾结点
(2)构造函数
无参构造:
什么都没有做,平平无奇
有参构造:
指定一个集合来实例化LinkedList。调用了无参构造完成初始化,然后调用了addAll()方法,将原先集合的元素填充到新的LinkedList中。
(3)节点类
非常简洁。
item:元素值
next:后继节点
prev:前驱节点
有参构造:前驱结点、节点值、后继节点
(4)add()
在链表末尾添加元素
调用了linkLast()方法:
将指定元素实例化成一个新节点,添加在链表的末尾。这里也存在modCount,所以LinkedList也有迭代时的快速失败机制。
(5)remove()
删除链表的第一个元素
。
调用了removeFirst()方法:
如果链表为空,抛出异常:没有这个参数。否则调用unlinkFirst()方法:
先保留头结点的元素值和后继引用,然后将它们置null。
然后让第二个节点成为头结点。尾结点的后继节点是指向first的,所以不用发生变化。
如果此时头结点为空,说明链表为空,就将尾结点置null;否则将新的头结点的prev置null(原先指向原来的first头结点)。
此时原先的头结点三个属性全部为null,可以被GC回收。
最后返回被删除节点的元素值。
(6)remove(Object o)
按照元素值删除节点
和ArrayList的处理方式类似,先判断元素是否为null,如果为null,就用“==”匹配;如果不为null,就用“equals()”匹配
。
按顺序遍历节点,如果找到了匹配的元素,就调用unLink()方法删除它:
首先存储要删除节点的全部属性;
判断前驱结点是否为null。如果为null,说明要删除的是头结点;
判断后继节点是否为null。如果为null,说明要删除的是尾节点;
操作完成后,将节点的所有属性置为null,方便GC回收。
(7)contains(Object o)
调用indexOf(Object o)进行查找。它返回的是该元素的索引,如果没找到就返回-1。而contains方法做了加工,返回boolean值。
indexOf(Object o)方法,还是先判断元素是否为null,然后维护一个索引,遍历节点,找到元素就返回当时的索引,没找到就返回-1
这种随机查找也没有办法做优化,所以最坏情况会完整遍历一次整个链表,效率很低。
(8)get(int index)
根据“索引”查找元素。链表中不存在索引,所以需要在遍历的过程中手动维护一个索引。
先调用checkElementIndex()方法,判断索引是否合法:
如果合法,调用node(int index)方法,按照索引返回节点:
这里做了优化,使用二分查找法提高索引查找的效率
。
也很简单,判断索引的值和size的大小关系。如果索引小于size的一半,就从头节点开始遍历;如果索引大于size的一半,就从尾节点开始反向遍历。
这样,只需要遍历一半的链表,提升了一些查找速度
。这也得益于双向链表的设计。