(注:本文内容基于JDK1.6)

一、概述:

LinkedList与ArrayList一样实现List接口,只是ArrayList是List接口的大小可变数组的实现,LinkedList是List接口链表的实现。基于链表实现的方式使得LinkedList在插入和删除时更优于ArrayList,而随机访问则比ArrayList逊色些。

LinkedList实现所有可选的列表操作,并允许所有的元素包括null。

除了实现 List 接口外,LinkedList 类还为在列表的开头及结尾 get、remove 和 insert 元素提供了统一的命名方法。这些操作允许将链接列表用作堆栈、队列或双端队列。

此类实现 Deque 接口,为 add、poll 提供先进先出队列操作,以及其他堆栈和双端队列操作。

所有操作都是按照双重链接列表的需要执行的。在列表中编索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。同时,与ArrayList一样此实现不是同步的。

二、源码分析:

LinkedList的定义:

public class LinkedList<E>extends AbstractSequentialList<E>implements List<E>, Deque<E>, Cloneable, java.io.Serializable

从这段代码中我们可以清晰地看出LinkedList继承AbstractSequentialList,实现List、Deque、Cloneable、Serializable。其中AbstractSequentialList提供了 List 接口的骨干实现,从而最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作,从而以减少实现List接口的复杂度。Deque一个线性 collection,支持在两端插入和移除元素,定义了双端队列的操作。
在LinkedList中提供了两个基本属性size、header。

private transient Entry<E> header = new Entry<E>(null, null, null);
private transient int size = 0;

其中size表示的LinkedList的大小,header表示链表的表头,Entry为节点对象。

private static class Entry<E> {E element;        //元素节点Entry<E> next;    //下一个元素Entry<E> previous;  //上一个元素Entry(E element, Entry<E> next, Entry<E> previous) {this.element = element;this.next = next;this.previous = previous;}}

上面为Entry对象的源代码,Entry为LinkedList的内部类,它定义了存储的元素。该元素的前一个元素、后一个元素,这是典型的双向链表定义方式。

来看 LinkedList的构造方法::

public LinkedList() {header.next = header.previous = header;
}
public LinkedList(Collection<? extends E> c) {this();addAll(c);
}

LinkedList提供了两个构造方法。第一个构造方法不接受参数,只是将header节点的前一节点和后一节点都设置为自身(注意,这个是一个双向循环链表,如果不是循环链表,空链表的情况应该是header节点的前一节点和后一节点均为null),这样整个链表其实就只有header一个节点,用于表示一个空的链表。第二个构造方法接收一个Collection参数c,调用第一个构造方法构造一个空的链表,之后通过addAll将c中的元素全部添加到链表中。来看addAll的内容。

public boolean addAll(Collection<? extends E> c) {return addAll(size, c);
}
// index参数指定collection中插入的第一个元素的位置
public boolean addAll(int index, Collection<? extends E> c) {// 插入位置超过了链表的长度或小于0,报IndexOutOfBoundsException异常if (index < 0 || index > size)throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);Object[] a = c.toArray();
int numNew = a.length;
// 若需要插入的节点个数为0则返回false,表示没有插入元素if (numNew==0)return false;modCount++;// 保存index处的节点。插入位置如果是size,则在头结点前面插入,否则获取index处的节点
Entry<E> successor = (index==size ? header : entry(index));
// 获取前一个节点,插入时需要修改这个节点的next引用
Entry<E> predecessor = successor.previous;
// 按顺序将a数组中的第一个元素插入到index处,将之后的元素插在这个元素后面for (int i=0; i<numNew; i++) {
// 结合Entry的构造方法,这条语句是插入操作,相当于C语言中链表中插入节点并修改指针Entry<E> e = new Entry<E>((E)a[i], successor, predecessor);// 插入节点后将前一节点的next指向当前节点,相当于修改前一节点的next指针predecessor.next = e;// 相当于C语言中成功插入元素后将指针向后移动一个位置以实现循环的功能predecessor = e;
}
// 插入元素前index处的元素链接到插入的Collection的最后一个节点
successor.previous = predecessor;
// 修改sizesize += numNew;return true;
}

