前言
上一篇介绍了ArrayList的一个源码,这遍我们来介绍在List下的另一个常用的类,LinkedList,和ArrayList总是用来比较,实现方式和ArrayList还有很多不同的。
构造器:
首先,我们应该知道两类集合的底层实现是不同的,ArrayList的底层结构是数组,所以需要在初始化的时候,去申请一块连续的内存空间,空间不够了还要去选择扩容,而LinkedList的底层结构是链表,也就导致了他无需连续的内存空间,只要有空间他就能插入元素,离散型空间分布。
public ExLinkedList() {}
public ExLinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
链表不用在初始化的时候做别的事情,所以较为简单。
添加方法
LinkedList的三个关键的成员变量,分别是第一个节点的引用,最后一个节点的引用,和当前链表的实际长度。
//第一个节点,用于查找或者遍历的功能,从第一个节点一直可next到需要的地方
private Node<E> firstNode;
//最后一个节点,用于添加元素,可以直接在最后面加入元素
private Node<E> lastNode;
//集合实际使用的大小
private int size = 0;
同时我们需要创建一个私有静态成员类,Node类存储了上个节点、下个节点和当前的节点的数据内容。
private static class Node<E> {
//该节点的上一个节点
private Node<E> preNode;
//该节点的下一个节点
private Node<E> nextNode;
//该节点的存储的数据
private E item;
public Node(Node<E> preNode, E item, Node<E> nextNode) {
this.preNode = preNode;
this.nextNode = nextNode;
this.item = item;
}
}
add(E e):
//直接添加对象
public boolean add(E e) {
//在最后一个节点后添加元素
linkLast(e);
return true;
}
LinkedList实现的双向链表的概念,即当前链表有上个节点的引用,也有下个链表的引用,添加的时候只需要将添加的链表,和当前最后一个链表进行关系,即可以实现添加
void linkLast(E e) {
//获得当前最后一个节点
final Node<E> oldLastNode = lastNode;
//构造一个新节点:
// 第一个参数:新节点的上一个节点为当前的最后一个节点
// 第二个参数:存储的元素
// 第三个参数:null
final Node<E> newAddNode = new Node<>(oldLastNode, e, null);
//将最后一个节点设置为当前新节点
lastNode = newAddNode;
if (firstNode == null) {
//如果第一个节点还未设置值,则第一个节点为新节点
firstNode = newAddNode;
} else {
//旧的最后一个节点的下一个节点设置为当前新节点
oldLastNode.nextNode = newAddNode;
}
}
add(int index, E element):
这里我们要实现根据下标找到链表中对应节点的方法,链表的查找方法,就是需要第一个节点或最后一个节点,一个需要一直next,一个需要pred,来进行节点的一个查找,所以链表的查找效率是远远低于数组的,他需要遍历整个链表才能找到对应的节点,这里还用了二分法查询来优化查找的效率。
//根据下标获得值
Node<E> node(int index) {
// 判断该下标是在相对中间的左边还是在右边,二分法查询
// 即是顺时针查更快查到,还是逆时针更快查到
if (index < (size << 1)) {
Node node = firstNode;
for (int i = 0; i < index; i++) {
//一直不断的遍历链表,获得你需要的值
//如需要下标为0的节点,firstNode需要nextNode0次(就是本身)
// 需要下标为1的节点,firstNode需要nextNode1次
// 需要下标为2的节点,firstNode需要nextNode2次
// 。。。。
// 需要下标为N的节点,firstNode需要nextNodeN次
node = node.nextNode;
}
//根据规律,循环结束返回的值就是需要的值
return node;
} else {
Node node = lastNode;
for (int i = size - 1; i > index; i--) {
//倒序,获得上一个节点
node = node.preNode;
}
return node;
}
}
指定下标处插入元素,这里我需要分为两种逻辑,一种就是常见的直接在链表尾部追加元素,一种就是在链表中间插入元素,这时候你需要查出当前下标的节点,然后得到当前节点的上一个节点,之后你要添加的新节点需要和这两个节点建立关系,完成中间插入,链表的中间添加操作,就无需像数组那样移动元素。
public void add(int index, E element) {
//1.先检查传入参数的准确性
checkPositionIndex(index);
//2.根据下标值判断是在链表之后加元素,还是在之前添加元素
if (size == index) {
//直接在链表之后追加元素
linkLast(element);
} else {
linkBefore(element, node(index));
}
}
private void linkBefore(E element, Node<E> cur) {
Node pred = cur.preNode;
Node newNode = new Node(pred, element, cur);
cur.preNode = newNode;
if (pred == null) {
firstNode = newNode;
} else {
pred.nextNode = newNode;
}
size++;
modCount++;
}
addAll(int index, Collection c):
addAll(Collection c)方法就不详细介绍了,内部还是使用的addAll(int index, Collection c)方法,addAll(int index, Collection c)其实也是上个添加方法也非常相似,不断的建立新旧节点之间的关系,来完成添加。
public boolean addAll(int index, Collection c) {
//1.先检查传入参数的准确性
checkPositionIndex(index);
Object[] objects = c.toArray();
int numNew = objects.length;
if (numNew == 0) {
return false;
}
//2.获得该下标的节点,和该节点的上一个节点
Node<E> pred, cur;
if (index == size) {
//如果是在集合最后追加节点
pred = lastNode;
cur = null;
} else {
//查出当前节点
cur = node(index);
pred = cur.preNode;
}
//3.将传入的集合的数据插入到数组中,并完成和pred的联系
for (Object object : objects) {
//构造一个新节点
Node newNode = new Node(pred, object, null);
//如果要插入的节点的上一个节点为空,说明是在头节点前面插入
if (pred == null) {
//头节点就是新添加的第一个节点
firstNode = newNode;
} else {
pred.nextNode = newNode;
}
//此时需要更改上一个节点的值,即下一个新节点的上一个节点为当前插入的节点
pred = newNode;
}
//4.已经完成插入的值和上个节点的连接关系,现在完成和cur的连接关系
if (cur == null) {
//如果cur节点为null,说明一直是在尾部追加,即最后一个节点为pred
lastNode = pred;
} else {
pred.nextNode = cur;
cur.preNode = pred;
}
size += numNew;
modCount++;
return true;
}
删除方法:
E remove(int index):
根据下标去删除节点的大概思路是,如果你删除的节点在中间部分的话,你需要将上一个节点和他的下一个节点建立关系,将需要删除的节点从终孤立出去,即该链表访问不到该节点就即可。
public E remove(int index) {
//1.先检查传入参数的准确性
checkPositionIndex(index);
return unlink(node(index));
}
private E unlink(Node<E> node) {
//获得需要删除节点的数据
E element = node.item;
//获得需要删除节点的上一个节点
Node pred = node.preNode;
//获得需要删除节点的下一个节点
Node next = node.nextNode;
//如果删除的为第一个节点
if (pred == null) {
//将第一个节点设置为下一个节点
firstNode = next;
} else {
//将上一个节点的下一个节点设置为next
pred.nextNode = next;
node.preNode = null;
}
//如果删除的为最后一个节点
if (next == null) {
//将最后一个节点设置为上一个节点
lastNode = pred;
} else {
//将下一个节点的上一个节点设置为pred
node.nextNode = null;
next.preNode = pred;
}
node.item=null;
size--;
modCount++;
return element;
}
总结:
通过介绍,我们可以知道:
ArrayList更加适合查找元素,通过下标即可定位到元素本身,而在中间进行操作的情况下,效率上就很难比的过LinkedList的,很大原因是因为数组需要移动数据,进行数据的一个copy,而LinkedList只需要改变节点的上下引用即可,而对于随机的get,set效率就显得没那么快,因为他需要进行链表的一个遍历。