后续我会陆陆续续更新java源码的一些阅读。大家如果觉得对自己有用就点个关注吧。
文中描述如有问题,欢迎留言指证。
引言
本文中的例子我尽量写的简单,避免一些我平时查资料时一些例子中出现的大量无用代码,产生让人阅读不下去的感觉
LinkedList
注意:如果有小伙伴不知道链表是啥,请自行百度一下,再看这篇文章,毕竟这篇文章主要是带大家进行源码阅读.
描述:其实LinkList就是链表.(外带一句ArrayList本质就是数组),其实链表也有很多种,单向链表,双向链表,单向循环链表,双向循环链表,等等,LinkedList就是一个双向链表!
链表的特点
- 链表随机访问很慢,因为每次访问一个变量都需要循环,从一个元素开始比遍历
- 插入和删除很容易,因为链表不涉及到移位,只需要修改节点指针的指向
数组的特点:
- 数组存储在内存中是连续的.
- 由于是连续的所以插入和删除效率就会比较慢(相对链表等)因为插入一个数值,后面的元素都要依次往后移,删除的话后面的元素都会依次往前移.
- 随意访问数据很快,因为数组是连续的,知道每一个数据的内存地址,可以直接找到给地址的数据。
类的参数
我将只展现类的属性,将所有没必要的方法,全部删除,这样也可以更直观
LinkedList展示
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
//链表的长度,你如果调用Size()方法,他就把这个参数返给你
transient int size = 0;
//链表的头结点
transient Node<E> first;
//链表的尾节点
transient Node<E> last;
//无参构造函数,啥都没做
public LinkedList() {
}
//这个有参构造函数也只是创建完对象,调用addAll方法没什么好说的
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
}
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;
}
}
看到这几个参数,我找张图来形象的标准双向链表
这张图太形象了,咋能这么形象,参数名都对上了.
构造函数
无参构造函数
public LinkedList() {
}
对你看的没错就是一个空方法,这就意味着当我们
new LinkedList()
,只是创建了一个对象没有其他操作,并且其他有参构造方法先不看,因为无非就相当于创建了这个对象然后调用一个这个对象的方法(不要有疑问,我连他的父类都看了,全是空方法)这个我们就可以不看了哈哈哈!!!
add方法
第一部分代码
public boolean add(E e) {
linkLast(e);
return true;
}
这一部分没什么,继续进入方法
第二部分代码
void linkLast(E e) {
//获取当前尾节点,如果添加的是第一个元素l拿到的就是null
final Node<E> l = last;
//将添加元素e打包成节点对象,这个节点对象头指针,
//指向尾节点,尾指针指向null
final Node<E> newNode = new Node<>(l, e, null);
//将新添加的节点设置成尾节点,因为链表添加都是在尾节点的
//后面再加一个节点,这样新加的节点就成为了,新的尾结点
last = newNode;
//判断再添加新元素之前尾结点是不是空,如果是空将头结点也设置成新节
//这个时候链表中头结点和尾节点都是同一个节点
if (l == null)
first = newNode;
//如果不是空则建原来的尾节点的尾指针,指向新节点
else
l.next = newNode;
//链表长度加1
size++;
//对链表的操作次数加1
modCount++;
}
其实注释写了这么多.如果你会链表你会接受的很透彻,标准的双向链表的增操作,就是将新加入的元素连接到链表尾部.
注意重点
你发没发现这里面不涉及扩容的操作,它可以无限的在尾结点添加元素,这也是插入操作为什么LinkedList比ArrayList快的原因之一(啥呀!你不懂ArrayList 原理那么请看我的另一篇文章ArrayList的构造函数和Add方法)
get方法
第一部分代码
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
依然没什么可以看的,我们来进入方法
第二部分代码
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
这个方法只是校验你获取的元素下标是否越界,比如你
list.get(-1)
或者list.get(10000000)
这样就会报错,我们继续看
第三部分代码
Node<E> node(int index) {
//size >> 1 二进制运算右移以为,就相当于10进制中
//除以2取整型的结果,注意不带小数,是取整型,1/2 = 0的那种
//判断要获取元素的下标,在前半部分还是后半部分
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(链表)想获取元素需要遍历,而ArrayList不需要,所以对于数据的访问,ArrayList的性能更好
remove(int a)方法
第一部分代码
public E remove(int index) {
//校验下标是否越界的,如果越界报错
checkElementIndex(index);
return unlink(node(index));
}
没啥说的,进入第二部分
第二部分代码
注意:这里面有一个
node(index)
方法,我们在get方法第三部分代码讲过了,他的功能就是获取到指定下标的节点
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;
//细节:这块他把需要删除元素的尾指针致null了
x.prev = null;
}
//判断是不是尾指针,如果是直接将链表中的尾节点替换成
//删除节点的上一个节点
if (next == null) {
last = prev;
} else {
//如果并不是将删除节点的下一个节点头指针指向删除节点的
//下一个节点,就完成了操作
next.prev = prev;
x.next = null;
}
//将删除节点的各个属性彻底置空
x.item = null;
//长度减1
size--;
modCount++;
//返回已删除的节点
return element;
}
这块操作我们直接看图,可能更透彻一点.
set方法
第一部分代码
public E set(int index, E element) {
//校验下标是否越界
checkElementIndex(index);
//获取需要修改的节点
Node<E> x = node(index);
E oldVal = x.item;
//将新节点替换进去
x.item = element;
//返回旧节点
return oldVal;
}
node(int index)
这个方法我们在get方法第三部分代码讲过了,然后更新节点内容就行,不用对指针指向进行操作
总结:阅读这些代码的关键就是你需要了解链表的增删改查,之后,这些代码就好理解多了,我也是越写越纳闷,这篇文章没有讲链表增删改查,但是你要是会了增删改查,可能也就没必要读这篇文章了.我这写的劲劲的,到底是写给谁看的.擦!