构造方法中的调用了addAll(Collection<? extends E> c)方法,而在addAll(Collection<? extends E> c)方法中仅仅是将size当做index参数调用了addAll(int index,Collection<? extends E> c)方法。

private Entry<E> entry(int index) {if (index < 0 || index >= size)throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);Entry<E> e = header;// 根据这个判断决定从哪个方向遍历这个链表if (index < (size >> 1)) {for (int i = 0; i <= index; i++)e = e.next;} else {// 可以通过header节点向前遍历,说明这个一个循环双向链表,header的previous指向链表的最后一个节点,这也验证了构造方法中对于header节点的前后节点均指向自己的解释for (int i = size; i > index; i--)e = e.previous;}return e;}

结合上面代码中的注释及双向循环链表的知识,应该很容易理解LinkedList构造方法所涉及的内容。下面开始分析LinkedList的其他方法。

add(E e):

 public boolean add(E e) {addBefore(e, header);return true;}

从上面的代码可以看出,add(E e)方法只是调用了addBefore(E e,Entry<E> entry)方法,并且返回true。

 addBefore(E e,Entry<E> entry):

private Entry<E> addBefore(E e, Entry<E> entry) {Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);newEntry.previous.next = newEntry;newEntry.next.previous = newEntry;size++;modCount++;return newEntry;
}

addBefore(E e,Entry<E> entry)方法是个私有方法,所以无法在外部程序中调用(当然,这是一般情况,你可以通过反射上面的还是能调用到的)。

addBefore(E e,Entry<E> entry)先通过Entry的构造方法创建e的节点newEntry(包含了将其下一个节点设置为entry,上一个节点设置为entry.previous的操作,相当于修改newEntry的“指针”),之后修改插入位置后newEntry的前一节点的next引用和后一节点的previous引用,使链表节点间的引用关系保持正确。之后修改和size大小和记录modCount,然后返回新插入的节点。

总结,addBefore(E e,Entry<E> entry)实现在entry之前插入由e构造的新节点。而add(E e)实现在header节点之前插入由e构造的新节点。

add(int index,E e):

public void add(int index, E element) {addBefore(element, (index==size ? header : entry(index)));}

也是调用了addBefore(E e,Entry<E> entry)方法,只是entry节点由index的值决定。

构造方法,addAll(Collection<? extends E> c),add(E e),addBefor(E e,Entry<E> entry)方法可以构造链表并在指定位置插入节点,为了便于理解,下面给出插入节点的示意图。

addFirst(E e):

 public void addFirst(E e) {addBefore(e, header.next);}

addLast(E e):

public void addLast(E e) {addBefore(e, header);}

看上面的示意图,结合addBefore(E e,Entry<E> entry)方法,很容易理解addFrist(E e)只需实现在header元素的下一个元素之前插入,即示意图中的一号之前。addLast(E e)只需在实现在header节点前(因为是循环链表,所以header的前一个节点就是链表的最后一个节点)插入节点(插入后在2号节点之后)。

clear():

public void clear() {
Entry<E> e = header.next;
// e可以理解为一个移动的“指针”,因为是循环链表,所以回到header的时候说明已经没有节点了
while (e != header) {// 保留e的下一个节点的引用Entry<E> next = e.next;// 接触节点e对前后节点的引用e.next = e.previous = null;// 将节点e的内容置空e.element = null;// 将e移动到下一个节点e = next;
}
// 将header构造成一个循环链表,同构造方法构造一个空的LinkedList
header.next = header.previous = header;
// 修改sizesize = 0;modCount++;
}

上面代码中的注释已经足以解释这段代码的逻辑,需要注意的是提到的“指针”仅仅是概念上的类比,Java并不存在“指针”的概念,而只有引用,为了便于理解所以部分说明使用了“指针”。

contains(Object o):

public boolean contains(Object o) {return indexOf(o) != -1;}

仅仅只是判断o在链表中的索引。先看indexOf(Object o)方法。

public int indexOf(Object o) {int index = 0;if (o==null) {for (Entry e = header.next; e != header; e = e.next) {if (e.element==null)return index;index++;}} else {for (Entry e = header.next; e != header; e = e.next) {if (o.equals(e.element))return index;index++;}}return -1;
}

