Java集合框架位於java.util包下,主要包含List、Set、Map、Iterator和Arrays、Collections集合工具類,涉及的數據結構有數組、鏈表、隊列、鍵值映射等,Collection是一個抽象接口,對應List、Set兩類子接口,Map是key-value形式的鍵值映射接口,Iterator是集合遍歷的迭代器,下面是總體框架圖java
咱們先來看看初始化方式,node
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
複製代碼
從源碼中定義的兩個Object數組可知ArrayList採用數組做爲基本存儲方式,在String字節碼中也有定義數組,不過是private final char[] value,transient關鍵字主要是序列化時忽略當前定義的變量;在ArrayList無參函數中給定默認數組長度爲10,在實際開發中,通常若是能預知數組長度則會調用帶有長度閾值的構造函數,算法
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);
}
}
複製代碼
源碼方法中會直接按照指定長度建立Object數組並賦值給this.elementData,接下來繼續看看沒有指定數組長度時,數組是如何擴容從而知足可變長度?此時ArrayList中的add方法登場數組
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
複製代碼
size爲ArrayList中定義的int類型變量,默認爲0,當調用ensureCapacityInternal(1),繼續往下看bash
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
複製代碼
該方法會判斷底層數組elementData與臨時數組DEFAULTCAPACITY_EMPTY_ELEMENTDATA是否相等,若是是就取兩個閾值中的最大值,minCapacity爲1,DEFAULT_CAPACITY爲10(默認值),而後繼續走ensureExplicitCapacity(10),數據結構
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
複製代碼
modCount用於記錄操做次數,若是minCapacity大於底層數組長度,開始調用擴容方法grow框架
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
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);
}
複製代碼
oldCapacity爲默認底層數組長度,newCapacity = oldCapacity + (oldCapacity >> 1)等價於 newCapacity = oldCapacity + (oldCapacity / 2);在Java位運算中,oldCapacity << 1 至關於oldCapacity乘以2;oldCapacity >> 1 至關於oldCapacity除以2 , 此時新的長度爲原始長度的1.5倍,若是擴容後的長度小於minCapacity,則直接賦值爲minCapacity,再往下的if判斷中是對int最大值的邊界斷定,能夠看到最後經過Arrays.copyOf進行數組的copy操做,這是Arrays工具類中的方法,該方法最終調用以下函數
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));
return copy;
}
複製代碼
經過System.arraycopy進行數組複製並return this,System.arraycopy方法爲native方法,對應方法以下工具
//原始數組 //位置 //目標數組 //位置
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos,
int length);//copy長度
複製代碼
調用一連串方法最終也只是copy一個默認長度爲10的空數組,咱們繼續看add方法中的 elementData[size++] = e;//把當前對象放置在elementData[0]上,在ArrayList中size()方法是直接返回定義的size值,即爲返回數組元素長度,而非底層數組長度(默認10),因此ArrayList在初始時就佔用必定空間,下面咱們看下ArrayList中的查詢方法oop
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
return (E) elementData[index];
}
複製代碼
若是要查找List中某個對象,假如已知對象在數組中的位置,則直接return (E) elementData[index]返回,在計算算法效率中以O表示時間,此時能夠以O(1)表示查詢到指定對象的時間複雜度,由於經過下標查找咱們只須要執行一次,若是咱們沒法得知具體下標,一般是for循環查找位置直到返回對象,假設數組長度爲n,此時的時間複雜度爲O(n),這種方式實則取的是查找到該對象所消耗的時間的最大值,有可能在for循環中第一個或是中間一個位置就已經查找到了,則可記爲O(1)或者O(n/2)
LinkedList基於雙向鏈表+內部類Node<>泛型集合實現,初始化沒有默認空間大小,根據頭尾節點查找元素,下面先看下雙向鏈表的數據結構圖
transient int size = 0; //transient標記序列化時忽略
transient Node<E> first; //頭節點
transient Node<E> last; //尾節點
public LinkedList() {
}
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;
}
}
複製代碼
E表示泛型元素類型,prev記錄當前元素的上一個節點,next記錄下一個節點,當咱們往LinkedList中add一個元素時,看下源碼是怎麼處理Node節點,
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
複製代碼
在進行添加操做時默認是追加至last節點,linkLast方法中首先將當前數組中last節點賦值臨時變量,而後調用Node<>構造函數將當前添加元素與last關聯,此時l賦值給Node<>中的perv節點,接着判斷l是否爲null,若是是表示數組中沒有元素,就直接賦值給first做爲第一個,不然就追加至原數組最後一個元素的next節點,從而完成add操做,下面咱們看下指定插入位置add方法,
public void add(int index, E element) {
checkPositionIndex(index); //校驗index邊界>=0 && <=size
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;
}
}
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
複製代碼
linkBefore方法中首先會調用node(int index)節點生成方法,該方法中首先經過二分法的方式斷定元素插入位置,而後分別對first、last節點中的next、prev節點進行賦值操做,最後返回插入的節點元素,傳遞給linkBefore方法,該方法會判斷傳入的節點元素的上一個節點是否爲null,對節點進行相應賦值操做,從而完成指定下標插入元素,下面繼續看下LinkedList刪除元素方法remove(int index),
public E remove(int index) {
checkElementIndex(index); //index邊界斷定>=0 && <size
return unlink(node(index));
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
複製代碼
刪除元素方法基本都會調用unlink(Node x),原理就是把x元素的先後節點指向關係進行替換,而後將當前x元素全部屬性置空,達到刪除元素目的同時等待GC回收,順帶看下LinkedList查詢方法
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
複製代碼
核心是經過相似二分法定位下標對應的元素並返回該對象的element值,能夠看到LinkedList在增刪元素時,只是修改當前下標所在元素的先後節點指向關係,相對於ArrayList的copy數組效率要高,而查詢元素時雖採用二分法提升查詢效率,但其時間複雜度仍是O(logN),二分法找一次就排除一半的可能,log是以2做爲底數,相對於ArrayList直接索引查詢要慢得多
PriorityQueue是基於優先堆的一個無界隊列,該優先隊列中的元素默認以天然排序方式或者經過傳入可比較的Comparator比較器進行排序,下面看下PriorityQueue的add方法源碼
private static final int DEFAULT_INITIAL_CAPACITY = 11; //隊列默認大小
transient Object[] queue; //隊列底層數組存儲結構
int size;
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}
複製代碼
在offer方法中若是插入的元素爲null則會直接拋出異常,當隊列長度大於等於容量值時開始自動擴容,grow方法與ArrayList的擴容方法相似,最後都調用了Arrays.copyOf方法,惟一區別在於擴容長度不同,可自行查看此處源碼,offer方法中i=0時標記爲隊列第一個元素,直接賦值queue[0],若是不是第一個,則開始調用siftUp()上浮方法,
private void siftUp(int k, E x) { // k != 0
if (comparator != null)
siftUpUsingComparator(k, x); //指定排序比較器
else
siftUpComparable(k, x); //使用默認天然順序比較器
}
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable,<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
複製代碼
在siftUpComparable方法中,將傳入的可比較的對象轉換爲Comparable,若是k下標大於0,計算父節點的下標int parent = (k - 1) >>> 1 等價於int parent = (k - 1)/2;而後取出父一級的節點對象,經過compareTo方法對插入的對象於當前對象比較是否>=0,若是不大於則把當前對象賦值給k位置,再把parent位置賦值給k作替換,最後經過queue[k] = key實現元素上浮排序,繼續看下remove方法
public boolean remove(Object o) {
int i = indexOf(o); //遍歷數組找到第一個知足o.equals(queue[i])元素的下標
if (i == -1)
return false;
else {
removeAt(i);
return true;
}
}
E removeAt(int i) {
// assert i >= 0 && i < size;
modCount++;
int s = --size;
if (s == i) // removed last element
queue[i] = null;
else {
E moved = (E) queue[s];
queue[s] = null;
siftDown(i, moved); //調整順序
if (queue[i] == moved) {
siftUp(i, moved);
if (queue[i] != moved)
return moved;
}
}
return null;
}
複製代碼
刪除元素時下標是從後往前,當i = s是最後一個元素下標時直接置空,不然從隊列數組中取出要刪除的元素,調用一次siftDown下沉方法對最小堆節點位置進行調整,以不指定比較器的方法源碼分析
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = queue[child];
int right = child + 1;
if (right < size && //對比左右節點大小
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c; //將子節點c上移
k = child;
}
queue[k] = key; //key向下移動到k位置
}
複製代碼
根據int half = size/2 找到非葉子節點的最後一個節點下標並與當前k的位置做比較,從k的位置開始,將x逐層向下與當前節點的左右節點中較小的那個交換,直到x小於或等於左右節點中的任何一個爲止,從而達到刪除非最後一個元素的節點排序,相應的時間複雜度也是O(logN),經過此處的方法也能夠得知在數組下標從0開始狀況下,節點下標計算方式爲
left = k * 2 + 1 ,right = k * 2 + 2, parent = (k -1) / 2, 固然PriorityQueue還有一些其它Queue接口的實現方法,如poll、peek方法,包括在concurrent包下的PriorityBlockingQueue,DelayQueue,ConcurrentLinkedDeque等實現Queue、Deque接口的擴展類,可自行去看下jdk 1.8源碼實現,加深二叉隊列排序原理的理解
HashSet底層是基於HashMap實現,限於本文篇幅過長,剩餘源碼分析參見下篇
加入星球一塊兒討論項目、研究新技術,共同成長!