注:示例基於JDK1.8版本node
參考資料:Java知音公衆號 算法
本文超長,也是搬運的乾貨,但願小夥伴耐心看完。數組
Collection集合體系安全
List、Set、Map是集合體系的三個接口。數據結構
其中List和Set繼承了Collection接口。app
List有序且元素能夠重複,默認大小爲10;ArrayList、LinkedList和Vector是三個主要的實現類。源碼分析
Set元素不能夠重複,HashSet和TreeSet是兩個主要的實現類。性能
Map也屬於集合系統,但和Collection接口不一樣。Map是key-value鍵值對形式的集合,key值不能重複,value能夠重複;HashMap、TreeMap和Hashtable是三個主要的實現類。測試
--------------------------------------------------------------------------------------------------------------------優化
1、ArrayList
ArrayList基層是以數組實現的,能夠存儲任何類型的數據,但數據容量有限制,超出限制時會擴增50%容量,查找元素效率高。
ArrayList是一個簡單的數據結構,因超出容量會自動擴容,可認爲它是常說的動態數組。
源碼分析
A、屬性分析
/** * 默認初始化容量 */ private static final int DEFAULT_CAPACITY = 10; /** * 若是自定義容量爲0,則會默認用它來初始化ArrayList。或者用於空數組替換。 */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * 若是沒有自定義容量,則會使用它來初始化ArrayList。或者用於空數組比對。 */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * 這就是ArrayList底層用到的數組 * 非私有,以簡化嵌套類訪問 * transient 在已經實現序列化的類中,不容許某變量序列化 */ transient Object[] elementData; /** * 實際ArrayList集合大小 */ private int size; /** * 可分配的最大容量 */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
B、構造方法分析
一、不帶參數初始化,默認容量爲10
/** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
二、根據initialCapacity初始化一個空數組,若是值爲0,則初始化一個空數組
/** * 根據initialCapacity 初始化一個空數組 */ public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
三、經過集合作參數的形式初始化,若是集合爲空,則初始化爲空數組
/** * 經過集合作參數的形式初始化 */ public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
C、主要方法
一、trimToSize()方法:
用來最小實例化存儲,將容器大小調整爲當前元素所佔用的容量大小。
/** * 這個方法用來最小化實例存儲。 */ public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }
判斷size的值,若爲0,則將elementData置爲空集合,若大於0,則將一份數組容量大小的集合複製給elementData。
二、clone()方法
克隆一個新數組。
public Object clone() { try { ArrayList<?> v = (ArrayList<?>) super.clone(); v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } }
經過調用Object
的clone()
方法來獲得一個新的ArrayList
對象,而後將elementData
複製給該對象並返回。
三、add(E e)
在ArrayList末尾添加元素。
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
該方法首先調用了ensureCapacityInternal()方法,注意參數是size+1(數組已有參數個數+1個新參數),先來看看ensureCapacityInternal的源碼:
private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); }
方法說明:計算容量+確保容量(上上代碼)
計算容量:若elementData爲空,則minCapacity值爲默認容量和size+1(minCapacity)的最大值;若elementData不爲空,minCapacity(size+1)不用進行操做
確保容量:若是size+1 > elementData.length
證實數組已經放滿,則增長容量,調用grow()
。
private void ensureExplicitCapacity(int minCapacity) { modCount++;//計算修改次數 // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; // 擴展爲原來的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); // 若是擴爲1.5倍還不知足需求,直接擴爲需求值 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
增長容量grow():默認1.5倍擴容。
獲取當前數組長度=>oldCapacity
oldCapacity>>1 表示將oldCapacity右移一位(位運算),至關於除2。再加上1,至關於新容量擴容1.5倍。
若是newCapacity<mincapacity`,則`newcapacity mincapacity="size+1=2" elementdata="1" newcapacity="1+1""">>1=1
,1<2
因此若是不處理該狀況,擴容將不能正確完成。
若是新容量比最大值還要大,則將新容量賦值爲VM要求最大值。
將elementData拷貝到一個新的容量中。
也就是說,當增長數據的時候,若是ArrayList的大小已經不知足需求時,那麼就將數組變爲原長度的1.5倍,以後的操做就是把老的數組拷到新的數組裏面。
例如,默認的數組大小是10,也就是說當咱們add10個元素以後,再進行一次add時,就會發生自動擴容,數組長度由10變爲了15具體狀況以下所示:
四、add(int index, E element)方法
public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
rangeCheckForAdd()是越界異常檢測方法。
private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
System.arraycopy方法:
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
Object src : 原數組
int srcPos : 從元數據的起始位置開始
Object dest : 目標數組
int destPos : 目標數組的開始起始位置
int length : 要copy的數組的長度
五、set(int index, E element)方法:
用指定元素替換此列表中指定位置的元素。
public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; } E elementData(int index) { return (E) elementData[index]; }
六、indexOf(Object o):
返回數組中第一個與參數相等的值的索引,容許null。
public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1; }
七、get(int index)方法:
返回指定下標處的元素的值。
public E get(int index) { rangeCheck(index); return elementData(index); }
rangeCheck(index)
會檢測index值是否合法,若是合法則返回索引對應的值。
八、remove(int index):
刪除指定下標的元素。
public E remove(int index) { // 檢測index是否合法 rangeCheck(index); // 數據結構修改次數 modCount++; E oldValue = elementData(index); // 記住這個算法 int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; }
ArrayList優缺點
優勢:
一、由於其底層是數組,因此修改和查詢效率高。
二、自動擴容(1.5倍)。
缺點:
一、插入和刪除效率不高。(文末對比LinkedList)
二、線程不安全。
2、LinkedList
LinkList以雙向鏈表實現,鏈表無容量限制,但雙向鏈表自己使用了更多空間,每插入一個元素都要構造一個額外的Node對象,也須要額外的鏈表指針操做。容許元素爲null,線程不安全。
源碼分析
一、變量
/** * 集合元素數量 **/ transient int size = 0; /** * 指向第一個節點的指針 * Invariant: (first == null && last == null) || * (first.prev == null && first.item != null) */ transient Node<E> first; /** * 指向最後一個節點的指針 * Invariant: (first == null && last == null) || * (last.next == null && last.item != null) */ transient Node<E> last;
二、構造方法
/** * 無參構造方法 */ public LinkedList() { } /** * 將集合c全部元素插入鏈表中 */ public LinkedList(Collection<? extends E> c) { this(); addAll(c); }
三、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; } }
Node既有prev也有next,因此證實它是一個雙向鏈表。
四、添加元素
a、addAll(Collection c)方法
將集合c添加到鏈表,若是不傳index,則默認是添加到尾部。若是調用addAll(int index, Collection<? extends E> c)
方法,則添加到index後面。
/** * 將集合添加到鏈尾 */ 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; //若是新增元素數量爲0,則不增長,並返回false if (numNew == 0) return false; //定義index節點的前置節點,後置節點 Node<E> pred, succ; // 判斷是不是鏈表尾部,若是是:在鏈表尾部追加數據 //尾部的後置節點必定是null,前置節點是隊尾 if (index == size) { succ = null; pred = last; } else { // 若是不在鏈表末端(而在中間部位) // 取出index節點,並做爲後繼節點 succ = node(index); // index節點的前節點 做爲前驅節點 pred = succ.prev; } // 鏈表批量增長,是靠for循環遍歷原數組,依次執行插入節點操做 for (Object o : a) { @SuppressWarnings("unchecked") // 類型轉換 E e = (E) o; // 前置節點爲pred,後置節點爲null,當前節點值爲e的節點newNode Node<E> newNode = new Node<>(pred, e, null); // 若是前置節點爲空, 則newNode爲頭節點,不然爲pred的next節點 if (pred == null) first = newNode; else pred.next = newNode; pred = newNode; } // 循環結束後,若是後置節點是null,說明此時是在隊尾追加的 if (succ == null) { // 設置尾節點 last = pred; } else { //不然是在隊中插入的節點 ,更新前置節點 後置節點 pred.next = succ; succ.prev = pred; } // 修改數量size size += numNew; //修改modCount modCount++; return true; } /** * 取出index節點 */ Node<E> node(int index) { // assert isElementIndex(index); // 若是index 小於 size/2,則從頭部開始找 if (index < (size >> 1)) { // 把頭節點賦值給x Node<E> x = first; for (int i = 0; i < index; i++) // x=x的下一個節點 x = x.next; return x; } else { // 若是index 大與等於 size/2,則從後面開始找 Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } } // 檢測index位置是否合法 private void checkPositionIndex(int index) { if (!isPositionIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } // 檢測index位置是否合法 private boolean isPositionIndex(int index) { return index >= 0 && index <= size; }
假設咱們要在index=2處添加{1,2}到鏈表中,圖解以下:
第一步:拿到index=2的前驅節點 prev=ele1
第二步:遍歷集合prev.next=newNode,並實時更新prev節點以便下一次遍歷:prev=newNode
第三步:將index=2的節點ele2接上:prev.next=ele2,ele2.prev=prev
注意node(index)方法:方法:尋找處於index的節點,有一個小優化,結點在前半段則從頭開始遍歷,在後半段則從尾開始遍歷,這樣就保證了只須要遍歷最多一半結點就能夠找到指定索引的結點。
b、addFirst(E e)方法
將e元素添加到鏈表並設置其爲頭節點(first)。
public void addFirst(E e) { linkFirst(e); } //將e連接成列表的第一個元素 private void linkFirst(E e) { final Node<E> f = first; // 前驅爲空,值爲e,後繼爲f final Node<E> newNode = new Node<>(null, e, f); first = newNode; //若f爲空,則代表列表中尚未元素,last也應該指向newNode if (f == null) last = newNode; else //不然,前first的前驅指向newNode f.prev = newNode; size++; modCount++; }
拿到first節點命名爲f
新建立一個節點newNode設置其next節點爲f節點
將newNode賦值給first
若f爲空,則代表列表中尚未元素,last也應該指向newNode;不然,前first的前驅指向newNode。
圖解以下:
c、addLast(E e)方法
將e元素添加到鏈表並設置其爲尾節點(last)。
public void addLast(E e) { linkLast(e); } /** * 將e連接成列表的last元素 */ void linkLast(E e) { final Node<E> l = last; // 前驅爲前last,值爲e,後繼爲null final Node<E> newNode = new Node<>(l, e, null); last = newNode; //最後一個節點爲空,說明列表中無元素 if (l == null) //first一樣指向此節點 first = newNode; else //不然,前last的後繼指向當前節點 l.next = newNode; size++; modCount++; }
過程與linkFirst()
方法相似,這裏略過。
d、add(E e)方法
在尾部追加元素e。
public boolean add(E e) { linkLast(e); return true; } void linkLast(E e) { final Node<E> l = last; // 前驅爲前last,值爲e,後繼爲null final Node<E> newNode = new Node<>(l, e, null); last = newNode; //最後一個節點爲空,說明列表中無元素 if (l == null) //first一樣指向此節點 first = newNode; else //不然,前last的後繼指向當前節點 l.next = newNode; size++; modCount++; }
e、add(int index, E element)方法
在鏈表的index處添加元素element.
public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } /** * 在succ節點前增長元素e(succ不能爲空) */ void linkBefore(E e, Node<E> succ) { // assert succ != null; // 拿到succ的前驅 final Node<E> pred = succ.prev; // 新new節點:前驅爲pred,值爲e,後繼爲succ final Node<E> newNode = new Node<>(pred, e, succ); // 將succ的前驅指向當前節點 succ.prev = newNode; // pred爲空,說明此時succ爲首節點 if (pred == null) // 指向當前節點 first = newNode; else // 不然,將succ以前的前驅的後繼指向當前節點 pred.next = newNode; size++; modCount++; }
五、獲取/查詢元素
a、get(int index)
根據索引獲取鏈表中的元素。
public E get(int index) { checkElementIndex(index); return node(index).item; } // 檢測index合法性 private void checkElementIndex(int index) { if (!isElementIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } // 根據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; } }
b、getFirst()方法
獲取頭節點。
public E getFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return f.item; }
c、getLast()方法
獲取尾節點。
public E getLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return l.item; }
六、刪除元素
a、remove(Object o)
根據Object對象刪除元素。
public boolean remove(Object o) { // 若是o是空 if (o == null) { // 遍歷鏈表查找 item==null 並執行unlink(x)方法刪除 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; } E unlink(Node<E> x) { // assert x != null; // 保存x的元素值 final E element = x.item; //保存x的後繼 final Node<E> next = x.next; //保存x的前驅 final Node<E> prev = x.prev; //若是前驅爲null,說明x爲首節點,first指向x的後繼 if (prev == null) { first = next; } else { //x的前驅的後繼指向x的後繼,即略過了x prev.next = next; // x.prev已無用處,置空引用 x.prev = null; } // 後繼爲null,說明x爲尾節點 if (next == null) { // last指向x的前驅 last = prev; } else { // x的後繼的前驅指向x的前驅,即略過了x next.prev = prev; // x.next已無用處,置空引用 x.next = null; } // 引用置空 x.item = null; size--; modCount++; // 返回所刪除的節點的元素值 return element; }
遍歷鏈表查找 item==null 並執行unlink(x)方法刪除
若是前驅爲null,說明x爲首節點,first指向x的後繼,x的前驅的後繼指向x的後繼,即略過了x.
若是後繼爲null,說明x爲尾節點,last指向x的前驅;不然x的後繼的前驅指向x的前驅,即略過了x,置空x.next
引用置空:x.item = null
圖解:
b、remove(int index)方法
根據鏈表的索引刪除元素。
public E remove(int index) { checkElementIndex(index); //node(index)會返回index對應的元素 return unlink(node(index)); }
c、removeLast()方法
刪除尾節點。
public E removeLast() { final Node<E> l = last; if (l == null) throw new NoSuchElementException(); return 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指向前last的前驅,也就是列表中的倒數2號位 last = prev; // 若是此時倒數2號位爲空,那麼列表中已無節點 if (prev == null) // first指向null first = null; else // 尾節點無後繼 prev.next = null; size--; modCount++; // 返回尾節點保存的元素值 return element; }
七、修改元素
修改元素比較簡單,先找到index對應節點,而後對值進行修改。
public E set(int index, E element) { checkElementIndex(index); // 獲取到須要修改元素的節點 Node<E> x = node(index); // 保存以前的值 E oldVal = x.item; // 執行修改 x.item = element; // 返回舊值 return oldVal; }
LinkedList優勢:不須要擴容和預留空間,空間效率高。
3、ArrayList與LinkedList插入和查找消耗時間測試對比
參考連接:https://blog.csdn.net/dearKundy/article/details/84663512
在ArrayList和LinkedList的頭部、尾部和中間三個位置插入與查找100000個元素所消耗的時間來進行對比測試,下面是測試結果:(時間單位ms)
插入 | 查找 | |
ArrayList尾部 | 26 | 12 |
ArrayList頭部 | 859 | 7 |
ArrayList中間 | 1848 | 13 |
LinkedList尾部 | 28 | 9 |
LinkedList頭部 | 15 | 11 |
LinkedList中間 | 15981 | 34928 |
測試結論:
ArrayList的查找性能絕對是一流的,不管查詢的是哪一個位置的元素
ArrayList除了尾部插入的性能較好外(位置越靠後性能越好),其餘位置性能就不如人意了
LinkedList在頭尾查找、插入性能都是很棒的,可是在中間位置進行操做的話,性能就差很遠了,並且跟ArrayList徹底不是一個量級的
根據源碼分析所得結論: