來,進來的小夥伴們,咱們認識一下。html
我是俗世遊子,在外流浪多年的Java程序猿java
這兩天看到太多小夥伴在秀公司「10.24程序員節」的福利,我認可我酸了o(╥﹏╥)onode
上一節咱們聊過了ArrayList,對其底層結構和源碼實現進行了瞭解,那這節咱們來聊一聊關於它的「兄弟集合」:LinkedList程序員
一樣屬於List的子類,那麼也就一樣擁有了其特色:api
能夠看到,LinkedList除了實現List接口外,還實現了Queue接口,在Java中,該接口定義的是隊列的,向咱們以後要聊到的數組
等等的都是屬於該類的實現安全
基於這種方式,那麼咱們的LinkedList也適合作隊列的處理場景,好比:數據結構
LinkedList底層是基於雙向鏈表的方式來存儲的,那確定有人在想,什麼是鏈表呢?咱們這就來聊一聊oracle
鏈表是一種在邏輯上連續,可是物理存儲上非連續的存儲結構,其保證邏輯連續是經過指針指向來肯定順序的。ide
上面看到的是單向鏈表,能夠看到:
Node
,其中包含兩個部分
Node
還有一種雙向鏈表的形式
看上圖:
LinkedList就是採用的雙向鏈表的形式,下面咱們來看具體的代碼
背景:這裏已雙向鏈表爲例
對鏈表操做,實際上就是修改指針的指向,好比
頭尾的插入很是簡單,直接指向 next 和 prev 就能夠了,這裏咱們看插入到中間
移除元素和插入元素很相似,無非就是將指定元素刪除掉,而後將指針指向下一個節點
前面的鏈表介紹都是爲以後作鋪墊,咱們繼續來看今天的主角:LinkedList
LinkedList底層是採用雙向鏈表的結構來進行數據存儲的,能夠說,LinkedList全部的操做都是針對引用指向來進行操做的。下面來看具體的方法
LinkedList<String> linkedList = new LinkedList<>(); // Arrays.asList("item1", "item2"):爲了方便演示 LinkedList<String> linkedList2 = new LinkedList<>(Arrays.asList("item1", "item2"));
一樣,咱們仍是經過構造方法來看:
public LinkedList() { } public LinkedList(Collection<? extends E> c) { this(); addAll(c); }
和ArrayList不一樣,這裏只有兩個構造方法。
在LinkedList中,初始長度是不須要設置的,並且也不須要擴容操做。
不須要設置初始長度和底層存儲結構有關,若是想不明白能夠先去上一節看一看數組的介紹
不過也有說,LinkedList是存在最大容量的:不能超過Integer.MAX_VALUE
你們能夠查找下資料,好好驗證下該說法(本人沒有在源碼中發現對應的驗證)
同時,咱們來看一看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; } }
雙向鏈表節點:在代碼中對應的具體實現。
這裏我須要讓你們考慮一個問題:
ArrayList和LinkedList一樣存儲了100W的數據,哪一種集合佔用的空間更大?
前面也說到了,LinkedList除了實現List接口外,還實現了Queue接口,天然也重寫其對應的方法,下面咱們一一來看看:
linkedList.add("item2"); linkedList.add(1, "item3"); linkedList.addFirst("item0"); linkedList.addLast("item9");
add(e)
和addLast(e)
的源代碼public boolean add(E e) { linkLast(e); return true; } public void addLast(E e) { linkLast(e); } void linkLast(E e) { // 獲得臨時變量尾結點 final Node<E> l = last; // 新節點的上一個節點是以前的尾結點,下一個節點是null final Node<E> newNode = new Node<>(l, e, null); // 新的節點成爲尾結點, last = newNode; // 若是尾結點是null,這個鏈表是空的,那麼頭結點也是新增的節點 if (l == null) first = newNode; else // 以前尾結點的next是新節點 l.next = newNode; size++; modCount++; }
linkLast(e)
方法,從字面和具體的實現方法上來看,默認的add(e)
方法是將元素添加到了鏈表的尾部,這裏專業名詞叫:尾插法addLast(e)
就更不用說了,直接將元素添加到尾部
linkLast(e)
方法註釋都已經有了,其實就是在調整指針的指向,總體描述以下圖:public void addFirst(E e) { linkFirst(e); } private void linkFirst(E e) { // 獲得臨時變量頭結點 final Node<E> f = first; // 新節點 插入到頭部,因此新節點的next指向f final Node<E> newNode = new Node<>(null, e, f); // 一樣,新節點稱爲新的頭 first = newNode; // 若是頭結點是null,這個鏈表是空的,那麼尾結點也是新增的節點 if (f == null) last = newNode; else // 不然的話,新節點的prev指向新節點 f.prev = newNode; size++; modCount++; }
addFirst(e)
和addLast(e)
方法正好相反這裏就不給圖了,就是上面插入尾部改爲插入頭部
這裏咱們要看一下指定位置的插入:
public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } 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; } }
linkBefore()
方法就不貼出來了,也就是改變prev和next的指向,下面咱們看一下node(index)
方法
這裏咱們舉個例子
好比:LinkedList中存儲了500條,咱們須要往250個位置上添加,那麼node(index)
對應的遍歷就是在後半段:
Node<E> x = last; for (int i = 500 - 1; i > 250; i--) x = x.prev; return x; // 好比這裏是 400 Node<E> x = last; for (int i = 500 - 1; i > 400; i--) x = x.prev; return x;
雖然遍歷採用簡單二分法提高了總體遍歷的性能,可是若是遍歷的節點越靠近中間位置,檢索的效率也就越低
這裏給你們留一個試驗:ArrayList的插入和LinkedList的插入,性能相好比何?須要考慮一下幾個方面:
- ArrayList的擴容問題
- 插入到頭部,中間,尾部的性能
linkedList.get(0); // 在LinkedList中已經記錄了頭節點和尾結點,這裏就是獲得當前的數據就好了 linkedList.getFirst(); linkedList.getLast();
get(index)
咱們上面已經介紹到了,就是經過node(index)
來獲得指定索引的數據的public E get(int index) { checkElementIndex(index); return node(index).item; }
Iterator<String> iterator = linkedList.iterator();
這種模式咱們就不介紹了,iterator()
實現實際上是採用這種方式來作的:
public ListIterator<E> listIterator() { return listIterator(0); } public ListIterator<E> listIterator(final int index) { rangeCheckForAdd(index); return new ListItr(index); } private class ListItr implements ListIterator<E> { private Node<E> lastReturned; private Node<E> next; private int nextIndex; private int expectedModCount = modCount; ListItr(int index) { // assert isPositionIndex(index); next = (index == size) ? null : node(index); nextIndex = index; } }
這裏默認傳入的參數是:0,LinkedList爲咱們開放了該方法:
ListIterator<String> listIterator = linkedList.listIterator(0); // == linkedList.iterator();
ListIterator和Iterator二者的對比咱們上節也介紹過了
那麼,問題來了:在迭代LinkedList的時候咱們該採用那種方式?
咱們來作個實驗進行驗證:1W的數據,咱們來進行驗證
int len = 1_0000; LinkedList<String> linkedList = new LinkedList<String>() {{ for (int i = 0; i < len; i++) { add("item" + i); } }}; long start = System.currentTimeMillis(); for (int i = 0; i < len; i++) { linkedList.get(i); } System.out.println("for i 耗時:" + (System.currentTimeMillis() - start)); start = System.currentTimeMillis(); Iterator<String> iterator = linkedList.iterator(); while (iterator.hasNext()) { iterator.next(); } System.out.println("iterator 耗時:" + (System.currentTimeMillis() - start));
猜一猜最終的結果如何?
咱們來想想爲何:
get(index)
底層實現是:node(index)
,最外層每循環一次,node(index)
每次都會進行一次二分查找,而後循環迭代索引取出對應的值iterator()
的話,只會在構造方法中進行一次node(0)
的迭代取出第一個節點,由於雙向鏈表的形式,因此經過item.next
來就能夠取出對應的元素public E next() { checkForComodification(); if (!hasNext()) throw new NoSuchElementException(); lastReturned = next; next = next.next; nextIndex++; return lastReturned.item; }
因此咱們在LinkedList的迭代的時候,最好採用iterator()
的方式
咱們都是經過remove()
來移除元素,那麼對應其中的實現:
E unlink(Node<E> x) { // assert x != null; // 獲得當前元素,當前next和prev的指向對象 final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; // 若是prev是null,說明是第一個節點 if (prev == null) { first = next; } else { // 不然就將當前節點的prev的下一個節點指向當前節點的next prev.next = next; x.prev = null; } // 若是next是null,說明是最後一個節點 if (next == null) { last = prev; } else { // 不然就將當前節點的prev的下一個節點指向當前節點的prev next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; }
這裏其實就是修改引用的過程,這裏就不展現圖了,你們感興趣的話我在後面給出一個站點,你們能夠在那個站點上查看對應具體的過程
這裏咱們就過了,下面基於LinkedList簡單聊一點隊列的東西
隊列也是咱們經常使用的一種數據結構:並且只容許在一端進行插入操做,另外一端進行刪除操做。
這不就是排排站,先插入進來的先被處理掉。
這種結構能夠稱爲先進先出的方式,也就是咱們所說的:FIFO,具體以下:
那麼,LinkedList又是怎麼作的呢?
簡單來兩個方法來實現一下:
將元素推送到由此列表表示的堆棧上
底層實現也很是簡單,就是調用以前的addFirst(e)
來操做的,都是上面介紹過的
public void push(E e) { addFirst(e); }
檢索並刪除此列表的最後一個元素
public E pollLast() { final Node<E> l = last; return (l == null) ? null : unlinkLast(l); } private E unlinkLast(Node<E> l) { // assert l == last && l != null; final E element = l.item; final Node<E> prev = l.prev; l.item = null; l.prev = null; // help GC last = prev; if (prev == null) first = null; else prev.next = null; size--; modCount++; return element; }
根據LinkedList提供的方法就能夠有不少中實現方式,只要知足先進先出的方式
和ArrayList是同樣的,若是隻是當作局部變量來使用的話,是不存在線程問題的;可是若是當作共享資源來使用,那麼必然是線程不安全的,針對解決方式:
更多關於LinkedList使用方法推薦查看其文檔: