LinkedList源码阅分析

﹏ヽ暗。殇╰゛Y 2023-07-23 11:58 38阅读 0赞

LinkedList里面涉及到的一些操作,非常细致,以避免出现的空指针,理解后对于其优点与确定会有一个更加整体的认识吧。

继承关系图(对比ArrayList)

70

元素的存储结构
在LinkedList中,每一个元素都是Node存储,Node拥有一个存储值的item与一个前驱prev和一个后继next,如下:

// 典型的链表结构

  1. private static class Node<E> {
  2. E item;// 存储元素
  3. Node<E> next;// 指向上一个元素
  4. Node<E> prev;// 指向下一个元素
  5. Node(Node<E> prev, E element, Node<E> next) {
  6. this.item = element;
  7. this.next = next;
  8. this.prev = prev;
  9. }
  10. }

构造函数与成员变量
变量主要有3个:

  1. transient int size = 0;//当前列表的元素个数
  2. /**
  3. * Pointer to first node.
  4. * Invariant: (first == null && last == null) ||
  5. * (first.prev == null && first.item != null)
  6. */
  7. transient Node<E> first;// 第一个元素
  8. /**
  9. * Pointer to last node.
  10. * Invariant: (first == null && last == null) ||
  11. * (last.next == null && last.item != null)
  12. */
  13. transient Node<E> last;// 最后一个元素

在LinkedList中的构造函数有两个,一个是无参的,另一个是带Collection参数的。

  1. public LinkedList() {}//无参构造函数
  2. public LinkedList(Collection<? extends E> c) {
  3. this();
  4. addAll(c);//将c中的元素都添加到此列表中
  5. }

其添加的过程中,此时size = 0,如下:

  1. public boolean addAll(Collection<? extends E> c) {
  2. return addAll(size, c);//此时 size == 0
  3. }

如果index==size,则添加c中的元素到列表的尾部;否则,添加的第index个元素的前面;

  1. public boolean addAll(int index, Collection<? extends E> c) {
  2. // 检查位置是否合法 位置是[0,size],注意是闭区间 否则报异常
  3. checkPositionIndex(index);
  4. Object[] a = c.toArray();// 得到一个元素数组
  5. int numNew = a.length;// c中元素的数量
  6. if (numNew == 0)
  7. return false;// 没有元素,添加失败
  8. // 主要功能是找到第size个元素的前驱和后继。得到此元素需要分情况讨论。
  9. // 这段代码是各种情况的总和,可能有一点点容易懵逼。
  10. Node<E> pred, succ;// 前驱与后继
  11. if (index == size) {// 如果位置与当前的size相同
  12. succ = null;// 无后继
  13. pred = last;// 前驱为last,即第size个元素(最后一个元素)
  14. } else {// 若与size不同,即index位于[0, size)之间
  15. succ = node(index);// 后继为第index个元素
  16. pred = succ.prev;// 前驱为后继的前驱
  17. }// 后文有详细的图片说明
  18. // 开始逐个插入
  19. for (Object o : a) {
  20. @SuppressWarnings("unchecked") E e = (E) o;
  21. // 新建一个以pred为前驱、null为后继、值为e的节点
  22. Node<E> newNode = new Node<>(pred, e, null);
  23. if (pred == null)// 前驱为空,则此节点被当做列表的第一个节点
  24. first = newNode;
  25. else// 规避掉了NullPointerException,感觉又达到了目的,又实现了逻辑
  26. pred.next = newNode;// 不为空,则将前驱的后继改成当前节点
  27. pred = newNode;// 将前驱改成当前节点,以便后续添加c中其它的元素
  28. }
  29. // 至此,c中元素已添加到链表上,但链表中从size开始的那些元素还没有链接到列表上
  30. // 此时就需要利用到之前找出来的succ值,它是作为这个c的整体后继
  31. if (succ == null) {// 如果后继为空,说明无整体后继
  32. last = pred;// c的最后一个元素应当作为列表的尾元素
  33. } else {// 有整体后继
  34. pred.next = succ;// pred即c中的最后一个元素,其后继指向succ,即整体后继
  35. succ.prev = pred;// succ的前驱指向c中的最后一个元素
  36. }
  37. // 添加完毕,修改参数
  38. size += numNew;
  39. modCount++;
  40. return true;
  41. }