indexOf(Object o)判断o链表中是否存在节点的element和o相等,若相等则返回该节点在链表中的索引位置,若不存在则放回-1。

contains(Object o)方法通过判断indexOf(Object o)方法返回的值是否是-1来判断链表中是否包含对象o。

 element():

public E element() {return getFirst();}

 getFirst():

public E getFirst() {if (size==0)throw new NoSuchElementException();return header.next.element;}

element()方法调用了getFirst()返回链表的第一个节点的元素。为什么要提供功能一样的两个方法,像是包装了一下名字?其实这只是为了在不同的上下文“语境”中能通过更贴切的方法名调用罢了。

get(int index):

public E get(int index) {return entry(index).element;}

get(int index)方法用于获得指定索引位置的节点的元素。它通过entry(int index)方法获取节点。entry(int index)方法遍历链表并获取节点,在上面有说明过,不再陈述。

 set(int index,E element):

public E set(int index, E element) {Entry<E> e = entry(index);E oldVal = e.element;e.element = element;return oldVal;}

先获取指定索引的节点,之后保留原来的元素,然后用element进行替换,之后返回原来的元素。

getLast():

public E getLast()  {if (size==0)throw new NoSuchElementException();return header.previous.element;}

getLast()方法和getFirst()方法类似,只是获取的是header节点的前一个节点的元素。因为是循环链表,所以header节点的前一节点就是链表的最后一个节点。

lastIndexOf(Object o):

public int lastIndexOf(Object o) {int index = size;if (o==null) {for (Entry e = header.previous; e != header; e = e.previous) {index--;if (e.element==null)return index;}} else {for (Entry e = header.previous; e != header; e = e.previous) {index--;if (o.equals(e.element))return index;}}return -1;
}

因为查找的是last index,即最后一次出现的位置,所以采用由后向前的遍历方式。因为采用了有后向前的遍历,所以index被赋值为size,并且循环体内执行时都进行减操作。分两种情况判断是否存在,分别是null和不为空。

 offer(E e):

public boolean offer(E e) {return add(e);}

在链表尾部插入元素。

offerFirst(E e):

public boolean offerFirst(E e) {addFirst(e);return true;}

在链表开头插入元素。

offerLast(E e):

public boolean offerLast(E e) {addLast(e);return true;}

在链表末尾插入元素。

上面这三个方法都只是调用了相应的add方法,同样只是提供了不同的方法名在不同的语境下使用。

peek():

 public E peek() {if (size==0)return null;return getFirst();}

peekFirst():

public E peekFirst() {if (size==0)return null;return getFirst();}

 peekLast():

public E peekLast() {if (size==0)return null;return getLast();}

上面的三个方法也很简单,只是调用了对应的get方法。

poll():

public E poll() {if (size==0)return null;return removeFirst();}

pollFirst():

public E pollFirst() {if (size==0)return null;return removeFirst();}

pollLast():

public E pollLast() {if (size==0)return null;return removeLast();}

poll相关的方法都是获取并移除某个元素。都是和remove操作相关。

pop():

public E pop() {return removeFirst();}

push(E e):

public void push(E e) {addFirst(e);}

这两个方法对应栈的操作,即弹出一个元素和压入一个元素,仅仅是调用了removeFirst()和addFirst()方法。

下面集中看remove相关操作的方法。

remove():

public E remove() {return removeFirst();}

remove(int index):

public E remove(int index) {return remove(entry(index));}

remove(Object o):

public boolean remove(Object o) {if (o==null) {for (Entry<E> e = header.next; e != header; e = e.next) {if (e.element==null) {remove(e);return true;}}} else {for (Entry<E> e = header.next; e != header; e = e.next) {if (o.equals(e.element)) {remove(e);return true;}}}return false;
}

removeFirst():

public E removeFirst() {return remove(header.next);}

removeLast():

public E removeLast() {return remove(header.previous);}

 removeFirstOccurrence():

public boolean removeFirstOccurrence(Object o) {return remove(o);}

removeLastOccurence():

public boolean removeLastOccurrence(Object o) {if (o==null) {for (Entry<E> e = header.previous; e != header; e = e.previous) {if (e.element==null) {remove(e);return true;}}} else {for (Entry<E> e = header.previous; e != header; e = e.previous) {if (o.equals(e.element)) {remove(e);return true;}}}return false;
}

