本文属于单列集合容器第二篇,在上一篇文章中分析了ArrayList常用Api的源码,今天我们来学习下它的兄弟LinkedList;
同样我们从下面3方面来学习LinkedList:
1、底层数据结构实现;
2、构造方法;
3、常用Api;
1、看图理解Node
说到LinkedList的底层数据结构实现我们都清楚的知道是链表,那么什么是链表,在这里通过一个简单的图形描绘;
上图描述:在上图中几个小伙伴手牵手过河的图形可以形象的比喻成链表的实现原理,前者的右手牵着后者的左后依次类推,左手(Node prev)、右手(Node next)、人物(E item),比如你想找到我们的小西西,在链表中的实现是先判断从左边开始找还是从右边开始找的链路比较短,并不是说找小西西就一定得问掌柏滋,说不定问謝听风的时候就没有下文了,在链表的实现中的步骤应该是这样的,先问牛的滑 你认识小西西吗? 牛:我不认识,要不你问问周星星吧。周:你算是问对人了,我前面那个就是小西西,听说小西西最近在剪辑动作片,切勿打扰......
####2、Node类实现源码
上图描述的此图此景正如彼时彼景,言归正传下面我们看看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;
}
}
E item :当前节点的值;
Node<E> next :当前节点的后一个节点的引用(可以理解为指向当前节点的后一个节点的指针)
Node<E> prev:当前节点的前一个节点的引用(可以理解为指向当前节点的前一个节点的指针)
复制代码
Node类实现的源码还是很容易理解的,在此基础上我们看看LinkedList
3、LinkedList源码的理解
3.1 对象的创建
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
//记录集合的大小(比如,我们这里有6个人物)
transient int size = 0;
//链表头节点
transient Node<E> first;
//链表尾节点
transient Node<E> last;
//空参构造 就是创建对象
public LinkedList() {
}
//有参构造 创建对象 我们看看addAll( )这个方法
public LinkedList(Collection<? extends E> c) {
//1、调用空参构造
this();
//2、调用addAll()方法
addAll(c);
}
//3、方法的重载 size:集合的大小 c : 传入的集合对象
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
//4、二个参数的addAll() index 可以理解为添加元素的节点位置
public boolean addAll(int index, Collection<? extends E> c) {
//5、检查越界 [0,size] 闭区间,几乎所有的涉及到在指定位置添加或者删除或修改操作都调用了此方法
checkPositionIndex(index);
//转换成Object[]数组
Object[] a = c.toArray();
//获取数组的长度
int numNew = a.length;
if (numNew == 0)
return false;
//定义二个节点 pred:添加节点的前一个节点 succ:添加节点的位置
Node<E> pred, succ;
/* 6、判断添加元素的位置与当前集合大小的关系 注意:位置从0开始计算,size从1开始计算(默认为0)
比如需要在LinkedList对象A(对象中有二个元素 a b )中添加一个集合对象B(有1个元素 c )如果A.addAll(1,b)--> a c b */
//6.1 判断当前添加元素的位置是否等于集合大小,如果等于则说明添加的元素将位于原有集合的尾部,所以将当前节点元素置为null,pred指向原有集合的尾节点;
if (index == size) {
succ = null;
pred = last;
} else {
//7、succ指向插入待插入位置的节点
succ = node(index);
//pred指向succ节点的前一个节点
pred = succ.prev;
}
//8、遍历传入进来的数组
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
//创建一个Node对象保存数组a遍历出来的值 pred:当前节点的前一个节点,e:当前节点的元素 null:当前元素的后一个元素为null
Node<E> newNode = new Node<>(pred, e, null);
//如果此节点的前一个节点对象为null,就将此元素作为首节点
if (pred == null)
first = newNode;
else
//将此节点的前一个节点next值(后节点指向)此节点
pred.next = newNode;
//pred指向当前节点,便于后续新节点的添加
pred = newNode;
}
//如果需要添加的元素为null,说明是需要添加的元素位于LinkedList集合的最后一个元素之后
if (succ == null) {
//设置末尾节点
last = pred;
} else {
//否则将此节点的下一个节点指向待插入位置的节点
pred.next = succ;
//待插入位置的节点的前一个节点指向当前节点
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
/* 第 5 步检查参数的方法 */
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
/* 第 7 步中的node()方法 */
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;
}
}
//debug调试
public static void main(String[] args) {
List<String> integersOne = new ArrayList<String>();
integersOne.add("a");
integersOne.add("b");
LinkedList<String> linkedList = new LinkedList<String>(integersOne);
List<String> integersTwo = new ArrayList<String>();
integersTwo.add("c");
integersTwo.add("g");
linkedList.addAll(1,integersTwo);
}
复制代码
3.2常用API
add(E e) 普通的添加方式
add(E e) //普通添加方式
public boolean add(E e) {
linkLast(e);
return true;
}
复制代码
addFirst(E e) 添加元素到首节点
addFirst(E e) //添加元素到首节点
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first;
//创建一个对象,将首节点设置为null
final Node<E> newNode = new Node<>(null, e, f);
//当前节点作为首节点
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
复制代码
addLast(E e) 添加元素到尾节点
addLast(E 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++;
}
复制代码
remove(Object o) 删除元素
remove(Object o) //删除元素
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
//关键方法,将需要删除的节点设置为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;
//如果此节点的前节点为null
if (prev == null) {
//首节点为此节点
first = next;
} else {
//相当于 prev.next = x.prev 前面节点记录待删除节点的后面节点
prev.next = next;
//将待删除节点的前面节点设置为null
x.prev = null;
}
//如果此节点的后节点为null
if (next == null) {
//首节点为尾节点
last = prev;
} else {
//相当于next.prev = x.prev 待删除节点的后面节点记录前面节点
next.prev = prev;
//将待删除节点的后面节点设置为null
x.next = null;
}
//将节点设置为null
x.item = null;
size--;
modCount++;
//返回删除节点的元素
return element;
}
复制代码
set(int index, E element) 修改指定节点的元素
set(int index, E element) //修改指定节点的元素
public E set(int index, E element) {
//检查是否索引越界
checkElementIndex(index);
//获取指定索引的节点
Node<E> x = node(index);
E oldVal = x.item;
//将指定节点元素值指向传入进来的值
x.item = element;
//返回删除的值
return oldVal;
}
复制代码
get(int index) 查询指定索引位置的元素
public E get(int index) {
//检查索引是否越界
checkElementIndex(index);
//获取指定索引位置节点元素的值
return node(index).item;
}
复制代码
ArrayList的修改与查询效率高,增删效率慢,因为ArrayList底层基于数组实现,可以通过索引快速定位到需要查询或者修改的元素,但是在增加和删除的时候需要涉及到扩容需要将前面的数据进行复制;而LinkedList恰好相反,底层基于链表实现,在获取元素的时候需要先获取对应的节点,但是在删除和增加的时候只需要修改前后链表指针即可;
在此感谢大佬以及队友的帮助,在工作中得到了成长,小生不才,如有不当之处,望各位大佬指点一二!