內容更新中……
Java集合框架
集合類
Collection(interface)
- List(interface)
- ArrayList:數組實現,適合隨機訪問元素
- LinkedList(實現了Queue接口):鏈表實現,適合插入、刪除、移動
- Vector(與ArrayList相比多了個線程安全)
Set(interface)
- HashSet(使用散列函數)——> 通過HashMap實現,add(E e)是通過HashMap的add(K,V)方法實現,E是K;remove(Object o)是通過HashMap的remove(Object o)實現的。按哈希值保存其值,通過Iterator迭代器訪問hashset。
- TreeSet(使用紅黑樹)——> 通過TreeMap實現。保存的值是排序的。
- LinkedHashSet(鏈表結合散列函數)。繼承了HashSet,通過LinkedHashMap實現的。值是按照插入的順序存儲的。(結構有HashMap,和LinkList。雙向鏈表存儲插入的Entry元素。)
Queue(interface)
- List(interface)
Map(interface)
- HashMap(可以存放null)
- TreeMap(不可以存放null,排序功能)
- HashTable(不可以存放null,與HashMap比多了個線程安全)
其中線程安全類除了上面說的Vector和HashTable外,還有Stack、Enumeration是線程安全類。
除此集合類外,線程安全的類還有StringBuffer等。
線程安全是指在訪問方法時不允許別的線程去改變,並不是說一系列操作是同步的。
比如StringBuffer的apeend方法線程安全的,但是調用多次append方法可能會對最後的結果造成順序上的差別(append單句是順序的)。
幾個類和接口的比較
Collection:接口,各種集合結構的父接口
Collections:專用靜態類,包含各種有關集合操作的靜態方法。提供一系列靜態方法實現對各種集合的搜索、排序、線程安全化等操作。
Array:Java中最基本的一個存儲結構,提供動態創建和訪問Java數組的方法。元素類型必須相同。效率高,但容量固定無法改變。無法判斷實際多少元素。
Arrays:靜態類。專門用來操作數組,提供搜索、排序、複製等靜態方法。
HashMap的設計:
數據結構中有數組和鏈表來存數據。
數組的優缺點:訪問迅速,插入和刪除難
鏈表的優缺點:插入和刪除簡單,訪問複雜度高
這確實是兩個極端。
如何綜合數組和鏈表的特性,做出訪問容易,插入和刪除都容易的數據結構?
可以,就是哈希表(HashTable):拉鍊法,使用數組,數組裏面是鏈表(鏈表的數組,數組中存的是鏈表的頭結點,就像鄰接表一樣)
這些元素是通過散列公式存進去的。
HashMap也是一個線性的數組實現的,其存儲的容器就是一個線性數組。(如何用線性數組來存取鍵值對呢)
首先HashMap裏面有一個靜態內部類Entry,其重要的屬性有key,value,next,從key,value看出來Entry是HashMap鍵值對實現的一個基礎bean,HashMap的基礎就是一個線性數組,這個數組就是Entry[],Map裏面的內容都保存在Entry[]裏面。
transient Entry[] table;
HashMap的存取實現
//存儲時
int hash = key.hashCode();
int index = hash % Entry[].length;
Entry[hash] = value;
//取值時
int hash = key.hashCode;
int index = hash % Entry[].length;
return Entry[index];
Put操作
如果兩個key通過hash%Entry[].length得到的index相同會不會有覆蓋的風險(兩個不同的key擁有相同的index)。
因爲有個next屬性,作用是指向下一個Entry。如果A的index是0,B的index也是0,那麼進來B的時候,A的next是B。index = 0的位置其實是放了三個鍵值對的。數組中存儲的是最後插入的元素。
public V put(K key, V value){
if(k == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash,table.length);
//遍歷鏈表,看有沒有相同的,就是拉鍊法的那個拉鍊後面的東西
for(Entry<K,V> e = table[i] ; e != null ; e = e.next){
Object k;
if(e.hash == hash && ((k = e.key) == key || key.equals(k)) )//如果key在鏈表中已存在,替換爲新的value{
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash,key,value,i);
return null;
}
void addEntry(int hash ,K key , V value, int buketIndex)
{
Entry<K,V> e = table[buketIndex];
table[bucketIndex] = new Entry<K,V>(hash,key,value,e);//e是Entry.next
if(size++ >= threshold)
resize(2*table.length);
}
HashMap裏面包含一些優化方面的實現,比如Entry數組的長度一定後,隨着map裏面數據的越來越長,這樣同一個index的鏈長度就會很長,網HashMap裏面設置一個引子,隨着map的size越來越大,Entry[]會以一定的規則加長長度。
說完HashMap後,從源碼來簡單看看Collection的子接口List接口的實現類ArrayList的添加和刪除元素到底是怎麼操作的
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
直接添加元素就是先判斷是否需要擴容,然後直接把元素添加到末尾
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++;
}
先看這個添加的函數,在索引index處添加元素E element。
首先是確定index的範圍,然後根據當前的size+1判斷是否需要擴容(擴容機制:看當前的arraylist是否爲空,如果爲空就是取默認容量和傳入參數的較大值,然後調用enSureExplicitCapacity方法進行擴容。
modCount++,然後看傳入的size+1參數是否大於add之前數組 elementData 的大小,如果是,就調用grow方法進行擴容 newCapacity=oldCapacity+(oldCapacity>>1),擴大1.5倍,然後將擴容後的數組複製一份Array.copyof給 elementData )
再者就是使用native方法arraycopy去對數組進行一個複製。其聲明如下:
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
將elementData數組第index個元素後的元素拷到elementData從第index+1位置到後面的位置,長度是size-index。其實做的就是後移操作。
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;
}
至於這個移除第index個位置的元素操作,依舊先檢查index的範圍,修改次數modCount加1,然後獲取要刪除索引的元素的值。
然後就是數組往前移動覆蓋到index的值,如果移動的長度要大於0,則使用System.arraycopy 的方法去複製數組;然後把數組的最後一個元素置空,讓GC去回收它,返回的是一箇舊的元素的值。
(PS:在一個迭代器初始的時候會賦予它調用這個迭代器的對象的mCount,如何在迭代器遍歷的過程中,一旦發現這個對象的mcount和迭代器中存儲的mcount不一樣那就拋異常。
下面是這個mcount相關機制的解釋引用:
Fail-Fast 機制
我們知道 java.util.HashMap 不是線程安全的,因此如果在使用迭代器的過程中有其他線程修改了map,那麼將拋出ConcurrentModificationException,這就是所謂fail-fast策略。這一策略在源碼中的實現是通過 modCount 域,modCount 顧名思義就是修改次數,對HashMap 內容的修改都將增加這個值,那麼在迭代器初始化過程中會將這個值賦給迭代器的 expectedModCount。在迭代過程中,判斷 modCount 跟 expectedModCount 是否相等,如果不相等就表示已經有其他線程修改了 Map:注意到 modCount 聲明爲 volatile,保證線程之間修改的可見性。所以當大家遍歷那些非線程安全的數據結構時,儘量使用迭代器)
由此也可以聯想在遍歷ArrayList的過程中,不能add和remove元素,此時會報錯誤
java.util.ConcurrentModificationException
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
再看一下這個移除object的操作,如果要移除的對象爲空,List是可以添加null的。所以需要遍歷list,然後根據index去查看如果有相同的那麼就用新的數組去覆蓋原來舊的數組。
private void fastRemove(int index) {
modCount++;
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
}
移除元素操作用到了快速移除fastRemove方法,與remove(int index)方法類似,只是不需要檢查index的範圍,因爲這個index已經確定好是存在的。
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
clear元素很簡單,全部置空然後長度歸0
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
往ArrayList中添加實現Collection的類,先轉換成Object數組,然後判斷當前默認值和傳入參數(兩個list的長度和)確定是否需要擴容,再把新的數組a複製到數組的末尾。
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray(); // c可以轉換成數組
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
在指定位置index中往ArrayList中添加實現Collection的類,先轉換成Object數組,然後判斷當前默認值和傳入參數(兩個list的長度和)確定是否需要擴容,把原list數組index及以後的數據移到index+numNew(要添加數組的長度)處,再把新的數組a複製到數組的index處,再更新新list的size。
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
removeAll首先需要判斷要移除的c是否爲空,如果爲空,拋出空指針異常。
removeAll基於batchRemove來實現的,batchRemove源碼如下:
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
先將當前的數據備份到同步局部變量final數組elementData(這個數組不能指向其它數組對象,內容可以變),設置讀寫兩個指針r,w。遍歷當前list,把當前list中除了要移除的元素之外的元素保存到elementData中。如果沒能遍歷完list(可能c.contains拋出異常),將未能夠遍歷的元素(index爲r之後)全部複製到w指位置後(類似於未能遍歷元素的恢復)。w指針更新位置(w位置和w之前的元素就是新數組的元素)。如果w位置不在list末尾,說明元素改動過了(至少有一次c.contains爲true),此時將w後面多餘的元素刪除置空,讓GC回收它們。