几个remove方法最终都是调用了一个私有方法:remove(Entry<E> e),只是其他简单逻辑上的区别。下面分析remove(Entry<E> e)方法。

private E remove(Entry<E> e) {if (e == header)throw new NoSuchElementException();// 保留将被移除的节点e的内容
E result = e.element;
// 将前一节点的next引用赋值为e的下一节点e.previous.next = e.next;// 将e的下一节点的previous赋值为e的上一节点e.next.previous = e.previous;// 上面两条语句的执行已经导致了无法在链表中访问到e节点,而下面解除了e节点对前后节点的引用
e.next = e.previous = null;
// 将被移除的节点的内容设为null
e.element = null;
// 修改size大小size--;modCount++;// 返回移除节点e的内容return result;
}

clone():

public Object clone() {LinkedList<E> clone = null;try {clone = (LinkedList<E>) super.clone();} catch (CloneNotSupportedException e) {throw new InternalError();}clone.header = new Entry<E>(null, null, null);clone.header.next = clone.header.previous = clone.header;clone.size = 0;clone.modCount = 0;for (Entry<E> e = header.next; e != header; e = e.next)clone.add(e.element);return clone;
}

调用父类的clone()方法初始化对象链表clone,将clone构造成一个空的双向循环链表,之后将header的下一个节点开始将逐个节点添加到clone中。最后返回克隆的clone对象。

toArray():

public Object[] toArray() {Object[] result = new Object[size];int i = 0;for (Entry<E> e = header.next; e != header; e = e.next)result[i++] = e.element;return result;
}

创建大小和LinkedList相等的数组result,遍历链表,将每个节点的元素element复制到数组中,返回数组。

toArray(T[] a):

public <T> T[] toArray(T[] a) {if (a.length < size)a = (T[])java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size);int i = 0;Object[] result = a;for (Entry<E> e = header.next; e != header; e = e.next)result[i++] = e.element;if (a.length > size)a[size] = null;return a;
}

先判断出入的数组a的大小是否足够,若大小不够则拓展。这里用到了发射的方法,重新实例化了一个大小为size的数组。之后将数组a赋值给数组result,遍历链表向result中添加的元素。最后判断数组a的长度是否大于size,若大于则将size位置的内容设置为null。返回a。

从代码中可以看出,数组a的length小于等于size时,a中所有元素被覆盖,被拓展来的空间存储的内容都是null;若数组a的length的length大于size,则0至size-1位置的内容被覆盖,size位置的元素被设置为null,size之后的元素不变。

LinkedList的Iterator:

除了Entry,LinkedList还有一个内部类:ListItr。ListItr实现了ListIterator接口,可知它是一个迭代器,通过它可以遍历修改LinkedList。在LinkedList中提供了获取ListItr对象的方法:listIterator(int index)。

public ListIterator<E> listIterator(int index) {return new ListItr(index);}

该方法只是简单的返回了一个ListItr对象。

LinkedList中还有通过集成获得的listIterator()方法,该方法只是调用了listIterator(int index)并且传入0。

下面详细分析ListItr。

