LinkeList原理
LinkedList 和 ArrayList 一样,都实现了 List 接口,但其内部的数据结构有本质的不同。LinkedList 是基于链表实现的(通过名字也能区分开来),所以它的插入和删除操作比 ArrayList 更加高效。但也是由于其为基于链表的,所以随机访问的效率要比 ArrayList 差。
1.ArrayList在实现添加、插入底层都要进行扩容及位置偏移问题。删除要进行位置偏移,但是LinkeList 是不需要的。(所以ArrayList在这方面效率比LinkeList差)
2.LinkeList在做查询的时候,效率是非常低的。
链表讲解
链表分为:单链表和双链表
单向链表特性:
下图为单向链表的特性:
上一个节点会包含下一个节点的指向,假设:我要找到Node3节点,就需要通过Node1 找到 Node2 再找到Node3节点,不能直接找Node3节点,所以在查询的时候,性能是比较慢的。
双向链表的特性
当前这个节点会包含上一个节点和下一个节点的指向。(第一个节点是没有上一个节点的,所以不包含上一个)。
双向链表删除节点是怎样的?
会获取删除元素的上一个节点和下一个接点。
删除之前的图
删除之后的图
将Node3的上一个节点变为Node1
将Node1的下一个节点变为Node3
LinkeList源码分析
//实际存储大小
transient int size = 0;
//第一个元素
transient Node<E> first;
//最后一个元素
transient Node<E> last;
first和last解析:
图中链表中只存放一个节点Node1。那么first和last都是=Node1节点
图中链表中只存放2节点Node1和Node2,那么first是=Node1,last是=Node2。
为什么需要first元素?
first元素是为了查询,从哪里开始到哪里结束。
为什么需要last元素?
last是为了定位添加元素的时候从哪里开始。
总之就是 查询的时候从头开始,添加的时候从尾开始。下面源码可以看出。linkLast
public boolean add(E e) {
linkLast(e);
return true;
}
Node节点定义
源码:
//一个节点包含上一个节点和下一个节点。
private static class Node<E> {
E item; //泛型 元素obj
Node<E> next; //下一个节点
Node<E> prev;//上一个节点
//构造方法
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
LinkeList的默认构造函数:
源码
默认的构造函数没有做任何事情。
// 默认的构造函数没有做任何事情。
public LinkedList() {
}
add(obj)解析:
源码:
/**
* Links e as last element.
*/
void linkLast(E e) {
//获取最后一个节点
final Node<E> l = last;
//创建一个 新的节点,最后一个节点作为当前新节点的上一个几点
final Node<E> newNode = new Node<>(l, e, null);
//创建新的节点作为最末尾节点,用于添加的开始
last = newNode;
//判断最后一个节点是否为null,如果为null表示容器中没有任何元素,不为null,则把最后一个节点的下一个节点指向新创建的节点
if (l == null)
first = newNode;
else
l.next = newNode;
size++;//容器的长度
modCount++;
}
get(index)解析:
原理图
上图中的是比较简单的一个,从头查到尾。但是效率的话是比较低的。
源码是通过二分法进行获取,因为链表是双向的。这样效率比一个一个的找效率快(比较适合单向链表)。
源码:
public E get(int index) {
//越界检查
checkElementIndex(index);
//根据下标获取元素
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) {
//通过二分法进行获取,因为链表是双向的。这样效率比一个一个的找效率快
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;
}
}
remove(index)解析:
源码
public E remove(int index) {
checkElementIndex(index);
//先根据下标获取节点
return unlink(node(index));
}
//根据下标获取节点之后的操作
E unlink(Node<E> x) {
//获取节点中的内容及改节点的上一个节点和下一个节点
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//判断获取到的上一个节点是否为null,为null表示:删除的节点是第一个节点或者容器中只有一个节点。
if (prev == null) {
first = next;//下一个节点作为首节点
} else {
//更新上一个节点的下一个节点指向
prev.next = next;
x.prev = null;//待删除的节点的上一个节点设为null,用于gc回收(不可达对象)
}
//判断获取到的下一个节点是否为null,为null表示:删除的节点是末尾节点或者容器中只有一个节点。
if (next == null) {
last = prev;//上一个节点作为末节点
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
//长度-1
size--;
modCount++;
return element;
}
add(index,obj)解析:
原理图
根据下标添加数据,其实实现方式跟remove差不多。
源码:
public void add(int index, E element) {
//检查下标是否越界及是否遵循标准
checkPositionIndex(index);
//
if (index == size)
linkLast(element);//直接走add()方法
else
//先根据下标获取节点。
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
//获取下标的上一个节点
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++;
}