返回序号为index的元素节点。看这段代码中的if语句,真的是佩服,这样写代码,都可以这样减少查找次数。

  1. Node<E> node(int index) {
  2. // assert isElementIndex(index);
  3. // 这个地方很有意思。视其与中值得差距,觉得从前遍历还是从后遍历。
  4. if (index < (size >> 1)) {
  5. Node<E> x = first;
  6. // 循环index次 迭代到所需要的元素
  7. for (int i = 0; i < index; i++)
  8. x = x.next;
  9. return x;
  10. } else {
  11. Node<E> x = last;
  12. // 循环size-1-index次
  13. for (int i = size - 1; i > index; i--)
  14. x = x.prev;
  15. return x;
  16. }
  17. }

测试代码以及验证输出如下:

  1. public class Main {
  2. public static void main(String[] args) {
  3. List<String> list = new LinkedList<>(Arrays.asList("1", "2", "3"));
  4. System.out.println(list.toString());
  5. list.addAll(2, Arrays.asList("4", "5"));
  6. System.out.println(list.toString());
  7. list.addAll(0, Arrays.asList("6", "7"));
  8. System.out.println(list.toString());
  9. }
  10. }
  11. ---
  12. [1, 2, 3]
  13. [1, 2, 4, 5, 3]
  14. [6, 7, 1, 2, 4, 5, 3]

增加元素
对于向列表中添加元素,先看一组基本的添加操作,具体如下:

将e链接成列表的第一个元素
源代码以及相应的分析如下:

  1. private void linkFirst(E e) {
  2. final Node<E> f = first;
  3. // 前驱为空,值为e,后继为f
  4. final Node<E> newNode = new Node<>(null, e, f);
  5. first = newNode;// first指向newNode
  6. // 此时的f有可能为null
  7. if (f == null)// 若f为空,则表明列表中还没有元素
  8. last = newNode;// last也应该指向newNode
  9. else
  10. f.prev = newNode;// 否则,前first的前驱指向newNode
  11. size++;
  12. modCount++;
  13. }

其过程大致如下两图所示:
初始状态:

70 1

后续状态:
添加元素作为第一个元素时,所需要做的工作,有下列所述:
首先,获取第一个节点,然后将该节点的前驱指向新添加的元素所在的节点;
接着,将新添加的节点的后继指向前第一个节点;
最后,将first指向新添加的元素的节点。添加完毕。

70 2

将e链接为最后一个元素
源代码以及相应的解释如下:

  1. void linkLast(E e) {
  2. final Node<E> l = last;// 找到最后一个节点
  3. // 前驱为前last,值为e,后继为null
  4. final Node<E> newNode = new Node<>(l, e, null);
  5. last = newNode;// last一定会指向此节点
  6. if (l == null)// 最后一个节点为空,说明列表中无元素
  7. first = newNode;// first同样指向此节点
  8. else
  9. l.next = newNode;// 否则,前last的后继指向当前节点
  10. size++;
  11. modCount++;
  12. }

其操作过程与前述linkFirst()的过程类似,因此其替换后的示意图如下:

70 3

将e链接到节点succ前
源代码以及相应的解析如下:

  1. void linkBefore(E e, Node<E> succ) {
  2. // assert succ != null;
  3. final Node<E> pred = succ.prev; // 找到succ的前驱
  4. // 前驱为pred,值为e,后继为succ
  5. final Node<E> newNode = new Node<>(pred, e, succ);
  6. // 将succ的前驱指向当前节点
  7. succ.prev = newNode;
  8. if (pred == null)// pred为空,说明此时succ为首节点
  9. first = newNode;// 指向当前节点
  10. else
  11. pred.next = newNode;// 否则,将succ之前的前驱的后继指向当前节点
  12. size++;
  13. modCount++;
  14. }

