分析一个类的时候,我们首先要从它的继承关系入手。继承关系很大程度上反应了该类的功能。
首先看大家比较熟悉的List接口和Queue接口,这两个接口分别说明了LinkedList可以同时作为队列和列表来使用。
LinkedList实现了Deque(double ended queue,双端队列),Deque的父类就是Queue,实现该接口代表了LinkedList可以作为一个队列来使用。在文章末尾我会就该使用方法来做一个演示。
LinkedList同时也实现了List(列表),所以代表LinkedList也能像ArrayList一样,能过通过索引来实现增删改查,这也是我们后面进行源码分析的主要部分。
然后就是就是继承了AbstractSequentialList,这个类就是实现了列表的核心类。子类继承该类,只需要额外实现部分代码即可完成的创造一个能够访问某种列表(链表)的类。
Serializable接口,允许LinkedList被序列化和反序列化。
Cloneable接口,允许LinkedList被克隆(复制),是浅拷贝。
至此,我们已经大概看完LinkedList的继承关系了,大概总结一下,就是LinkedList的底层实现其实就是一个双端队列(双向链表),该类可以由Queue的多态形式作为一个队列使用,也可以以List的多态形式作为一个列表来使用。
List方法的核心无非就是增删改查,我们就对这四个点一一分析。
该类内部有一个节点类,了解即可。
private static class Node {
E item;
Node next;
Node prev;
Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
先看不指定插入位置的add(E e)方法,该方法将元素插入到列表的末尾。
/**
* 直接向链表末尾添加元素,尾添加
* @param e 需要添加的元素
* @return 是否添加成功
*/
public boolean add(E e) {
linkLast(e);
return true;
}
/**
* 向末尾添加元素的具体执行方法
* @param e 需要添加的元素
*/
void linkLast(E e) {
//将类中的成员变量last复制一份
final Node l = last;
//通过构造方法创建了新的节点,该新节点的pre指向当前的尾节点,next指向null
final Node newNode = new Node<>(l, e, null);
//然后让类的last节点尾新创建的节点
last = newNode;
//如果我们之前复制的尾节点是空的话,说明原来的列表是空的
//当前创建的节点是列表中的唯一一个节点,所以要令first也是这个新创建的节点
if (l == null)
first = newNode;
else //如果之前复制的尾节点不是空节点,那么代表列表中至少存在一个以上节点
//我们就直接让之前复制的这个节点的next指向我们新创建的节点
//这样就实现了之前复制的节点和我们新创建的节点的前后联系
//是的我们新创建的节点成功成为最新的尾节点
l.next = newNode;
//列表的长度+1
size++;
//列表的操作次数+1
modCount++;
}
这里的modCount需要注意,这里记录列表的操作次数,是为了防止在并发的时候出现的一些并发异常(比如一个遍历在删除某元素,另一个遍历在修改该元素)。这个modCount就起到了一个校验的作用。
之后再看根据索引插入的add(int index,E element)方法,一个个慢慢来分析:
/**
* 通过索引将元素插入指定的位置
* @param index 需要插入的位置(索引)
* @param element 需要插入的元素
*/
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
add方法先调用了checkPositionIndex(int index)方法来判断index是否合法:
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
如果index不合法,就抛出下标越界异常。在具体看一下isPositionIndex方法:
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
这里有一个小细节,就是index必须是>=0,这个我们知道,因为索引是从0开始的,但是为什么index要<=size呢?回到add方法,add方法有个判断index == size的,也就是说,如果插入的位置跟列表的长度一致,就是默认插在列表的最后,所以这里是允许index<=size。
这里特意讲这个,是因为后续的修改和查询也有相应的判断代码,但是它们的判断代码可没有=index的处理哦,index只能 继续回来看我们的add方法,需要注意的是,调用linkBefore的时候,同时调用node(index)这个方法哦,该方法我们只分析一遍,但是在后面也都会出现的。 这样就实现了我们List中通过索引增加元素的add方法了 删除和增加一样,有无参的remove()方法和能够指定删除某个索引的remove(int index)方法,一样是一个个来分析。 首先看无参的remove()方法: 再看一下根据索引删除的remove(int index)方法: 这样就实现了我们List中通过索引删除元素的remove方法了 相较于增加和删除,修改和查询是最容易理解的操作了,因为节点本质上就是一个引用类型的对象,这个对象只要取出来,修改它的值即可。原先列表的结构并没有发生变化: 修改的代码是不是看起来蛮简单的呀,但是也只是看起来简单,但是效率可是没有arrayList快的哦。 也就是List中的get方法,拿到对应的节点,并返回该节点的元素,这样就实现了通过索引取出某个值。 其实LinkedList中的实现逻辑都很清晰的,只要看明白了它的内部类Node的创建,它实现的list的功能也就都明白了,核心就是能够根据所以查询的Node(int index)方法。 Queue就是一个队列,那么队列基本的方法无非就是入队和出队。 来分析一下Queue,我个人的常用方法,一个offer(e),将元素入队,返回值是一个boolean,代表是否入队成功,另一个是 poll(),让元素出队并且删除该元素。 再看一下Queue的子接口,Deque,我们上面说了,它是一个双端队列,也就是可以同时从头尾进行操作,那么换个角度想一下,双端队列,和列表,有什么差别呢?这个就留给大家思考。 这里我挑两个队列(Queue)的方法出来,作为作业,看一下大家是否真的看明白了LinkedList的源码~ 如果3.1的练习的方法大家都能看懂,那么就证明确实理解了,如果还是迷迷糊糊的话,可以尝试自己点进LinkedList的源码中进行查看哦~ 其实LinkedList的成员属性,就记录并且帮助完成了一部分的功能,该类的底层实现并不复杂,还十分有趣,希望能对大家有所帮助,如果有疑惑或者文章出错也可以评论留言~ 最最最后的小作业,为什么我把final 修饰的临时存放的 l ( last ) ,或者 f (first) 叫做复制呢? /**
* 通过索引将元素插入指定的位置
* @param index 需要插入的位置(索引)
* @param element 需要插入的元素
*/
public void add(int index, E element) {
//检查索引是否合法
checkPositionIndex(index);
if (index == size) //如果索引等于列表的长度,代表需要插入尾部
linkLast(element);
else //否则就插入指定的位置
//先通过node(index)拿到指定位置的节点
linkBefore(element, node(index));
}
/**
* 根据索引返回指定位置处的节点
* @param index 需要查找的节点的位置(索引)
* @return 指定位置的节点
*/
Node
2.2 删(删除)
/**
* 删除并得到头节点的元素
* @return 删除的头节点的元素
*/
public E remove() {
return removeFirst();
}
/**
* 删除头节点的方法
* @return 删除的头节点的元素
*/
public E removeFirst() {
//先复制头节点
final Node
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
/**
* 根据索引删除元素
* @param index 需要删除的节点的位置(索引)
* @return 删除的节点的元素
*/
public E remove(int index) {
//先检查索引是否合法
checkElementIndex(index);
//同样是根据node(index),先取出对应位置的节点给unlink()方法
return unlink(node(index));
}
/**
* 根据索引返回指定位置处的节点
* @param index 需要查找的节点的位置(索引)
* @return 指定位置的节点
*/
Node
2.3 改(修改)
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
/**
* 根据索引修改某处节点的值
* @param index 需要修改的节点的位置
* @param element 需要修改成的元素
* @return 修改之前的那个元素
*/
public E set(int index, E element) {
//和删除一样,同样的索引检查是否合法
checkElementIndex(index);
//然后通过node方法拿到需要修改元素的节点
Node
2.4 查(查询)
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
public E get(int index) {
//跟删除一样,检查索引是否合法
checkElementIndex(index);
//直接通过node(int index)方法拿到对应节点
//然后返回该节点的元素即可
return node(index).item;
}
/**
* 根据索引返回指定位置处的节点
* @param index 需要查找的节点的位置(索引)
* @return 指定位置的节点
*/
Node
2.5 小结
3) Queue方法解析
3.1 练习
3.1.1 入队
public boolean offer(E e) {
return add(e);
}
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node
3.1.2 出队
public E poll() {
final Node
3.2 小结
4) 总结