ArrayList、LinkedList、Vector區別和實現原理。
ArrayList、LinkedList、Vector是集合中經常拿來比較和麪試的一個問題,我這裏簡要概括一下他們的區別和實現原理。這裏需要區別jdk1.6和jdk1.8。我們從三個方面去闡述:
存儲結構
ArrayList和Vector是按照順序將元素存儲(從下標爲0開始),刪除元素時,刪除操作完成後,需要使部分元素移位,默認的初始容量都是10(但jdk1.6確實初始容量爲10),但jdk1.8,如果只是初始化無參構造函數時,初始容量爲0,當第一次添加add()時,會擴容到10。
1.當創建方式爲 List list = new ArrayList(0)時,默認調用EMPTY_ELEMENTDATA初始化容量爲0,當首次添加元素時,容量擴爲 1;
ArrayList源碼:
//被用於空實例的共享空數組實例
private static final Object[] EMPTY_ELEMENTDATA = {};
//當創建爲ArrayList(0),默認調用EMPTY_ELEMENTDATA初始化容量爲0
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);
}
}
//首次添加add()數據時,擴容變爲1;
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
2.當創建方式爲 List list = new ArrayList()時,默認調用DEFAULTCAPACITY_EMPTY_ELEMENTDATA 初始化容量爲0,當首次添加元素時,容量擴爲 10;
//默認初始容量
private static final int DEFAULT_CAPACITY = 10;
//被用於默認大小的空實例的共享數組實例。其與EMPTY_ELEMENTDATA的區別是:當我們向數組中添加第一個元素時,知道數組該擴充多少。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//創建ArrayList()時,即無參構造方法時:默認調用DEFAULTCAPACITY_EMPTY_ELEMENTDATA初始化容量爲0
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//執行添加add()數據之後,查詢集合的size是否爲0
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
//如果size不爲0,調用calculateCapacity()方法
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
//首次添加add()數據時擴容爲10
private static int calculateCapacity (Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
ArrayList和Vector是基於動態數組實現的,LinkedList是基於雙向鏈表實現的(含有頭結點)。
線程安全性
ArrayList不具有有線程安全性,在單線程的環境中,LinkedList也是線程不安全的,如果在併發環境下使用它們,可以用Collections類中的靜態方法synchronizedList()對ArrayList和LinkedList進行調用即可,即可達到線程安全問題。
//調用Collections的靜態方法,即可達到線程安全
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
Vector實現線程安全的,即他的方法大都包含關鍵字synchronized,但是Vector的效率沒有ArraykList和LinkedList高。
//大部分方法被synchronized修飾
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
擴容機制
從內部實現機制來講,ArrayList和Vector都是使用Object的數組形式來存儲的,當向這兩種類型中增加元素的時候,若容量不夠,需要進行擴容。ArrayList擴容後的容量是之前的1.5倍,然後把之前的數據拷貝到新建的數組中去。而Vector默認情況下擴容後的容量是之前的2倍。
Vector可以設置容量增量,而ArrayList不可以。在Vector中,有capacityIncrement:當大小大於其容量時,容量自動增加的量。如果在創建Vector時,指定了capacityIncrement的大小,則Vector中動態數組容量需要增加時,如果容量的增量大於0,則增加的是大小是capacityIncrement,如果增量小於0,則增大爲之前的2倍。
在這裏需要說一下可變長度數組的原理:當元素個數超過數組的長度時,會產生一個新的數組,將原數組的數據複製到新數組,再將新的元素添加到新數組中。
增刪改查的效率
ArrayList和Vector中,從指定的位置檢索一個對象,或在集合的末尾插入,刪除一個元素的時間是一樣的,時間複雜度都是O(1)。但是如果在其他位置增加或者刪除元素花費的時間是O(n),LinkedList中,在插入、刪除任何位置的元素所花費的時間都是一樣的,時間複雜度都爲O(1),但是他在檢索一個元素的時間複雜度爲O(n)。所以如果只是查找特定位置的元素或只在集合的末端增加移動元素,那麼使用ArrayList或Vector都是一樣的。如果是在指定位置的插入、刪除元素,最好選擇LinkedList。
總結:ArrayList:動態數組結構,線程非安全,查詢速度較快,
LinkedList:雙向鏈表結構,線程非安全,增刪比較塊,
Vector :動態數組結構,線程安全。
java中數據存儲方式最底層的兩種結構,一種是數組,另一種就是鏈表,數組的特點:連續空間,尋址迅速,但是在刪除或者添加元素的時候需要有較大幅度的移動,所以查詢速度快,增刪較慢。而鏈表正好相反,由於空間不連續,尋址困難,增刪元素只需修改指針,所以查詢慢、增刪快。有沒有兩者的結合呢?有,哈希表具有較快(常量級)的查詢速度,及相對較快的增刪速度。
--------------------------------------以下內容部分摘抄於這位大神的博客------------------------------------
https://blog.csdn.net/zhangerqing/article/details/8193118
https://www.cnblogs.com/heyonggang/p/9112731.html
hashMap hashtable ConcurrentHashMap區別
HashMap
1.從類定義上:HashMap 繼承自 AbstractMap
public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable{};
2.hashMap內部存儲結構:
數組加鏈表結構:
從上圖中,我們可以發現哈希表是由數組+鏈表組成的,一個長度爲16的數組中,每個元素存儲的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則存儲到數組中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數組長度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存儲在數組下標爲12的位置。它的內部其實是用一個Entity數組來實現的,屬性有key、value、next。接下來我會從初始化階段詳細的講解HashMap的內部結構。
3.初始容量以及擴容:
初始化容量爲16,擴容:newsize = oldsize*2,size一定爲2的n次冪,底層調用rehash()方法進行擴容。 擴容機制:當Map中元素總數超過Entry數組的75%,觸發擴容機制;但,插入元素後才判斷該不該擴容,有可能無效擴容(插入後如果擴容,如果沒有再次插入,就會產生無效擴容)。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
4.線程是否安全:
沒有被synchronized修飾,線程非安全;
public boolean isEmpty() { }
public V get(Object key) { }
public boolean containsKey(Object key) { }
public V put(K key, V value){ }
......
5.hashMap允許鍵值爲空:而在 HashMap 的 put 方法中,調用了 putVal
()方法,該方法需要有一個 int 類型的 hash
值,這個值是利用內部的 hash
方法產生的。從下面的源代碼可以看出,當 key 爲 null 時,返回的 hash 值爲 0,說明在 HashMap 中是允許 key=null 的情況存在的。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict){
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
6.HashMap的初始值還要考慮加載因子: 1)哈希衝突:就是在Entry數組中的位置若干Key的哈希值按數組大小取模後,如果落在同一個數組下標上,將組成一條Entry鏈,對Key的查找需要遍歷Entry鏈上的每個元素執行equals()比較。
2)加載因子:爲了降低哈希衝突的概率,默認當HashMap中的鍵值對達到數組大小的75%時,即會觸發擴容。因此,如果預估容量是100,即需要設定100/0.75=134的數組大小。 3)空間換時間:如果希望加快Key查找的時間,還可以進一步降低加載因子,加大初始大小,以降低哈希衝突的概率
Hashtable
1.從類定義上:
Hashtable繼承Dictionary;
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {};
2.存儲結構上:數組加鏈表結構,和hashMap基本相同
3.初始容量以及擴容:
初始size爲11,擴容:newsize = olesize*2+1,通過rehash()方法進行擴容,
計算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
4.線程是否安全:
Hashtable 在很多方法定義時都會加上 synchronized關鍵字,說明 Hashtable 是線程安全的
public synchronized int size() { return count; }
public synchronized boolean isEmpty() { return count == 0;}
public synchronized V get(Object key){}
public synchronized boolean contains(Object value) {}
......
5.是否允許鍵值爲空:
在 Hashtable 添加元素源碼中,我們可以發現,如果添加元素的 value 爲 null 時,會拋出 NullPointerException。在程序內部,有這樣一行代碼 int hash = key.hashCode
,如果添加的 key 爲 null 時,此時也會拋出空指針異常,因此,在 Hashtable 中,是不允許 key 和 value 爲 null 的
public V setValue(V value) {
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
ConcurrentHashMap
1.底層採用分段的數組+鏈表實現,底層先調用lock(),lock是ReentrantLock類的一個方法,因此是線程安全
2.通過把整個Map分爲N個Segment,可以提供相同的線程安全,但是效率提升N倍,默認提升16倍。(讀操作不加鎖,由於HashEntry的value變量是 volatile的,也能保證讀取到最新的值。)
3.Hashtable的synchronized是針對整張Hash表的,即每次鎖住整張表讓線程獨佔,ConcurrentHashMap允許多個修改操作併發進行,其關鍵在於使用了鎖分離技術,分段加鎖。
4.有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖
5.擴容:段內擴容(段內元素超過該段對應Entry數組長度的75%觸發擴容,不會對整個Map進行擴容),插入前檢測需不需要擴容,有效避免無效擴容
注:
ConcurrentHashMap是使用了鎖分段技術來保證線程安全的。
鎖分段技術:首先將數據分成一段一段的存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。
ConcurrentHashMap提供了與Hashtable和SynchronizedMap不同的鎖機制。Hashtable中採用的鎖機制是一次鎖住整個hash表,從而在同一時刻只能由一個線程對其進行操作;而ConcurrentHashMap中則是一次鎖住一個桶。
ConcurrentHashMap默認將hash表分爲16個桶,諸如get、put、remove等常用操作只鎖住當前需要用到的桶。這樣,原來只能一個線程進入,現在卻能同時有16個寫線程執行,併發性能的提升是顯而易見的。