这个操作有点类似将上述的两个操作整合到一起。其操作简图如下:

70 4

有了上述的分析,我们再来看一些添加的操作,这些操作基本上是做了一些逻辑判断,然后再调用上述三个方法去实现添加功能,这里略过就好。

  1. public boolean add(E e) {
  2. linkLast(e);
  3. return true;
  4. }
  5. // 只有这个是有一点逻辑的
  6. public void add(int index, E element) {
  7. checkPositionIndex(index);
  8. if (index == size)// 为最后一个节点,当然是添加到最后一个~
  9. linkLast(element);
  10. else
  11. linkBefore(element, node(index));
  12. }
  13. public void addFirst(E e) {
  14. linkFirst(e);
  15. }
  16. public void addLast(E e) {
  17. linkLast(e);
  18. }

删除元素
删除就是添加过程的逆过程。同样,在分析我们使用的接口前,先分析几个我们看不到的方法,如下:

  1. 删除首节点
  2. private E unlinkFirst(Node<E> f) {
  3. // assert f == first && f != null;别忽略这里的断言
  4. final E element = f.item;// 取出首节点中的元素
  5. final Node<E> next = f.next;// 取出首节点中的后继
  6. f.item = null;
  7. f.next = null; // help GC
  8. first = next;// first指向前first的后继,也就是列表中的2号位
  9. if (next == null)// 如果此时2号位为空,那么列表中此时已无节点
  10. last = null;// last指向null
  11. else
  12. next.prev = null;// 首节点无前驱
  13. size--;
  14. modCount++;
  15. return element;// 返回首节点保存的元素值
  16. }

删除尾节点
此处的操作与删除首节点的操作类似。

  1. private E unlinkLast(Node<E> l) {
  2. // assert l == last && l != null;别忽略这里的断言
  3. final E element = l.item;// 取出尾节点中的元素
  4. final Node<E> prev = l.prev;// 取出尾节点中的后继
  5. l.item = null;
  6. l.prev = null; // help GC
  7. last = prev;// last指向前last的前驱,也就是列表中的倒数2号位
  8. if (prev == null)// 如果此时倒数2号位为空,那么列表中已无节点
  9. first = null;// first指向null
  10. else
  11. prev.next = null;// 尾节点无后继
  12. size--;
  13. modCount++;
  14. return element;// 返回尾节点保存的元素值
  15. }

删除某个非空节点
这个也类似添加元素时的第三个基本操作,与结合了上述两个操作有点类似。

  1. // x即为要删除的节点
  2. E unlink(Node<E> x) {
  3. // assert x != null;
  4. final E element = x.item;// 保存x的元素值
  5. final Node<E> next = x.next;// 保存x的后继
  6. final Node<E> prev = x.prev;// 保存x的前驱
  7. if (prev == null) {// 前驱为null,说明x为首节点
  8. first = next;// first指向x的后继
  9. } else {
  10. prev.next = next;// x的前驱的后继指向x的后继,即略过了x
  11. x.prev = null;// x.prev已无用处,置空引用
  12. }
  13. if (next == null) {// 后继为null,说明x为尾节点
  14. last = prev;// last指向x的前驱
  15. } else {
  16. next.prev = prev;// x的后继的前驱指向x的前驱,即略过了x
  17. x.next = null;// x.next已无用处,置空引用
  18. }
  19. x.item = null;// 引用置空
  20. size--;
  21. modCount++;
  22. return element;// 返回所删除的节点的元素值
  23. }