private class ListItr implements ListIterator<E> {
// 最近一次返回的节点,也是当前持有的节点private Entry<E> lastReturned = header;// 对下一个元素的引用private Entry<E> next;// 下一个节点的indexprivate int nextIndex;private int expectedModCount = modCount;// 构造方法,接收一个index参数,返回一个ListItr对象ListItr(int index) {// 如果index小于0或大于size,抛出IndexOutOfBoundsException异常if (index < 0 || index > size)throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);// 判断遍历方向if (index < (size >> 1)) {// next赋值为第一个节点next = header.next;// 获取指定位置的节点for (nextIndex=0; nextIndex<index; nextIndex++)next = next.next;} else {
// else中的处理和if块中的处理一致,只是遍历方向不同next = header;for (nextIndex=size; nextIndex>index; nextIndex--)next = next.previous;}}// 根据nextIndex是否等于size判断时候还有下一个节点(也可以理解为是否遍历完了LinkedList)public boolean hasNext() {return nextIndex != size;}// 获取下一个元素public E next() {checkForComodification();// 如果nextIndex==size,则已经遍历完链表,即没有下一个节点了(实际上是有的,因为是循环链表,任何一个节点都会有上一个和下一个节点,这里的没有下一个节点只是说所有节点都已经遍历完了)if (nextIndex == size)throw new NoSuchElementException();// 设置最近一次返回的节点为next节点lastReturned = next;// 将next“向后移动一位”next = next.next;// index计数加1nextIndex++;// 返回lastReturned的元素return lastReturned.element;}public boolean hasPrevious() {return nextIndex != 0;}// 返回上一个节点,和next()方法相似public E previous() {if (nextIndex == 0)throw new NoSuchElementException();lastReturned = next = next.previous;nextIndex--;checkForComodification();return lastReturned.element;}public int nextIndex() {return nextIndex;}public int previousIndex() {return nextIndex-1;}// 移除当前Iterator持有的节点public void remove() {checkForComodification();Entry<E> lastNext = lastReturned.next;try {LinkedList.this.remove(lastReturned);} catch (NoSuchElementException e) {throw new IllegalStateException();}if (next==lastReturned)next = lastNext;elsenextIndex--;lastReturned = header;expectedModCount++;}// 修改当前节点的内容public void set(E e) {if (lastReturned == header)throw new IllegalStateException();checkForComodification();lastReturned.element = e;}// 在当前持有节点后面插入新节点public void add(E e) {checkForComodification();// 将最近一次返回节点修改为headerlastReturned = header;addBefore(e, next);nextIndex++;expectedModCount++;}// 判断expectedModCount和modCount是否一致,以确保通过ListItr的修改操作正确的反映在LinkedList中final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}
}

