日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

一个问题让我直接闭门思过!!!拼多多面试必问项之List实现类:LinkedList

發布時間:2023/12/4 编程问答 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 一个问题让我直接闭门思过!!!拼多多面试必问项之List实现类:LinkedList 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、LinkedList概述

1、對于頻繁的插入或刪除元素的操作,建議使用LinkedList類,效率較高。

2、LinkedList是一個實現了List接口和Deque接口的雙端鏈表。

3、LinkedList底層的鏈表結構使它支持高效的插入和刪除操作,另外它實現了Deque接口,使得LinkedList類也具有 List 的操作以及雙端隊列和棧的性質。

4、LinkedList不是線程安全的,如果想使LinkedList變成線程安全的,可以調用靜態類Collections類中的synchronizedList方法:

List list=Collections.synchronizedList(``new` `LinkedList(...));

5、LinkedList 的繼承結構如下:

二、實現的接口

源碼:

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

(1)實現了 List 接口,具有List的操作方法;

(2)實現了 Deque 接口,具有雙端隊列和棧的特性;

(3)實現了 Cloneable 接口,支持克隆;

(4)實現了 Serializable 接口,支持序列化;

三、內部結構

1、雙向鏈表

ArrayList是通過數組實現存儲,而LinkedList則是通過鏈表來存儲數據,而且他實現的是一個雙向鏈表,簡單的說一下什么是雙向鏈表。

雙向鏈表是數據結構的一種形式,他的每個節點維護兩個指針,prev指向上一個節點,next指向下一個節點。

這種結構有什么特點呢?他可以實現雙向遍歷,這使得在鏈表中的數據讀取變得非常靈活自由。

同時,LinkedList中維護了兩個指針,一個指向頭部,一個指向尾部。維護這兩個指針后,可以使得元素從頭部插入,也可以使元素從尾部插入。

基于方式,用戶很容易就能實現FIFO(隊列),LIFO(棧)等效果。那么下面我們來看一下源碼中的具體實現。

2、內部結構分析

如圖所示:

**

**

LinkedList:雙向鏈表,內部沒有聲明數組,而是定義了Node類型的first和last,用于記錄首末元素。同時,定義內部類Node,作為LinkedList中保存數據的基

本結構。

Node除了保存數據,還定義了兩個變量:

① prev變量記錄前一個元素的位置

② next變量記錄下一個元素的位置

看完了圖之后,我們再看LinkedList類中的一個內部私有類Node就很好理解了:

1 private static class Node<E> { 2 E item; //本節點的值3 Node<E> next; //后繼節點 4 Node<E> prev; //前驅節點 5 6 Node(Node<E> prev, E element, Node<E> next) { 7 this.item = element;8 this.next = next; 9 this.prev = prev; 10 } 11 }

這個類就代表雙端鏈表的節點Node。這個類有三個屬性,分別是前驅節點,本節點的值,后繼結點。

四、成員變量

在 LinkedList 類中,還有幾個成員變量如下:

1 // list 的長度2 transient int size = 0;3 4 // 鏈表頭結點5 transient Node<E> first;6 7 // 鏈表尾結點8 transient Node<E> last; 9 10 //序列化標記 11 private static final long serialVersionUID = 876323262645176354L;

五、構造器

LinkedList 有兩個構造器,如下:

(1)無參構造:

1 public LinkedList() { 2 }

2)用已有的集合創建鏈表的構造方法:

1 public LinkedList(Collection<? extends E> c) { 2 this(); 3 addAll(c); 4 }

注意:由于鏈表的容量可以一直增加,因此沒有指定容量的構造器。

第一個為無參構造器。

第二個為使用指定集合的集合構造,并調用 addAll() 方法,繼續跟進該方法,代碼如下:

