Java Collection源码分析(1) LinkedList
1.链表的基本概念
线性表:一组元素,除第一个元素和最后一个元素外,每个元素都有相应的前驱和后继,这样的数据结构就是线性表
链表是线性表的一种,它不同于顺序表,它不需要连续的存储空间,链表元素存储于内存中的各个地方,元素之间通过指针连接因此能够很好的利用内存空间,但是由于每个元素都要维护指向其他元素的指针(java中是引用),因此需要额外的空间
一般来说,如果程序的侧重点是读取和写入元素,那么链表不太适合,链表不支持随机访问,访问元素需要遍历链表,代价比较大。但是如果程序的侧重点在于插入和删除元素,那就需要使用链表,不同于顺序表需要移动大量元素,链表的插入删除只需要修改表节点的前驱后继引用(双链表,LinkedList就是双链表),复杂度较低,性能较好。
2.表节点Node
LinkedList中970行有个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;
}
}
通过代码我们可以看到,表节点维护了
- 三个属性:item-值,prev-前驱节点,next-后继节点
- 一个带参构造器
3.构造器
- 无参构造器
public LinkedList() {
}
调用的话会默认调用父类的构造器,父类构造器调用父类构造器一直往上走,最后会到AbstractCollection 这个抽象类的构造器,而且这期间调用的所有构造器都是空的,那说明这个空构造器的作用就是构造一个LinedList对象,并且将他所有的属性以及从父类继承的属性初始化,数基本类型数字为0,boolean为false,对象为null。 详情请看对象实例化机制,这里不赘述。
- 有参构造器
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
有参构造器,形参是一个集合接口对象(接口可以声明但不可实例化)。
有参构造器调用无参构造器实例化一个“基本款”LinkedList,然后调用addAll将c中所有元素插入到LinedList中
4.方法
4.1 addAll
public boolean addAll(Collection<? extends E> c) {
//调用该类在指定位置插入集合的方法。
//size是类的实力域是指链表大小,这里是意图在链表表尾插入一个集合
return addAll(size, c);
}
形参c是集合容器,容器中的元素得是E(LinkedList容器中元素类型)的子类型。
public boolean addAll(int index, Collection<? extends E> c) {
//该方法功能是在指定位置处插入一个集合
//检查插入位置是否合法,index的范围应该在[0, size],
checkPositionIndex(index);
Object[] a = c.toArray();//将c转化成数组
int numNew = a.length;
if (numNew == 0)
return false;//如果插入集合c为空,返回false
Node<E> pred, succ;
if (index == size) {//如果插入位置刚好是表尾,那么令pred指向表尾元素
succ = null;
pred = last;
} else {//如果插入位置不是表尾,succ指向插入位置元素,prev指向其前驱
succ = node(index);
pred = succ.prev;
}
//到这里我们已经找到了插入位置的前驱和后继
//这样原链表被分为两块,一块在插入元素之前(前驱块),一块在插入元素之后(后继块)
//prev是前驱块最后一个元素,succ是后继块第一个元素
//下面只要先把集合中的元素一个一个插入到前驱块后形成新的前驱块
//然后把新的前驱块和后继块接起来
//把元素插入到前区块后
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
//根据集合重元素新建一个节点,指定前驱为prev节点
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
//前驱为空,说明插入这个元素前链表为空,插入的元素是该链表第一个元素
first = newNode;
else//前驱非空,需要把前驱的后继指向该元素
pred.next = newNode;
pred = newNode;//更新前驱变量,使其指向刚插入元素,典型的尾插法
}
//已经将所有的元素都插入到前驱块
//新的前驱块的最后一个元素就是现在prev指向的对象
//下面这段逻辑是那新前驱块与原后继块接起来
if (succ == null) {
//后继块为空,插入已经完成,修改尾元素实力域就好了
last = pred;
} else {
//后继块有内容
//需要将prev的后继引用指向后继块第一个节点succ
//把succ前驱引用指向prev
pred.next = succ;
succ.prev = pred;
}
size += numNew;//更新链表节点数量
modCount++;//更新链表修改次数
return true;//插入成功,返回真
}
addAll方法功能是向链表中插入集合对象的一个方法,使用的是链表数据结构“尾插法”
插入位置一共有三种情况:
- 表头
插入表头size=0,上述代码中prev=null,集合中第一个元素就成了表头元素,链表first也指向该元素。 - 表中
往表中插入元素是这个方法比较常见的情况,在这种情况下,原链表以插入位置前后分为前后两个块,集合中的元素依次插入到这两个块之间,最后将他们连起来 - 表尾
往表尾插入集合中所有元素,并且把last属性更新插入集合示意图
4.2 linkLast/linkFirst
这两个方法对称的一个往表头前一个往表尾后插入元素
private void linkFirst(E e) {
final Node<E> f = first;
//新建一个节点前驱为空,后继为表头节点
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;//更新链表的表头节点属性
//链表是双向的,原表头节点的前驱要修改一下
if (f == null)
last = newNode;//链表为空则更新尾节点
else
f.prev = newNode;
size++;//表节点数量+1
modCount++;//修改次数+1
}
往表头插节点无非两种情况:
- 链表为空
你插入的元素就是第一个元素,last和first引用的是同一个对象,要跟新last引用 - 链表非空
插入的元素后,必须更新原表头节点的前驱引用
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++;//表节点数量+1
modCount++;//修改次数+1
}
往表尾插节点无非两种情况:
- 链表为空
你插入的元素就是唯一一个元素,last和first引用的是同一个对象,要更新first引用 - 链表非空
插入的元素后,必须更新原表尾节点的后继引用
这两个方法很重要,几乎所有的add方法都是应用的这两个方法
- add()—linkLast
- addFirst()—linkFirst
- addLast()—linkLast
4.3unlinkFirst/unlinkLast
删除方法的原始方法,链表其他的删除方法几乎都是调用的它俩
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
删除一个节点最重要的是断链,并且将被删除节点的前驱节点和后继节点连起来
- 该段代码先取出了要删除节点的值,存放在变量element中
- 取出第二节点放到next变量中
- 删除原第一几点first,将值和前后驱节点置空,方便GC之后回收
- first重新赋值next节点也就是原来第二节点
- 此时的first节点(next)前驱还保留在原来第一节点对象,必须将其置空,不然GC不会回收内存中原第一节点空间(因为还保留引用)
- 更新更改次数和节点数量,返回element(被删除节点值);
unlinkLast和unlinkFirst很像,就不赘述了
链表中的删除方法(除了删除指定位置节点、指定对象的方法)都是在这两个方法的基础上做的
removeFirst()、removeLast()、remove()
4.4remove(Object)/remove(index)
删除指定对象和删除指定位置对象
unlink方法:
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;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
源码解读:
- 首先获取被删除节点的值以及前驱和后驱的节点引用
- 然后把前驱和后继连起来,prev.next = next;next.prev = prev;前驱节点后继等于后继节点,后继节点的前驱属性等于前驱节点。
- 连前驱后继的时候要注意删除节点是表头节点和表尾节点的情况,要更新链表表头节点和表尾节点属性
- 然后删除被删除节点,将节点的引用置空,稍晚GC会来垃圾回收的
- 更新链表节点数量以及链表修改次数
remove(object)就是先遍历找要删除节点然后调用unlink
remove (index)同样的遍历链表找到要删除的节点然后调用unlink
unlink是删除功能的实际操作方法,其他的删除方法大多是在这个方法的基础上加了找节点和判断参数合法性的功能
4.5 toArray
- toArray()返回的是该链表元素内容组成的数组,数组元素类型是Object,具体使用自己强转
- toArray(T[ ] a)
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
利用反射对a扩容,使得其能放下size个元素
a = (T[])java.lang.reflect.Array.newInstance(
a.getClass().getComponentType(), size);
int i = 0;
Object[] result = a;
//for循环将链表中元素写进数组a中
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
if (a.length > size)
//如果原数组size大于长度,那么就对第size位赋null
a[size] = null;
return a;
}
最后返回的数组有两种情况
- 大小与链表大小相同,数组中元素就是链表中的元素
- 数组长度大于链表大小,长度大于链表长度部分除第一个赋为null外其余保留,因此使用时要注意判断null的位置,后面的元素很有可能是无效数据
4.6push、pop
LinkedList还可以当做链栈用,栈顶为表头,push、pop操作的都是第一个元素,底层方法:
- push ---- addFirst----linkFirst
- pop -----removeFirst-----unlinkFirst