下面是一个ListItr的使用实例。

        LinkedList<String> list = new LinkedList<String>();list.add("First");list.add("Second");list.add("Thrid");System.out.println(list);ListIterator<String> itr = list.listIterator();while (itr.hasNext()) {System.out.println(itr.next());}try {System.out.println(itr.next());// throw Exception} catch (Exception e) {// TODO: handle exception}itr = list.listIterator();System.out.println(list);System.out.println(itr.next());itr.add("new node1");System.out.println(list);itr.add("new node2");System.out.println(list);System.out.println(itr.next());itr.set("modify node");System.out.println(list);itr.remove();System.out.println(list);
运行结果:
[First, Second, Thrid]
First
Second
Thrid
[First, Second, Thrid]
First
[First, new node1, Second, Thrid]
[First, new node1, new node2, Second, Thrid]
Second
[First, new node1, new node2, modify node, Thrid]
[First, new node1, new node2, Thrid]

LinkedList还有一个提供Iterator的方法:descendingIterator()。该方法返回一个DescendingIterator对象。DescendingIterator是LinkedList的一个内部类。

public Iterator<E> descendingIterator() {return new DescendingIterator();}

下面分析详细分析DescendingIterator类。

private class DescendingIterator implements Iterator {// 获取ListItr对象
final ListItr itr = new ListItr(size());
// hasNext其实是调用了itr的hasPrevious方法public boolean hasNext() {return itr.hasPrevious();}
// next()其实是调用了itr的previous方法public E next() {return itr.previous();}public void remove() {itr.remove();}
}

从类名和上面的代码可以看出这是一个反向的Iterator,代码很简单,都是调用的ListItr类中的方法。

原文转自:http://www.cnblogs.com/hzmark/archive/2012/12/25/LinkedList.html

其他比较好的博客:

https://blog.csdn.net/chenssy/article/details/18099417

https://blog.csdn.net/sinat_36246371/article/details/53709625

https://blog.csdn.net/i_lovefish/article/details/8042883

Java集合篇:LinkedList源码分析相关推荐

  1. java arraydeque_死磕 java集合之ArrayDeque源码分析

    问题 (1)什么是双端队列? (2)ArrayDeque是怎么实现双端队列的? (3)ArrayDeque是线程安全的吗? (4)ArrayDeque是有界的吗? 简介 双端队列是一种特殊的队列,它的 ...

  2. 死磕 java集合之ArrayDeque源码分析

    问题 (1)什么是双端队列? (2)ArrayDeque是怎么实现双端队列的? (3)ArrayDeque是线程安全的吗? (4)ArrayDeque是有界的吗? 简介 双端队列是一种特殊的队列,它的 ...

  3. 【死磕 Java 集合】— LinkedTransferQueue源码分析

    [死磕 Java 集合]- LinkedTransferQueue源码分析 问题 (1)LinkedTransferQueue是什么东东? (2)LinkedTransferQueue是怎么实现阻塞队 ...

  4. 死磕Java集合之BitSet源码分析(JDK18)

    死磕Java集合之BitSet源码分析(JDK18) 文章目录 死磕Java集合之BitSet源码分析(JDK18) 简介 继承体系 存储结构 源码解析 属性 构造方法 set(int bitInde ...

  5. java linkedlist源码_Java集合之LinkedList源码分析

    一.LinkedList简介 LinkedList是一种可以在任何位置进行高效地插入和移除操作的有序序列,它是基于双向链表实现的. ps:这里有一个问题,就是关于实现LinkedList的数据结构是否 ...

  6. Java集合:Hashtable源码分析

    1. 概述 上次讨论了HashMap的结构,原理和实现,本文来对Map家族的另外一个常用集合HashTable进行介绍.HashTable和HashMap两种集合非常相似,经常被各种面试官问到两者的区 ...

  7. java ee是什么_死磕 java集合之HashSet源码分析

    问题 (1)集合(Collection)和集合(Set)有什么区别? (2)HashSet怎么保证添加元素不重复? (3)HashSet是否允许null元素? (4)HashSet是有序的吗? (5) ...

  8. 死磕 java集合之ConcurrentSkipListMap源码分析——发现个bug

    前情提要 点击链接查看"跳表"详细介绍. 拜托,面试别再问我跳表了! 简介 跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表. 跳表在原有的有序链表上面增加了多级 ...

  9. Java集合之Vector源码分析

    概述 Vector与ArrayLIst类似, 内部同样维护一个数组, Vector是线程安全的. 方法与ArrayList大体一致, 只是加上 synchronized 关键字, 保证线程安全, 下面 ...

  10. Java集合干货——ArrayList源码分析

    前言 在之前的文章中我们提到过ArrayList,ArrayList可以说是每一个学java的人使用最多最熟练的集合了,但是知其然不知其所以然.关于ArrayList的具体实现,一些基本的都也知道,譬 ...

最新文章

  1. wxWidgets:wxFont实例
  2. [转]Github 中被 Fork 最多的库
  3. HarmonyOS之常用组件ToastDialog的功能和使用
  4. 【linux】服务器运维必备之linux常用命令合集
  5. IoTSharp 2.0 发布
  6. java github_GitHub Research:超过50%的Java记录语句写错了
  7. linux接收网络数据并存存储,Linux网络设备驱动之数据接收流程(六)
  8. Android知识总结——Path常用方法解析
  9. java-weixin-tools接入微信
  10. robocode java_IBM Robocode Java学习利器(1)Robocode 入门
  11. 编写简单的触摸屏驱动程序——基于QEMU
  12. Aspose PDF预览出现乱码
  13. chrome os 安装手册
  14. macbookpro2011安装单系统win10
  15. bitcoin全节点搭建
  16. 3D点云目标检测算法综述
  17. 聊天机器人 NLP(初识 学习思路)
  18. 统计|如何理解估计量的三条评价标准
  19. [Codeup]1814 问题 A: 剩下的树
  20. jQuery的id选择器

热门文章

  1. 五十四、Java日期Date,LocalDate类以及格式化输出
  2. MindSpore感恩节重磅福利,华为Mate 40E送送送!
  3. 今日arXiv精选 | 14篇EMNLP 2021最新论文
  4. 清华大学刘知远教授新作,图神经网络最佳解锁方式 | 送书福利
  5. 推荐算法注意点和DeepFM工程化实现
  6. CVPR 2019 | NAS-FPN:基于自动架构搜索的特征金字塔网络
  7. 基于对偶学习的跨领域图片描述生成
  8. 如何修改作者名称_PS2019中如何扩展文本选区 ps扩展文本选区的图文教程_photoshop教程...
  9. Spring5-IOC底层原理
  10. 【架构一】高可用之冗余备份