在分析LinedList之前先对链表做一个简单的介绍 毕竟链表不像数组一样使用的多 所以很多人不熟悉也在所难免。
链表是一种基本的线性数据结构 其和数组同为线性 但是数组是内存的物理存储上呈线性 逻辑上也是线性 而链表只是在逻辑上呈线性。在链表的每一个存储单元中不仅存储有当前的元素 还有下一个存储单元的地址 这样的可以通过地址将所有的存储单元连接在一起。每次查找的时候 通过第一个存储单元就可以顺藤摸瓜的找到需要的元素。执行删除操作只需要断开相关元素的指向就可以了。
LinkedList中使用的并不是最基本的单向链表,而是双向链表。
在LinedList中存在一个基本存储单元,是LinkedList的一个内部类 节点元素存在两个属性 分别保存前一个节点和后一个节点的引用。
//静态内部类
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;
}
}
定义
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
在定义上和ArrayList大差不差 但是需要注意的是 LinkedList实现了Deque(间接实现了Qeque接口) Deque是一个双向对列 为LinedList提供了从对列两端访问元素的方法
初始化
在分析ArrayList的时候我们知道ArrayList使用无参构造方法时的初始化长度是10,并且所有无参构造出来的集合都会指向同一个对象数组那么LinkedList的初始化是怎样的呢?
打开无参构造方法
public LinkedList() {
}
什么都没有,那么只能够去看属性了。
//初始化长度为0
transient int size = 0;
//有前后节点
transient Node<E> first;
transient Node<E> last;
方法
add(E e)
public boolean add(E e) {
linkLast(e);
return true;
}
从方法中我们知道在调用添加方法之后 并不是立马添加的 而是调用了linkLast方法 新元素的添加位置是集合最后。
void linkLast(E e) {
// 将最后一个元素赋值(引用传递)给节点l final修饰符 修饰的属性赋值之后不能被改变
final Node<E> l = last;
// 调用节点的有参构造方法创建新节点 保存添加的元素
final Node<E> newNode = new Node<>(l, e, null);
//此时新节点是最后一位元素 将新节点赋值给last
last = newNode;
//如果l是null 意味着这是第一次添加元素 那么将first赋值为新节点 这个list只有一个元素 存储元素 开始元素和最后元素均是同一个元素
if (l == null)
first = newNode;
else
//如果不是第一次添加,将新节点赋值给l(添加前的最后一个元素)的next
l.next = newNode;
//长度+1
size++;
//修改次数+1
modCount++;
}
从以上代码中我们可以看到其在添加元素的时候并不依赖下标。
而其中的处理是 通过一个last(Node对象)保存最后一个节点的信息(实际上就是最后一个节点) 每次通过不断的变化最后一个元素实现元素的添加 。
**add(int index, E element)**
*添加到指定的位置*
public void add(int index, E element) {
//下标越界检查
checkPositionIndex(index);
//如果是向最后添加 直接调用linkLast
if (index == size)
linkLast(element);
//反之 调用linkBefore
else
linkBefore(element, node(index));
}
//在指定元素之前插入元素
void linkBefore(E e, Node<E> succ) {
// assert succ != null; 假设断言 succ不为null
//定义一个节点元素保存succ的prev引用 也就是它的前一节点信息
final Node<E> pred = succ.prev;
//创建新节点 节点元素为要插入的元素e prev引用就是pred 也就是插入之前succ的前一个元素 next是succ
final Node<E> newNode = new Node<>(pred, e, succ);
//此时succ的上一个节点是插入的新节点 因此修改节点指向
succ.prev = newNode;
// 如果pred是null 表明这是第一个元素
if (pred == null)
//成员属性first指向新节点
first = newNode;
//反之
else
//节点前元素的next属性指向新节点
pred.next = newNode;
//长度+1
size++;
modCount++;
get
public E get(int index) {
//检查下标元素是否存在 实际上就是检查下标是否越界
checkElementIndex(index);
//如果没有越界就返回对应下标节点的item 也就是对应的元素
return node(index).item;
}
//下标越界检查 如果越界就抛异常
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);
//如果index小于size的二分之一 从前开始查找(向后查找) 反之向前查找
if (index < (size >> 1)) {//左移 效率高 值得学习
Node<E> x = first;
//遍历
for (int i = 0; i < index; i++)
//每一个节点的next都是他的后一个节点引用 遍历的同时x会不断的被赋值为节点的下一个元素 遍历到index是拿到的就是index对应节点的元素
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
在这段代码中充分体现了双向链表的优越性 可以从前也可以从后开始遍历 通过对index范围的判断能够显著的提高效率。但是在遍历的时候也可以很明显的看到LinkedList get方法获取元素的低效率。
remove(int index)
所谓删除节点 就是把节点的前后引用置为null 并且保证没有任何其他节点指向被删除节点。
public E remove(int index) {
//下标越界检查
checkElementIndex(index);
//此处的返回值实际上是执行了两个方法
//node获取制定下标非空节点
//unlink 断开指定节点的联系
return unlink(node(index));
}
E unlink(Node<E> x) {
//假设x不是null
// assert x != null;
//定义一个变量element接受x节点中的元素 最后会最后返回值返回
final E element = x.item;
//定义连个节点分别获得x节点的前后节点引用
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//如果节点前引用为null 说明这是第一个节点
if (prev == null) {
//x是第一个节点 即将被删除 那么first需要被重新赋值
first = next;
} else {
//如果不是x不是第一个节点 将prev(x的前一个节点)的next指向x的后一个节点(绕过x)
prev.next = next;
//x的前引用赋值null
x.prev = null;
}
//如果节点后引用为null 说明这是最后一个节点 一系列类似前引用的处理方式 不再赘述
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
//将x节点中的元素赋值null
x.item = null;
size--;
modCount++;
return element;
}
说明prev,item,next均置为null 是为了让虚拟机回收