文章目录
前言
说到集合,Java中所有集合的总接口时Collection,Collection规范了集合基本的操作方法,同时约束了所有集合都存在泛型约束。下面是集合的组织架构:
interface Collection<E>
interface List<E> extends Collection<E>
class ArrayList<E> implements List<E>
class LinkedList<E> implements List<E>
class Vector<E> implements List<E>
interface Set<E> extends Collection<E>
class TreeSet<E> implements Set<E>
class HashSet<E> implements Set<E>
今天我们要说的是LinkedList集合,LinkedList集合是一种链表类型的数据结构,在创建LinkedList节点时,jvm会在内存中找到一个足够的空间存储,一个节点中包含三部分内容:前趋节点,本节点内的值,后继节点。前趋节点保存的是前面一个节点的地址,后继节点保存的是后面一个节点的地址 。通过前趋节点和后继节点组成了一个双向链表(双向链表指的是可以从首节点遍历到尾节点,也可以从尾节点遍历到首节点,我以前一直以为双向链表指的是链表的首尾是相连的)。
双向链表的特征是:增删快,查询慢,并且理论上存储数量无限制。在内存方面,链表不需要直接申请连续的空间,而是每当创建一个节点再申请一个节点的空间,相比于ArrayList对内存的压力小。
下面我们来分析一下LinkedList源码中的几个重要方法来理解LinkedList。
LinkedList重要源码分析
内部私有类Node
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有一个内部私有类Node,这个类就是Linkedlist双向链表的节点,每添加一个数据就会创建一个节点。这个节点由三部分组成:节点内元素,后继节点,前驱节点。
linkFirst(E e)方法l
在linkFirst方法中有first和last变量,是这样定义的
transient Node<E> first;
transient Node<E> last;
这是双向链表的成员变量,first为链表的头节点(始终指向链表的第一个节点),last为链表的尾节点(始终指向节点的最后一个节点)
当链表为空时,first和last都为null;
当链表不为空时,first的前驱节点为空,元素内容必定不为空,last后继节点必定为空。
下面是linkedList方法源码(注释为我自己注释的,源码中没有)
private void linkFirst(E e) {
// 创建f保存头节点
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++;
}
linkFirst方法是创建一个新的节点插入到头节点之前,这个节点的后继节点为插入前的头节点。在插入完成后更新头节点指向(如果之前双向链表为空,同时更新尾节点指向)。
linkLast(E e)方法
void linkLast(E e) {
// 创建保存尾节点的l节点
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++;
}
linkLast方法是创建一个新的节点到尾节点之后,该节点的前趋节点为插入前的尾节点。在插入完成后更新尾节点指向(如果链表在插入前为空,同时更新头节点)。
linkBefore(E e, Node succ)方法
void linkBefore(E e, Node<E> succ) {
// 创建pred节点保存指定节点的前趋节点
// 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++;
}
linkBefore方法是在指定节点前面插入一个节点。
在链表中插入一个元素就是修改要插入位置前后两节点的指向。如图,要在指定节点前插入新的元素,就要修改指定节点的前趋节点的后继,和指定节点的前趋。
在源码中创建新节点时分别初始化了新节点的前趋和后继,即线1, 2。
然后通过 succ.prev = newNode; 修改了指定节点的前趋。即线3。
最后判断指定节点是否为头指针(判断指定节点前趋节点是否为空)来修改指定节点前趋节点的后继,即线4。
node(int 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;
}
}
node方法是查询指定下标位置的元素。因为LinkedList查询是根据前趋后继查询,所以查询速度较慢。
这里采用折半查找将链表根据有效节点个数从中间分开,前半部分从前往后遍历,后半部分从后往前遍历。这样能够节省查询时间。
以上是我对LinkedList几个关键方法的分析,如有错误,希望能够指出,让我得以查漏补缺,谢谢。