Java学习——集合(2)LinkedList 详解 —— 悟道
节点
我们知道,LinkedList 和 ArrayList 在使用方式上,并无多大区别。但是为什么就要区别成两种类型呢?上一节我们介绍了 ArrayList,其底层是数组。数组在 Java 中长度是不可变的,这就带来了一个问题。如果需要频繁增删较多或大量的元素,在集合是 ArrayList 时,数组这个定长因素,会导致整个过程效率会非常低下,因为要频繁创建新数组、销毁旧数组,将元素复制到新数组中。所以,在面临这样的问题下,LinkedList 孕育而生。由于 LinkedList 为了提高增删效率底层不再使用数组而是使用链表这种结构。所以,我们先来了解链表。学习链表首先要学习组成链表的元素 Node,也叫节点。
// Node源码
transient Node<E> first;
transient Node<E> last;
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 的源码。在 Node 中,主要分为两个域,数据域和指针域。数据域意思是指 item 引用指向集合中的具体元素,而指针域意思是 next, prev 指向下一个或上一个 Node。实质是对集合中的元素做了一层封装,这个封装就是 Node,Node 中的成员变量可以指向其他 Node。从而实现 Node 之间的连接,也就实现了链表这种结构。当然,链表知识点很多,这里只是介绍基础。其他这里暂不研究。好了,知道了链表是什么我们接着往下。
创建 LinkedList
// LinkedList 的构造函数
public LinkedList() {
}
// ArrayList 的构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
不同于 ArrayList 的创建,LinkedList 创建时不需要其他操作。另外,LinkedList 也没有初始化容量的构造方法。
添加元素
// 添加相关源码
public boolean add(E e) {
linkLast(e);
return true;
}
public void addFirst(E e) {
linkFirst(e);
}
public void addLast(E e) {
linkLast(e);
}
void linkLast(E e) {
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++;
}
private void linkFirst(E e) {
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++;
}
可以看到,向 LinkedList 末尾添加元素非常简单,获取到末尾节点并新建一个节点,如果末尾节点是 null 则让 first 指针指向新节点。如果不是, 则让末尾节点的 next 指针指向新节点。同时,我们同样看到了 modCount 成员变量,我们在 ArrayList 中解释了它,并解析了它会带来的问题。同理,向头部添加元素相反过来就可以了。具体过程如下:
第一次添加元素时
- 初始 last,first 都为null,所以,l = last = null
- 创建新节点 newNode(A),newNode(A).prev = l = null,newNode(A).next = null,newNode(A).item = 'A'
- last 指向 newNode(A,由于 l == null,first 指向 newNode(A)
第二次添加元素时
- l = last = node(A)
- 创建新节点 newNode(B),newNode(B).prev = l = node(A),newNode(B).next = null,newNode(B).item = 'B'
- last 指向 newNode(B),由于 l != null 并且指向 node(A),所以 node(A).next 指向 newNode(B)
第三次添加元素时
- l = last = node(B)
- 创建新节点 newNode(C),newNode(C).prev = l = node(B),newNode(C).next = null,newNode(C).item = 'C'
- last 指向 newNode(C),由于 l != null 并且指向 node(B),所以 node(B).next 指向 newNode(C)
删除元素
LinkedList 的 remove 方法逻辑与 ArrayList 基本一致,但在具体删除上,由于底层实现不一致区别较大。
// 删除元素方法
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final 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;
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
在 LinkedList 中,核心是通过 unlink 方法去删除元素的。见名知意,删除链接关系就可以删除掉元素。我们来一起看看过程是如何的。
- remove 方法中跟 ArrayList 中的 remove 方法一样,判断删除元素是否为 null 来执行不同的代码。但元素节点都是通过 unlink 方法删除。
- 在unlink 方法中,根据获取到的需要删除节点的 x,通过 prev == null 判断 x 是否是头结点。是的话,让 first 指针指向 x 的下一个节点。不是的话,则让 x 的前节点(prev.next)指向 x 的下一个节点。不用管下一个节点是否为空。并且将 x 的 prev 指针置 null(x.prev = null)。
- 同理,又通过 next == null 判断了 x 是否为尾节点。是的话,则让 last 指针指向 x 的前一个节点。不是的话,则让下一个节点的前指针(next.prev) 指向 x 的前一个节点, 并且将 x 的 next 指针置 null(x.next = null)。
- 最后将 x 节点的数据域置 null
更新元素
LinkedList 的 更新元素。
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
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;
}
}
根据代码可以看出,在更新元素上,LinkedList 的实现是较为麻烦的。我们解析一下。
- 首先根据 checkElementIndex 方法检查下标是否越界。
- 通过 node 方法获取目标索引数据。这里首先判断了 index < (size >> 1),>> 表示右移一位,相当于除以二。所以这里的判断的意思是 index 距离头部近还是尾部近。如果靠近头部,就从下标 0 开始遍历;如果靠近尾部,就从末尾开始遍历。
- 获取到元素后重新赋值数据域,并返回之前数据域的值。
查询元素
LinkedList 的查询元素方式跟被查询元素所处位置有关。
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
可以看出,查询头尾还是方便的,但查询中间元素时,就主要依靠上面提到的 node 方法了。和 ArrayList 相比,查询中间元素就较为慢了。
(。・∀・)ノ゙嗨!如果文章帮助到你,可以请小弟我喝瓶可乐呀。点赞是对我最大的支持与认可,感谢!