原文:chenmingyu.top/java-collec…java
java中爲了方便操做多個對象,須要將它們存放到一個容器中,這個容器就是集合類node
集合類提供了豐富的api來簡化咱們的編程,對於多個元素咱們可能會有不一樣的需求,爲此提供了多種集合類,底層數據結構包括數組,鏈表,隊列,棧,哈希表等,全部咱們就能夠根據不一樣的需求選擇合理的集合類進行解決算法
集合類做爲容器類能夠存儲任何類型的數據(存儲對象的引用),沒法存儲基礎類型,對於基礎類型須要將其包裝爲包裝類在進行存儲,底層實現使用數組的集合類支持動態擴容編程
java中的集合類都在java.util包中,可分爲Collection集合和Map映射兩種類型api
實現Collection接口的集合主要包括:List,Set,Queue數組
有序列表,能夠存儲重複數據安全
基於數組實現,因此隨機訪問快,增刪慢(須要移動數據),線程不安全數據結構
咱們看一下添加元素的時候如何進行擴容性能
public boolean add(E e) {
//擴容方法
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//若是數組是空的,使用默認初始容量 10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//擴容
ensureExplicitCapacity(minCapacity);
}
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);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 調用System.arraycopy方法進行擴容
elementData = Arrays.copyOf(elementData, newCapacity);
}
複製代碼
默認擴容的大小爲原來的1.5倍this
刪除方法
public E remove(int 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;
}
複製代碼
刪除數據的時候,若是刪除的數據後面還有數據須要在刪除數據後將刪除數據後面的數據往前移動 擴容和新增刪除須要移動數組,底層使用方法都是System.arraycopy()
基於雙向鏈表實現,因此增刪快,查詢慢,線程不安全
LinkedList中維護了雙向鏈表的頭節點和尾節點,讓咱們看一下節點的結構Node
Node的屬性有當前節點的值,下一個節點和前一個節點,LinkedList整個結構很是清晰
添加元素
public boolean add(E e) {
linkLast(e);
return true;
}
/** * Links e as last element. */
void linkLast(E e) {
// 將最後一個元素賦給變量 l
final Node<E> l = last;
// 構造新的節點 newNode
final Node<E> newNode = new Node<>(l, e, null);
// 將newNode放到隊列尾部
last = newNode;
// 若是原隊列尾部的節點爲null
if (l == null)
// 新構造的節點爲隊列頭節點
first = newNode;
else
// 不爲空則將 l 的next節點指向 newNode
l.next = newNode;
// 隊列數量加1
size++;
// 操做次數加1
modCount++;
}
複製代碼
代碼邏輯清晰,新添加的元素加到隊列的尾部
刪除元素
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
/** * Unlinks non-null first node f. */
private E unlinkFirst(Node<E> f) {
// 獲取當前節點的元素及下一個節點
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
// 將下一個元素設置爲隊列首元素
first = next;
if (next == null)
// 隊列爲空隊列
last = null;
else
// 隊列首元素的前一個節點引用置爲null
next.prev = null;
size--;
modCount++;
return element;
}
複製代碼
刪除的操做邏輯也比較清晰,刪除隊列的頭節點
還有一些其餘方法,可自行查看源碼實現
LinkedList實現了Deque接口,而Deque接口繼承自Queue接口,所以LinkedList也提供了對於雙端隊列的實現,能實現雙端隊列確定也能實現棧
Vector也是數組實現,線程安全,性能較ArrayList差
添加元素
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 擴容爲原來的一倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
複製代碼
看源碼的時候能夠看到,Vector的方法都使用synchronized進行修飾,因此線程安全,可是加鎖就意味着性能損耗,與ArrayList不一樣的是擴容時容量會擴爲原來的一倍
Stack
棧:保證數據先進後出,簡稱FILO原則
Stack繼承Vector類,實現了棧操做,看源碼能夠看到它調用的都是Vector的方法,這個類也基本不怎麼會用,若是想要實現一個棧能夠用實現了Dueue接口的子類,好比LinkedList,後面會詳解
隊列一種特殊的線性表,遵循的原則就是「先入先出」,簡稱FIFO
先看一下Queue接口定義的方法
add:添加元素到隊列尾部,若是操做失敗會報異常
remove:獲取隊列首部第一個元素,並從隊列中刪除,若是操做失敗會報異常
element:獲取隊列首部第一個元素,單不從隊列中刪除,若是操做失敗會報異常
offer:添加一個元素到隊列尾部,操做失敗不會報異常
poll:獲取隊列首部第一個元素,並從隊列中刪除,操做失敗不會報異常
peek:獲取隊列首部第一個元素,單不從隊列中刪除,操做失敗不會報異常
隊列新元素都插入隊列的末尾,移除元素都移除隊列的頭部
隊列
Queue接口的子類按不一樣維度能夠分爲兩種,一種是阻塞仍是非阻塞隊列,一種是單端仍是雙端隊列
阻塞與非阻塞主要看是否實現了BlockingQueue接口或BlockingDeque接口,實現了的是阻塞隊列,好比:ArrayBlockingQueue,LinkedBlockingQueue,LinkedBlockingDeque
未實現的是非阻塞隊列,好比:ArrayDeque,LinkedList,PriorityQueue
**Deque **接口是Queue接口的子接口,表明一個雙端隊列,好比前面學過的LinkedList
Deque 接口提供的方法:
方法的做用能夠根據方法名看出來,對比queue接口提供的方法,queue接口只提供了在隊列尾部添加元素,獲取移除隊列首部的元素,而Deque接口實現了對於隊列雙端的添加刪除操做
Deque 接口的實現類不只能夠當作隊列,也能夠實現棧,好比使用入棧方法:offerFirst(E e); 出棧方法:E peekFirst()
阻塞接口提供的方法:
看下BlockingQueue接口
阻塞接口中的阻塞方法
以ArrayBlockingQueue爲例:
阻塞隊列與非阻塞隊列的實現區別就是是否使用了ReentrantLock和Condition
添加元素:put(E e) 方法
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 隊列滿了
while (count == items.length)
//進入等待狀態
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
複製代碼
當數組裏的數量等於數組的長度,也就是隊列滿了,執行*notFull.await();*進行阻塞,直到當前線程被中斷或者其餘線程調用了改notFull這個Condition的signal()方法或signalAll()方法
刪除元素時阻塞的原理也是同樣的,調用notEmpty.await()進行阻塞,喚醒條件也是同樣的
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
複製代碼
因此,阻塞隊列的實現原理就是用ReentrantLock和Condition實現的
無序集合,不容許存放重複的元素
無序集合,容許元素爲null
HashSet底層使用HashMap實現,採用HashCode算法來存取集合中的元素,所以具備比較好的讀取和查找性能
對於添加的元素key爲要添加值,value爲一個static final object對象,對於添加劇復的值,之前的值會被覆蓋
對於HashSet的迭代,只須要調用的是HashMap的keySet()來獲取到map中的key值的Set集合進行迭代
public Iterator<E> iterator() {
return map.keySet().iterator();
}
複製代碼
HashSet的子類,底層實現是LinkedHashMap,利用雙向鏈表保證了元素的有序性
public LinkedHashSet() {
super(16, .75f, true);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
複製代碼
TreeSet實現了SortedSet接口,因此這個是一種有序的Set集合,查看源碼發現底層實現是用的TreeMap,而TreeMap使用紅黑樹實現
public TreeSet() {
this(new TreeMap<E,Object>());
}
複製代碼
使用TreeSet時須要注意,添加到TreeSet中的對象須要實現Comparable重寫compareTo接口,或者自定義一個類實現Comparator接口重寫compare方法,不然會報異常(講TreeMap時會詳解)
Set集合的實現基本都是複用了Map的實現,使用map中的key存儲set中的數據
Map,散列表,它存儲的內容是鍵值對映射(key-value),先上個圖,介紹下要講的四種map實現
最經常使用的當屬HashMap,jdk1.7時底層實現的數據結構是散列表(數組+鏈表),jdk1.8時底層數據結構爲數組+鏈表/紅黑樹,1.8加入紅黑樹是爲了解決鏈表過長所帶來的性能消耗
結構以下:網上找的圖片
經常使用的屬性:
/** * 默認容量 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/** * 默認的平衡因子 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/** * 由鏈表轉爲紅黑樹的閾值 */
static final int TREEIFY_THRESHOLD = 8;
/** * 由紅黑樹轉爲鏈表的閾值 */
static final int UNTREEIFY_THRESHOLD = 6;
/* * 負載因子 */
final float loadFactor;
/** * 是否擴容的閾值 threshold = 容量 * loadFactor */
int threshold;
/** * 數組,桶,槽位 存儲該位置上的鏈表的頭節點 */
transient Node<K,V>[] table;
複製代碼
咱們知道Hashmap中的數組存儲的是鏈表的頭節點的引用,那咱們看下節點的結構:
爲鏈表時節點的結構:
包括hash值,key,value,以及指向下一個節點的引用
爲紅黑樹時節點的結構:
包括父節點,左右節點,和節點顏色
對元素進行操做時,好比添加操做,會經過key的hash值找對應的數組下標位置,若是該位置對應的鏈表或紅黑樹爲空,則該元素爲頭節點,若是有元素,則調用equals方法進行比較,若是相等就進行覆蓋,若是不相等就進行添加
LinkedHashMap繼承自HashMap,與hashMap相比最大的區別在於LinkedHashMap存儲的數據是有序的
在LinkedHashMap內部維護了一個雙端隊列,保證添加的數據的順序性
LinkedHashMap重寫了HashMap提供的模板方法來對鏈表進行維護
在HashMap中的put操做
其中afterNodeAccess(e)就是暴露給子類的模板方法,此外還有:
TreeMap是SortedMap接口的實現類
TreeMap 是一個有序的key-value集合,經過紅黑樹實現,每一個key-value做爲紅黑樹的一個節點
Comparator,在講TreeSet的時候說過,TreeSet支持兩種排序方式,天然排序和定製排序
天然排序:key須要實現Comparable接口重寫compareTo接口,並且全部的key應該是同一個類的對象,不然會拋出異常
定製排序:自定義一個類實現Comparator接口重寫compare方法,這個類負責對TreeMap中的全部key進行排序,不然會拋出異常
咱們看下紅黑樹的節點:
包括:key,value信息,父節點,左右節點和節點顏色
在HashMap中判斷節點是否相等時,是先比較key的hash值,若是相等在用equals進行比較
而在TreeMap中是兩個key經過compareTo()方法若是返回值是0,則兩個key相等
Hashtable,散列表,存儲的也是鍵值對,繼承自Dictionary抽象類,其提供的方法都是同步的,key和value都不能夠爲null,數據結構爲散列表(數組+鏈表)
實現原理與HashMap相同,使用Synchronize實現線程安全,看了源碼,擴容時容量爲newCapacity = (oldCapacity << 1) + 1; 感興趣的能夠自行查看下源碼
終於寫完了,Java中經常使用的集合類都有講到,不過有些地方只是一律而過,感興趣的可自行看下源碼實現