有了上面的几个函数作为支撑,我们再来看下面的几个我们能用来删除节点的方法,他们也基本上是在一些逻辑判断的基础之上,再调用上述的基本操作:

  1. public E removeFirst() {
  2. final Node<E> f = first;
  3. if (f == null)
  4. throw new NoSuchElementException();
  5. return unlinkFirst(f);
  6. }
  7. public E removeLast() {
  8. final Node<E> l = last;
  9. if (l == null)
  10. throw new NoSuchElementException();
  11. return unlinkLast(l);
  12. }
  13. // 遍历列表中所有的节点,找到相同的元素,然后删除它
  14. public boolean remove(Object o) {
  15. if (o == null) {
  16. for (Node<E> x = first; x != null; x = x.next) {
  17. if (x.item == null) {
  18. unlink(x);
  19. return true;
  20. }
  21. }
  22. } else {
  23. for (Node<E> x = first; x != null; x = x.next) {
  24. if (o.equals(x.item)) {
  25. unlink(x);
  26. return true;
  27. }
  28. }
  29. }
  30. return false;
  31. }
  32. public E remove(int index) {
  33. checkElementIndex(index);
  34. return unlink(node(index));
  35. }

修改元素
通过遍历,循环index次,获取到相应的节点后,再通过节点来修改元素值。

  1. public E set(int index, E element) {
  2. checkElementIndex(index);
  3. Node<E> x = node(index);// 获取到需要修改元素的节点
  4. E oldVal = x.item;// 保存之前的值
  5. x.item = element;// 修改
  6. return oldVal;// 返回修改前的值
  7. }
  8. 查询元素
  9. 通过位置,循环index次,获取到节点,然后返回该节点中元素的值public E get(int index) {
  10. checkElementIndex(index);
  11. return node(index).item;// 获取节点,并返回节点中的元素值
  12. }
  13. 还有两个获取首尾节点的元素的方法:
  14. public E getFirst() {
  15. final Node<E> f = first;
  16. if (f == null)
  17. throw new NoSuchElementException();
  18. return f.item;
  19. }
  20. public E getLast() {
  21. final Node<E> l = last;
  22. if (l == null)
  23. throw new NoSuchElementException();
  24. return l.item;
  25. }
  26. 获取元素位置
  27. 0开始往后遍历public int indexOf(Object o) {
  28. int index = 0;
  29. if (o == null) {// null时分开处理
  30. for (Node<E> x = first; x != null; x = x.next) {
  31. if (x.item == null)// 说明找到
  32. return index;// 返回下标
  33. index++;
  34. }
  35. } else {
  36. for (Node<E> x = first; x != null; x = x.next) {
  37. if (o.equals(x.item))// 说明找到
  38. return index;// 返回下标
  39. index++;
  40. }
  41. }
  42. return -1;// 未找到,返回-1
  43. }
  44. size - 1开始遍历。基本操作与上述操作类似,只是起始位置不同。
  45. public int lastIndexOf(Object o) {
  46. int index = size;
  47. if (o == null) {
  48. for (Node<E> x = last; x != null; x = x.prev) {
  49. index--;
  50. if (x.item == null)
  51. return index;
  52. }
  53. } else {
  54. for (Node<E> x = last; x != null; x = x.prev) {
  55. index--;
  56. if (o.equals(x.item))
  57. return index;
  58. }
  59. }
  60. return -1;
  61. }

额外的话
在上面的诸多函数中,有许多是需要进行位置判断的。在源码中,位置判断有两个函数,一个是下标,一个是位置。看到这两个函数,确实是有一些感触,这确实是需要比较强的总结能力以及仔细的观察能力。

  1. // 下标,保证数组访问不越界。
  2. private boolean isElementIndex(int index) {
  3. return index >= 0 && index < size;
  4. }
  5. // 位置
  6. private boolean isPositionIndex(int index) {
  7. return index >= 0 && index <= size;
  8. }

后记
LinkedList还实现了Queue这个接口,在实现这些接口时,仍然是做一些逻辑处理,然后调用上面所描述的基本操作,如link()、unlink()之类的,因此不再分析。还有其中的关于序列化、Iterator这两块,与ArrayList的实现也是不尽相同的,故在此可参考ArrayList中的解析。

发表评论

表情:
评论列表 (有 0 条评论,38人围观)

还没有评论,来说两句吧...

相关阅读