1 public boolean addAll(Collection<? extends E> c) {2 return addAll(size, c); 3 }4 5 public boolean addAll(int index, Collection<? extends E> c) { 6 //1:檢查index范圍是否在size之內7 checkPositionIndex(index);8 9 //2:toArray()方法把集合的數據存到對象數組中 10 Object[] a = c.toArray(); 11 int numNew = a.length; 12 if (numNew == 0) 13 return false; 14 15 16 //3:獲取當前鏈表的前驅和后繼結點,得到插入位置的前驅節點和后繼節點 17 Node<E> pred, succ; 18 //如果插入位置為尾部,前驅節點為last,后繼節點為null 19 if (index == size) { 20 succ = null; 21 pred = last; 22 } 23 //若非尾結點,獲取指定位置的結點,調用node()方法得到后繼節點,再得到前驅節點, 24 else { 25 succ = node(index); //獲取當前節點 26 pred = succ.prev; //獲取當前節點前驅節點 27 } 28 29 // 4:循環將數組中的元素插入到鏈表 30 for (Object o : a) { 31 @SuppressWarnings("unchecked") E e = (E) o; 32 //創建新節點 33 Node<E> newNode = new Node<>(pred, e, null); 34 //如果插入位置在鏈表頭部 35 if (pred == null) 36 first = newNode; 37 else 38 pred.next = newNode; 39 pred = newNode; 40 } 41 42 //如果插入位置在尾部,重置last節點 43 // 若插入到末尾,則數組中的最后一個元素就是尾結點 44 if (succ == null) { 45 last = pred; 46 } 47 48 //否則,將插入的鏈表與先前鏈表連接起來 49 else { 50 // 若插入到指定位置,將數組中最后一個元素與下一個位置關聯起來 51 pred.next = succ; 52 succ.prev = pred; 53 } 54 size += numNew; 55 modCount++; 56 return true; 57 } 58 59 private void checkPositionIndex(int index) { 60 if (!isPositionIndex(index)) 61 throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); 62 } 63 64 private boolean isPositionIndex(int index) { 65 return index >= 0 && index <= size; 66 }

上面可以看出addAll方法通常包括下面四個步驟:

  • 檢查index范圍是否在size之內;

  • toArray()方法把集合的數據存到對象數組中;

  • 得到插入位置的前驅和后繼節點;

  • 遍歷數據,將數據插入到指定位置,如果沒有在尾部,把原來數據鏈起來;

  • 其中 node(index) 方法為獲取指定位置的結點,代碼如下:

    1 Node<E> node(int index) { 2 // assert isElementIndex(index); 3 // 判斷下標在哪里,若下標在前一半,則從前往后遍歷;否則從后往前遍歷4 if (index < (size >> 1)) {5 Node<E> x = first;6 for (int i = 0; i < index; i++)7 x = x.next; 8 return x; 9 } else { 10 Node<E> x = last; 11 for (int i = size - 1; i > index; i--) 12 x = x.prev; 13 return x; 14 } 15 }

    該方法通過遍歷鏈表獲取指定的元素。

    值得注意的是,該方法并非直接從頭到尾遍歷整個鏈表,而是先判斷下標的位置,若在前一半則從前往后遍歷;否則就從后往前遍歷。這樣能減少遍歷結點的個數。

    因為鏈表的內存空間是非連續的,所以不支持隨機訪問(下標訪問)。所以,查詢某個結點是通過遍歷整個鏈表來實現的。

    六、常用方法

    1、新增結點方法【尾插法】:add(),addLast(),offer(),offerLast()

    源碼分析:

    1 public boolean add(E e) { 2 linkLast(e);3 return true;4 }5 public void addLast(E e) { 6 linkLast(e);7 }8 public boolean offer(E e) { 9 return add(e); 10 } 11 public boolean offerLast(E e) { 12 addLast(e); 13 return true; 14 }

    可以看到他們都是調用了同一個方法 linkLast(e) 實現的,如下:

    1 /**2 * Links e as last element.3 */4 void linkLast(E e) { 5 final Node<E> l = last; 6 // 創建一個節點,將prev指針指向鏈表的尾節點。7 final Node<E> newNode = new Node<>(l, e, null); 8 9 // 將last指針指向新創建的這個節點。 10 last = newNode; 11 12 if (l == null) 13 // 如果當前鏈表為空,那么將頭指針也指向這個節點。 14 first = newNode; 15 16 else 17 // 若鏈表不為空,將新結點插入到鏈表尾部 18 // 將鏈表的尾節點的next指針指向新建的節點,這樣就完整的實現了在鏈表尾部添加一個元素的功能。 19 l.next = newNode; 20 size++;21 modCount++; 22 }

    該操作就是將指定的結點添加到鏈表末尾。

    2、新增節點【頭插法】:addFirst(),offerFirst()

    源碼:

    1 public void addFirst(E e) { 2 linkFirst(e); 3 } 4 public boolean offerFirst(E e) { 5 addFirst(e); 6 return true; 7 }

    可以看到他們都是調用了同一個方法 linkFirst(e) 實現的,如下:

    1 /**2 * Links e as first element.3 */4 private void linkFirst(E e) { 5 final Node<E> f = first; 6 // 創建一個新元素,將元素的next指針指向當前的頭結點7 final Node<E> newNode = new Node<>(null, e, f);8 // 將頭指針指向這個節點。9 first = newNode; 10 if (f == null) 11 // 如果當前節點為空,則把尾指針指向這個節點。 12 last = newNode; 13 else 14 // 將當前頭結點的prev指針指向此結點。 15 f.prev = newNode; 16 size++; 17 modCount++; 18 }

    這段代碼就是實現將元素添加的鏈表頭部。

    3、新增節點【指定位置插入】:add(int index, E element)

    源碼:

    1 public void add(int index, E element) { 2 checkPositionIndex(index); 3 4 if (index == size) 5 linkLast(element); 6 else 7 linkBefore(element, node(index)); 8 }

    在這里分了兩種情況:

    ① 如果剛好到尾部,直接在尾部插入;

    ② 如果沒有在尾部,在非null節點之前插入元素e。

    源碼:

    1 void linkLast(E e) { 2 final Node<E> l = last; 3 final Node<E> newNode = new Node<>(l, e, null);4 last = newNode; 5 if (l == null)6 first = newNode; 7 else8 l.next = newNode;9 size++; 10 modCount++; 11 } 12 13 void linkBefore(E e, Node<E> succ) { 14 // assert succ != null; 15 final Node<E> pred = succ.prev; 16 final Node<E> newNode = new Node<>(pred, e, succ); 17 succ.prev = newNode; 18 if (pred == null) 19 first = newNode;20 else 21 pred.next = newNode;22 size++; 23 modCount++; 24 }

    4、設置值:set(int index, E element)

    源碼:

    1 public E set(int index, E element) { 2 //索引檢查3 checkElementIndex(index);4 5 //獲取該索引的元素6 Node<E> x = node(index); 7 E oldVal = x.item; 8 x.item = element; 9 return oldVal; 10 }

    5、查找值:get(int index)

    源碼:

    1 public E get(int index) { 2 checkElementIndex(index);3 return node(index).item; 4 }5 private void checkElementIndex(int index) { 6 if (!isElementIndex(index))7 throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); 8 }9 private boolean isElementIndex(int index) { 10 return index >= 0 && index < size; 11 }

    可以看到,這里還是調用了上面的 node() 方法進行查找的。

    6、獲取頭節點:

    源碼:

    1 public E getFirst() { 2 final Node<E> f = first; 3 if (f == null)4 throw new NoSuchElementException(); 5 return f.item; 6 }7 public E element() { 8 return getFirst(); 9 }10 public E peek() { 11 final Node<E> f = first; 12 return (f == null) ? null : f.item; 13 } 14 public E peekFirst() { 15 final Node<E> f = first; 16 return (f == null) ? null : f.item; 17 }

    區別:

    getFirst(),element(),peek(),peekFirst() 這四個獲取頭結點方法的區別在于對鏈表為空時的處理,是拋出異常還是返回null,

    其中getFirst()element() 方法將會在鏈表為空時,拋出異常;element()方法的內部就是使用getFirst()實現的。它們會在鏈表為空時,拋出NoSuchElementException;

    而 peek() 和 peekFirst() 方法在鏈表為空時會返回空;

    7、獲取尾結點

    1 public E getLast() { 2 final Node<E> l = last; 3 if (l == null)4 throw new NoSuchElementException();5 return l.item;6 }7 public E peekLast() { 8 final Node<E> l = last; 9 return (l == null) ? null : l.item; 10 }

    兩者區別: getLast() 方法在鏈表為空時,會拋出NoSuchElementException,而peekLast() 則不會,只是會返回 null

    8、根據對象得到索引的方法

    int indexOf(Object o): 從頭遍歷找

    1 public int indexOf(Object o) { 2 int index = 0;3 if (o == null) {4 //從頭遍歷5 for (Node<E> x = first; x != null; x = x.next) { 6 if (x.item == null)7 return index;8 index++;9 } 10 } else {11 //從頭遍歷 12 for (Node<E> x = first; x != null; x = x.next) { 13 if (o.equals(x.item)) 14 return index; 15 index++; 16 } 17 } 18 return -1;19 }

    int lastIndexOf(Object o): 從尾遍歷找

    1 public int lastIndexOf(Object o) { 2 int index = size; 3 if (o == null) {4 //從尾遍歷5 for (Node<E> x = last; x != null; x = x.prev) { 6 index--;7 if (x.item == null)8 return index; 9 } 10 } else { 11 //從尾遍歷 12 for (Node<E> x = last; x != null; x = x.prev) { 13 index--; 14 if (o.equals(x.item))15 return index; 16 } 17 }18 return -1; 19 }

    9、檢查鏈表是否包含某對象的方法:contains()

    源碼:

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

    10、刪除頭節點方法:remove() ,removeFirst(),pop(),poll(),pollFirst()

    源碼:

    1 public E remove() {2 return removeFirst();3 }4 5 public E pop() { 6 return removeFirst();7 }8 9 public E removeFirst() { 10 final Node<E> f = first; 11 if (f == null) 12 throw new NoSuchElementException(); 13 return unlinkFirst(f); 14 } 15 16 public E poll() { 17 final Node<E> f = first; 18 return (f == null) ? null : unlinkFirst(f); 19 } 20 public E pollFirst() {21 final Node<E> f = first; 22 return (f == null) ? null : unlinkFirst(f); 23 }

    本質上都是調用了 unlinkFirst()方法

    源碼:

    1 private E unlinkFirst(Node<E> f) {2 // assert f == first && f != null;3 final E element = f.item;4 final Node<E> next = f.next; 5 f.item = null;6 f.next = null; // help GC7 first = next;8 if (next == null)9 last = null; 10 else 11 next.prev = null; 12 size--; 13 modCount++; 14 return element; 15 }

    11、刪除尾節點方法:removeLast(),pollLast()

    源碼:

    1 public E removeLast() { 2 final Node<E> l = last; 3 if (l == null)4 throw new NoSuchElementException(); 5 return unlinkLast(l); 6 }7 8 public E pollLast() { 9 final Node<E> l = last; 10 return (l == null) ? null : unlinkLast(l); 11 }

    區別: removeLast()在鏈表為空時將拋出NoSuchElementException,而pollLast()方法返回null。

    本質上都是調用了 unlinkLast()方法。

    源碼:

    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 GC7 last = prev; 8 if (prev == null)9 first = null; 10 else 11 prev.next = null; 12 size--;13 modCount++; 14 return element; 15 }

    12、刪除指定元素:remove(Object o) & 刪除指定位置的元素:remove(int index)

    1 public boolean remove(Object o) { 2 if (o == null) {3 for (Node<E> x = first; x != null; x = x.next) { 4 if (x.item == null) {5 unlink(x);6 return true;7 }8 }9 } else { 10 for (Node<E> x = first; x != null; x = x.next) { 11 if (o.equals(x.item)) {12 unlink(x);13 return true; 14 } 15 } 16 }17 return false;18 } 19 20 public E remove(int index) { 21 checkElementIndex(index); 22 return unlink(node(index)); 23 }

    當刪除指定對象時,只需調用remove(Object o)即可,不過該方法一次只會刪除一個匹配的對象,如果刪除了匹配對象,返回true,否則false。

    本質上還是調用了 unlink(Node x) 方法:

    1   E unlink(Node<E> x) {2 // assert x != null;3 final E element = x.item;4 final Node<E> next = x.next;//得到后繼節點5 final Node<E> prev = x.prev;//得到前驅節點6 7 //刪除前驅指針8 if (prev == null) {9 first = next;//如果刪除的節點是頭節點,令頭節點指向該節點的后繼節點 10 } else { 11 prev.next = next;//將前驅節點的后繼節點指向后繼節點 12 x.prev = null; 13 } 14 15 //刪除后繼指針 16 if (next == null) { 17 last = prev;//如果刪除的節點是尾節點,令尾節點指向該節點的前驅節點 18 } else { 19 next.prev = prev; 20 x.next = null; 21 } 22 23 x.item = null; 24 size--; 25 modCount++; 26 return element; 27 }

    13、序列化方法:writeObject(java.io.ObjectOutputStream s)

    源碼:

    1 private void writeObject(java.io.ObjectOutputStream s)2 throws java.io.IOException { 3 // Write out any hidden serialization magic4 s.defaultWriteObject();5 6 // Write out size7 s.writeInt(size);8 9 // Write out all elements in the proper order. 10 for (Node<E> x = first; x != null; x = x.next) 11 s.writeObject(x.item); 12 }

    14、反序列化方法:readObject(java.io.ObjectInputStream s)

    源碼:

    1 private void readObject(java.io.ObjectInputStream s) 2 throws java.io.IOException, ClassNotFoundException { 3 // Read in any hidden serialization magic4 s.defaultReadObject();5 6 // Read in size7 int size = s.readInt();8 9 // Read in all elements in the proper order. 10 for (int i = 0; i < size; i++) 11 linkLast((E)s.readObject()); 12 }

    七、作為其他數據結構

    1、FIFO(隊列)實現原理

    隊列的原理就是每次都從鏈表尾部添加元素,從鏈表頭部獲取元素,就像生活中的排隊叫號,總是有個先來后到。

    源碼:

    1 // 隊列尾部添加一個元素,建議使用這個,約定俗成吧。2 publicboolean offer(E e){3 return add(e); 4 }5 6 // 隊列尾部添加一個元素7 publicboolean offerLast(E e){8 addLast(e);9 return true; 10 } 11 12 // offer和offerLast底層調用的都是linkLast這個方法,顧名思義就是將元素添加到鏈表尾部。 13 void linkLast(E e){ 14 finalNode<E> l =last; 15 16 // 創建一個節點,將prev指針指向鏈表的尾節點。 17 finalNode<E> newNode =newNode<>(l, e,null); 18 19 // 將last指針指向新創建的這個節點。 20 last= newNode; 21 22 if(l ==null) 23 // 如果當前鏈表為空,那么將頭指針也指向這個節點。 24 first = newNode; 25 else 26 // 將鏈表的尾節點的next指針指向新建的節點,這樣就完整的實現了在鏈表尾部添加一個元素的功能。 27 l.next= newNode; 28 29 size++; 30 modCount++; 31 } 32 33 // 在鏈表頭部刪除一個元素,建議用這個 34 public E poll(){ 35 final Node<E> f = first; 36 return(f ==null)?null: unlinkFirst(f); 37 } 38 // 在鏈表頭部刪除一個元素 39 public E pollFirst(){ 40 final Node<E> f = first; 41 return(f ==null)?null: unlinkFirst(f); 42 } 43 44 // poll和pollFirst底層調用的就是這個方法,將鏈表的頭元素刪除。 45 private E unlinkFirst(Node<E> f){ 46 // assert f == first && f != null; 47 final E element = f.item; 48 final Node<E>next= f.next; 49 f.item =null; 50 f.next=null;// help GC 51 first =next; 52 if(next==null) 53 last=null; 54 else 55 next.prev =null; 56 size--; 57 modCount++; 58 return element; 59 } 60 61 // 獲取頭元素,但是不會刪除他。 62 public E peek(){ 63 final Node<E> f = first; 64 return(f ==null)?null: f.item; 65 }

    更準確來說,鏈表是一個雙端鏈表的結構,可以在頭尾都進行操作節點。

    2、LIFO(棧)實現原理:

    棧的原理是每次從頭部添加元素,也從頭部獲取元素,那么后進入的元素反而最先出來。就像我們平時疊盤子,洗好了就一個一個往上放,然后要用了就從上往下一個一個拿。

    源碼:

    1 // 在鏈表的頭部添加一個元素2 publicvoid push(E e){3 addFirst(e);4 }5 6 // addFirst調用的就是linkFirst,這段代碼就是實現將元素添加的鏈表頭部。7 private void linkFirst(E e){8 final Node<E> f = first; 9 // 創建一個新元素,將元素的next指針指向當前的頭結點 10 final Node<E> newNode =newNode<>(null, e, f); 11 // 將頭指針指向這個節點。 12 first = newNode; 13 if(f ==null) 14 // 如果當前節點為空,則把尾指針指向這個節點。 15 last= newNode; 16 else 17 // 將當前頭結點的prev指針指向此結點。 18 f.prev = newNode; 19 size++; 20 modCount++; 21 } 22 23 // 彈出頂部結點。 24 public E pop(){ 25 return removeFirst(); 26 } 27 28 // removeFirst調用的就是unlinkFirst,unlinkFirst實現將鏈表頂部元素刪除 29 private E unlinkFirst(Node<E> f){ 30 // assert f == first && f != null; 31 final E element = f.item; 32 final Node<E>next= f.next; 33 f.item =null; 34 f.next=null;// help GC 35 first =next; 36 if(next==null) 37 last=null; 38 else 39 next.prev =null; 40 size--; 41 modCount++; 42 return element; 43 } 44 45 // 獲取頂部結點,但是不刪除 46 public E peek(){ 47 final Node<E> f = first; 48 return(f ==null)?null: f.item; 49 }

    八、迭代器相關

    LinkedList的迭代器實現有兩個,一個是實現了Iterator接口的DescendingIterator,另一個則是實現了ListIterator接口的ListItr。

    1、ListItr

    源碼:

    1 public ListIterator<E> listIterator(int index) { 2 checkPositionIndex(index);3 return new ListItr(index); 4 }5 6 private class ListItr implements ListIterator<E> { 7 private Node<E> lastReturned; 8 private Node<E> next; 9 private int nextIndex; 10 private int expectedModCount = modCount;11 12 // 實例化的時候,將next指針指向指定位置的元素13 ListItr(int index) { 14 // assert isPositionIndex(index);15 next = (index == size) ? null : node(index); 16 nextIndex = index; 17 }18 19 public boolean hasNext() { 20 return nextIndex < size; 21 }22 23 // 向后遍歷24 public E next() { 25 checkForComodification();26 if (!hasNext())27 throw new NoSuchElementException(); 28 29 lastReturned = next; 30 next = next.next; 31 nextIndex++;32 return lastReturned.item; 33 }34 35 public boolean hasPrevious() { 36 return nextIndex > 0;37 }38 39 // 向前遍歷40 public E previous() { 41 checkForComodification();42 if (!hasPrevious())43 throw new NoSuchElementException(); 44 45 lastReturned = next = (next == null) ? last : next.prev; 46 nextIndex--;47 return lastReturned.item; 48 }49 50 public int nextIndex() { 51 return nextIndex; 52 }53 54 public int previousIndex() {55 return nextIndex - 1;56 }57 58 public void remove() { 59 checkForComodification();60 if (lastReturned == null)61 throw new IllegalStateException(); 62 63 Node<E> lastNext = lastReturned.next; 64 unlink(lastReturned);65 if (next == lastReturned) 66 next = lastNext; 67 else68 nextIndex--;69 lastReturned = null;70 expectedModCount++;71 }72 73 public void set(E e) { 74 if (lastReturned == null)75 throw new IllegalStateException(); 76 checkForComodification();77 lastReturned.item = e; 78 }79 80 public void add(E e) { 81 checkForComodification();82 lastReturned = null;83 if (next == null)84 linkLast(e);85 else86 linkBefore(e, next);87 nextIndex++;88 expectedModCount++;89 }90 91 public void forEachRemaining(Consumer<? super E> action) { 92 Objects.requireNonNull(action);93 while (modCount == expectedModCount && nextIndex < size) { 94 action.accept(next.item);95 lastReturned = next; 96 next = next.next; 97 nextIndex++;98 }99 checkForComodification(); 100 } 101 102 final void checkForComodification() { 103 if (modCount != expectedModCount) 104 throw new ConcurrentModificationException(); 105 } 106 }

    2、DescendingIterator

    DescendingIterator迭代器實現的是對鏈表從尾部向頭部遍歷的功能,他復用里ListItr中的previous方法,將當前位置指向鏈表尾部,然后逐個向前遍歷。

    源碼:

    1 private class DescendingIterator implements Iterator<E> { 2 private final ListItr itr = new ListItr(size()); 3 public boolean hasNext() { 4 return itr.hasPrevious(); 5 }6 public E next() { 7 return itr.previous(); 8 }9 public void remove() { 10 itr.remove(); 11 } 12 }

    九、不同版本的 LinkedList

    在LinkedList 中 JDK1.6 之前為雙向循環鏈表,JDK1.7 取消了循環,采用雙向鏈表。

    1、雙向鏈表

    雙向鏈表屬于鏈表的一種,也叫雙鏈表雙向即是說它的鏈接方向是雙向的,它由若干個節點組成,每個節點都包含下一個節點和上一個節點的指針,所以從雙向鏈表的任意節點開始,都能很方便訪問他的前驅結點和后繼節點。

    2、雙向鏈表特點

      • 創建雙鏈表時無需指定鏈表的長度。
      • 比起單鏈表,雙鏈表需要多一個指針用于指向前驅節點,所以需要存儲空間比單鏈表多一點。
      • 雙鏈表的插入和刪除需要同時維護 next 和 prev 兩個指針。
      • 雙鏈表中的元素訪問需要通過順序訪問,即要通過遍歷的方式來尋找元素。

    3、雙向循環鏈表

    前面的雙向鏈表的 head 節點和鏈尾沒有連接關系,所以如果要訪問最后一個節點的話需要從頭開始遍歷,直到最后一個節點。在雙向鏈表基礎上改進一下,把 header 節點的 prev 指針指向最后一個節點,而最后一個節點的 next 指針指向 header 節點,于是便構成雙向循環鏈表。

    更多鏈表操作:https://juejin.cn/post/6844903648154271757#heading-0

    4、JDK6

    在JDK 1.7之前(此處使用JDK1.6來舉例),LinkedList是通過headerEntry實現的一個循環鏈表的。先初始化一個空的Entry,用來做header,然后首尾相連,形成一個循環鏈表:

    1 privatetransient Entry<E>header =new Entry<E>(null,null,null); 2 3 public LinkedList() {header.next =header.previous =header; }

    在LinkedList中提供了兩個基本屬性size、header。

    1 private transient Entry<E> header = new Entry<E>(null, null, null); 2 private transient int size = 0;
    • 其中size表示的LinkedList的大小,header表示鏈表的表頭,Entry為節點對象。
    1 private static class Entry<E> { 2 E element; //元素節點3 Entry<E> next; //下一個元素4 Entry<E> previous; //上一個元素5 6 Entry(E element, Entry<E> next, Entry<E> previous) {7 this.element = element; 8 this.next = next; 9 this.previous = previous;10 } 11 }
    • 上面為Entry對象的源代碼,Entry為LinkedList的內部類,它定義了存儲的元素。該元素的前一個元素、后一個元素,這是典型的雙向鏈表定義方式。

    每次添加/刪除元素都是默認在鏈尾操作。對應此處,就是在header前面操作,因為遍歷是next方向的,所以在header前面操作,就相當于在鏈表尾操作。

    如下面的插入操作addBefore以及圖示,如果插入obj_3,只需要修改header.previous和obj_2.next指向obj_3即可。`

    1 private Entry<E> addBefore(E e, Entry<E> entry) { 2 //利用Entry構造函數構建一個新節點 newEntry,3 Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);4 //修改newEntry的前后節點的引用,確保其鏈表的引用關系是正確的5 newEntry.previous.next = newEntry; 6 newEntry.next.previous = newEntry; 7 //容量+18 size++;9 //修改次數+1 10 modCount++; 11 return newEntry; 12 }
    • 在addBefore方法中無非就是做了這件事:構建一個新節點newEntry,然后修改其前后的引用。

    5、JDK7

    在JDK 1.7,1.6的headerEntry循環鏈表被替換成了first和last組成的非循環鏈表。

    1 transient int size = 0;2 3 /**4 * Pointer to first node.5 * Invariant: (first == null && last == null) ||6 * (first.prev == null && first.item != null)7 */8 transient Node<E> first; 9 10 /** 11 * Pointer to last node. 12 * Invariant: (first == null && last == null) || 13 * (last.next == null && last.item != null) 14 */ 15 transient Node<E> last;

    [

    • 在初始化的時候,不用去new一個Entry。

    1 /** 2 * Constructs an empty list. 3 */ 4 public LinkedList() { 5 }
    • 在插入/刪除的時候,也是默認在鏈尾操作。把插入的obj當成newLast,掛在oldLast的后面。另外還要先判斷first是否為空,如果為空則first = obj。

    如下面的插入方法linkLast,在尾部操作,只需要把obj_3.next指向obj_4即可。

    1 void linkLast(E e) { 2 final Node<E> l = last; 3 final Node<E> newNode = new Node<>(l, e, null);4 last = newNode; 5 if (l == null)6 first = newNode; 7 else8 l.next = newNode; 9 size++; 10 modCount++; 11 }

    其中

    1 private static class Node<E> { 2 E item;3 Node<E> next; 4 Node<E> prev; 5 6 Node(Node<E> prev, E element, Node<E> next) { 7 this.item = element; 8 this.next = next; 9 this.prev = prev; 10 } 11 }

    6、【1.6-header循環鏈表】 V.S 【1.7-first/last非循環鏈表】

    JDK 1.7中的first/last對比以前的header有下面幾個好處:

    • (1) first / last有更清晰的鏈頭、鏈尾概念,代碼看起來更容易明白。

    • (2)first / last方式能節省new一個headerEntry。(實例化headerEntry是為了讓后面的方法更加統一,否則會多很多header的空校驗)

    • (3)在鏈頭/尾進行插入/刪除操作,first /last方式更加快捷。

    插入/刪除操作按照位置,分為兩種情況:中間 和 兩頭。

    在中間插入/刪除,兩者都是一樣,先遍歷找到index,然后修改鏈表index處兩頭的指針。
        在兩頭,對于循環鏈表來說,由于首尾相連,還是需要處理兩頭的指針。而非循環鏈表只需要處理一邊first.previous/last.next,所以理論上非循環鏈表更高效。      恰恰在兩頭(鏈頭/鏈尾) 操作是最普遍的

    (對于遍歷來說,兩者都是鏈表指針循環,所以遍歷效率是一樣的。)

    十、線程安全性

    線程安全的概念不再贅述。分析以下場景:

    若有線程 T1 對 LinkedList 進行遍歷,同時線程 T2 對其進行結構性修改。

    對 LinkedList 的遍歷是通過 listIterator(index) 方法實現的,如下:

    1 public ListIterator<E> listIterator(int index) { 2 checkPositionIndex(index);3 return new ListItr(index); 4 }5 6 7 private class ListItr implements ListIterator<E> { 8 private Node<E> lastReturned; 9 private Node<E> next; 10 private int nextIndex; 11 // 初始化時二者是相等的 12 private int expectedModCount = modCount; 13 14 15 ListItr(int index) { 16 // assert isPositionIndex(index); 17 next = (index == size) ? null : node(index); 18 nextIndex = index; 19 } 20 21 22 public E next() { 23 checkForComodification(); 24 if (!hasNext()) 25 throw new NoSuchElementException();26 27 28 lastReturned = next; 29 next = next.next; 30 nextIndex++; 31 return lastReturned.item; 32 } 33 34 35 public void remove() { 36 checkForComodification(); 37 if (lastReturned == null) 38 throw new IllegalStateException(); 39 40 41 Node<E> lastNext = lastReturned.next; 42 unlink(lastReturned); 43 if (next == lastReturned) 44 next = lastNext; 45 else 46 nextIndex--; 47 lastReturned = null; 48 expectedModCount++; 49 } 50 51 52 // ... 53 54 // 是否有其他線程對當前對象進行結構修改 55 final void checkForComodification() { 56 if (modCount != expectedModCount) 57 throw new ConcurrentModificationException(); 58 } 59 }

    該類的 next(), add(e) 等方法在執行時會檢測 modCount 與創建時是否一致(checkForComodification() 方法),從而判斷是否有其他線程對該對象進行了結構修改,若有則拋出 ConcurrentModificationException 異常。

    因此,LinkedList 是線程不安全的。

    十一、總結

    1、LinkedList 內部是【雙向鏈表】,同時實現了 List 接口 和 Deque 接口,因此也具備 List、雙端隊列和棧的性質。

    2、線程不安全。
    最后,祝大家早日學有所成,拿到滿意offer

    創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

    總結

    以上是生活随笔為你收集整理的一个问题让我直接闭门思过!!!拼多多面试必问项之List实现类:LinkedList的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。