上文书说到ArrayList的简要源码分析,就不得不提到和它相近的类似的LinkedList,同样都是列表,让我们一起来看看有何相同和不同之处。
talk is cheap,show me the code ---undefined
老规矩,先来一段代码示例
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class LinkedListTest {
public static void main(String[] args) {
//无参构造函数new一个linklist
List linklist=new LinkedList();
//linklist的add操作
linklist.add("hello world");
linklist.add("world hello");
System.out.println("当前linklist的容量大小"+linklist.size());
//linklist的getter setter操作
System.out.println("getter"+linklist.get(3));
System.out.println("setter"+linklist.set(3,"huhu"));
System.out.println("当前linklist的容量大小"+linklist.size());
//linklist的addall操作
linklist.addAll(linklist);
System.out.println("当前linklist的容量大小"+linklist.size());
//linklist的index操作
System.out.println("这是索引"+linklist.indexOf("world hello"));
System.out.println("这是最后索引"+linklist.lastIndexOf("world"));
System.out.println("当前linklist的容量大小"+linklist.size());
System.out.println("是否包含有"+linklist.contains("huhu"));
System.out.println("是否包含所有"+linklist.containsAll(linklist));
System.out.println("当前linklist的容量大小"+linklist.size());
//linklist的remove操作
System.out.println("删除元素"+linklist.remove("world hello"));
System.out.println("删除元素"+linklist.remove(4));
System.out.println("当前linklist的容量大小"+linklist.size());
//linklist的retain操作
List linklist2=new LinkedList();
linklist2.add("hu hu");
linklist2.add("world hello");
linklist2.add(1);
linklist2.add(100);
linklist2.add(150);
System.out.println("保留指定集合中的内容"+linklist.retainAll(linklist2));
System.out.println("删除指定集合中的内容"+linklist.removeAll(linklist2));
System.out.println("当前linklist的容量大小"+linklist.size());
LinkedListTest linklistTest=new LinkedListTest();
linklistTest.linklistop(linklist2);
}
public boolean linklistop(List linklist) {
Iterator iterator = linklist.iterator();
while (iterator.hasNext()) {
System.out.println("这是第几个元素 \t " + iterator.next());
}
return true;
}
}
1. 无参构造函数
public LinkedList() {
}
可以看到,无参构造函数是空的,即什么也没有做。
注1:和ArrayList无参构造函数不同,由于采用的底层数据结构不同,ArrayList的底层数据结构为数组,因此无参构造在初始化时,就需要提前申请一个数组的空间;而LinkedList采用的是双向链表+双向队列,链表的一个特性就是即用即申请,因此在LinkedList中的无参构造就什么也没有做啦。
Q1:LinkedList有哪些构造函数?分别对应哪种情况?
Q2:为什么LinkedList的底层数据结构是双向链表?为什么不是单向链表?这两种链表有何处异同以及好坏处?
Q3:为什么LinkedList还实现了Deque(队列接口)?实现了双向链表+双向队列的数据存储结构?
2. add函数
public boolean add(E e) {
linkLast(e);
return true;
}
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++;
}
add实现了LinkedList的末尾追加,也就是添加元素的功能,其步骤如下
1. add函数调用了linkLast函数
2. linkLast函数以插入节点数据为形参
2.1 将LinkedList中的last(末尾标志指针,永远指向最后一个元素节点)赋值给l节点
2.2 创建新的节点newNode,节点的前驱指向l节点,节点的后驱设置为空
2.3 将last指向刚刚创建的新节点newNode
2.4 判断l节点是否是空节点,如果是将first(首部指针,永远指向第一个元素节点)指向新创建的newNode节点,如果不是,将l的后驱指向newNode
2.5 LinkedList大小加1
2.6 标志结构性变化的变量加1
3. 返回真值
注2:很显然,add函数调用了linkLast函数,而linkLast函数采用的是尾插法,也就是说,作为末尾追加元素时,LinkedList采用了尾插法作为元素新建和插入。
Q4:为什么LinkedList的add是末尾追加元素呢?追加元素为什么会采用尾插法,而不是头插法呢?
Q5:前面提到了LinkedList是双向链表,也就说头插法和尾插法都可以?那么什么情况适合头插法?什么情况适合尾插法呢?
3. getter setter函数
getter函数
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) {
// 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;
}
}
1. get函数调用了checkElementIndex函数
2. checkElementIndex函数调用了isElementIndex函数
3. isElementIndex则是简单的对index索引值的判断,判断该值是否合法,如果不合法就抛出IndexOutOfBoundsException异常
4. 合法就继续调用了node函数
5. node函数实现比较有技巧性(重点!请注意一下)
注3:node函数采用了二分法来循环遍历来查找所需要的index上的元素值
5.1 在if(index<(size>>1))判断语句中,可以看见了判断是从size右移了一位除以2,即判断了index是否小于size的一半,如果是从前往后遍历,根据后驱慢慢查找,直到处于索引的位置
5.2 如果index大于size的一半,则是从后往前遍历,根据前驱慢慢查找,直到处于索引的位置
Q6:关于node节点的查找函数为什么是二分法进行查找?在二分法中为什么又采用了位运算?
Q7:node节点的查找遍历的时间复杂度如何?
setter函数
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
1. 先校验索引值是否合法
2. 合法就调用了node函数
3. node函数其实就是在上文getter方法中也出现过的node函数,这个函数实际上就是利用了二分法来查找对应index位置上的node节点
4. 将该位置的节点作为旧值返回
5. 设置传入的新值
注4:值得注意的是,LinkedList和ArrayList不同,其索引值不能同数组下标一样,直接返回(实际上ArrayList是随机访问,LinkedList则是顺序访问),因此需要设立循环,根据前驱或者后驱,循环遍历直到该索引值的节点处,因此,时间复杂度就是O(n),比起ArrayList的O(1)要大一些。
4. addAll函数
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
针对addAll函数所实现的功能大概都有这么几个情况
1. 原列表为空,没有任何节点,传入的集合节点,直接添加进去
2. 原列表有节点,在节点末尾追加传入的集合节点,这是addAll传入参数collection的情况
3. 原列表有节点,在指定的index位置上追加传入的集合节点,这是allAll传入index,collection的情况
可以看见addAll(collection)也是调用了addAll(index,collection),换句话说,addAll主要依靠addAll(index,collection)实现
那么我们来看下这个函数的实现
1. 首先是调用了checkPositionIndex函数针对index所处位置合法性的校验,如果不合法就要抛出异常
2. 将集合中元素转为数组
3. 对数组大小进行判断,如果数组是空的,就返回false,否则就下一步
4. 设立两个节点,一个节点pred指向要添加节点的前一个节点,一个节点succ指向要添加节点的位置
5. 如果要添加index的位置等于大小,就说明了是在末尾处添加(情况3),将要succ指向null,pred指向原列表的最后一个节点;如果不是,则将succ指向要添加的节点的位置(实际上是在添加的节点的前一个节点处),pred指向该节点的前驱节点(情况2)
6. 循环遍历数组,遍历一个,就创建一个节点,使用尾插法在要添加的节点处进行链接,此处需要对pred是否为空进行判断,如果为空(情况1),就指向新创建的节点,如果不是,则将pred后驱指向新节点
7. 如果succ是null,则将last指针指向添加链接完成后的pred节点
8. 如果不是,则将上面的pred的后驱指向succ,然后succ的前驱指向pred
9. 改变大小,modCount++,返回真值
注5:针对批量添加的情况,比较复杂,建议读者们手动画图模拟一下插入的过程
Q8:为什么会将集合元素要先转为数组,再遍历创建新节点进行链接呢?和直接在集合中遍历创建节点链接有什么异同?
Q9:设立pred,succ两个临时节点指针是否多余?
5. indexOf、lastindexOf、contains、containsAll函数
关于indexOf的函数实现如下
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
可以看出,步骤都比较简单
1. 设立index为0
2. 对object进行判空
3. 如果空值,从first节点开始,按照后驱遍历查找为空值的节点,如果找到了,就返回该节点的index
4. 如果非空,也同样的是从firs节点开始,按照后驱遍历查找为空值的节点,如果找到了,就返回该节点的index
5. 都没找到的,就返回-1
注6:针对空值和非空值的查找,其查找方式都是一样的,只是在对比值的时候,由于空值的特殊性只能采用==来判断,而非空值可以直接equals来判断
关于lastIndexOf函数实现
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}
同样可以看出,跟indexOf的实现方式是一样的,只不过遍历是从后驱开始慢慢查找的
再看看contains和containsAll函数实现
public boolean contains(Object o) {
return indexOf(o) != -1;
}
//AbstractCollection中的实现
public boolean containsAll(Collection<?> c) {
for (Object e : c)
if (!contains(e))
return false;
return true;
}
很显然,contains调用了indexOf的方法,即其实现也是用indexOf实现的,将indexOf返回跟-1进行判断就可以得出元素是否在列表中的结果了,然后返回结果
注7:需要注意的是,containsAll的实现在LinkedList中并没有,取而代之则是AbstractCollection中的containsAll实现,而AbstractCollection中的containsAll实现也是循环调用了contains方法
Q10:不知道为什么在LinkedList中没有实现containsAll的方法?
6. remove函数
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == 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;
}
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
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;
}
remove重载了两个函数,一个是参数是index,一个参数是object,一个是根据index移除节点,一个是根据object移除节点
根据index索引来移除节点的函数实现中要先对index进行合法性检查,再调用了unlink函数
根据object移除节点要对object是否为空值进行判断,采取不同的判断方式,遍历查找到要移除的节点位置,再调用unlink函数
重点来了,我们需要看看unlink是怎么移除节点的
1. 设立要返回的元素为该节点的元素值,设立prev为该节点的前驱节点,next为该节点的后驱节点
2. 对前驱节点prev进行判空,说明是否是首节点,如果不是首节点,则将前驱的后驱节点指向next的节点,然后把该节点的前驱置空
3. 对后驱节点next进行判空,说明是否是尾节点,如果不是尾节点,则将后驱的前驱节点指向prev节点,然后把该节点的后驱置空
4. 节点上的元素置空,大小减一,mouCount加一,返回删除节点的元素值
7. retainAll、removeAll函数
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<?> it = iterator();
while (it.hasNext()) {
if (c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<E> it = iterator();
while (it.hasNext()) {
if (!c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
可以看出,removeAll和retainAll其实现逻辑基本都是一样的
1. 调用requireNonNull对集合是否为空进行判断
2. 把修改标志modified设置为false
3. 取得迭代器iterator
4. 迭代器循环获取下一个节点元素
5. removeAll是针对集合中存在元素进行判断,然后调用remove进行删除;retainAll则是针对集合中非存在的元素进行判断,不存在就调用remove删除
6. 返回modified
Q11:为什么retainAll、removeAll使用了迭代器进行删除或者保留?和ArrayList有什么异同?
8. 其他常用的函数
9. 迭代器