本篇文章帶你從Java源碼深刻解析關於Java容器的概念。html
參考文獻:java
Java容器相關知識全面總結node
先上一張網上的繼承關係圖api
我的以爲有些地方不是很準確,好比Iterator不是容器,只是一個操做遍歷集合的方法接口,因此不該該放在裏面。而且Map不該該繼承自Collection。因此本身整理了一個經常使用繼承關係圖以下數組
如上圖所示,接下去會自頂向下解釋重要的接口和實現類。bash
在Java容器中一共定義了2種集合, 頂層接口分別是Collection和Map。可是這2個接口都不能直接被實現使用,分別表明兩種不一樣類型的容器。數據結構
簡單來看,Collection表明的是單個元素對象的序列,(能夠有序/無序,可重複/不可重複 等,具體依據具體的子接口Set,List,Queue等);Map表明的是「鍵值對」對象的集合(一樣能夠有序/無序 等依據具體實現)oracle
根據Java官方文檔對Collection的解釋app
The root interface in the collection hierarchy. A collection represents a group of objects, known as its elements. Some collections allow duplicate elements and others do not. Some are ordered and others unordered. The JDK does not provide any direct implementations of this interface: it provides implementations of more specific subinterfaces like Set and List. This interface is typically used to pass collections around and manipulate them where maximum generality is desired.
大概意思就是
是容器繼承關係中的頂層接口。是一組對象元素組。有些容器容許重複元素有的不容許,有些有序有些無序。 JDK不直接提供對於這個接口的實現,可是提供繼承與該接口的子接口好比 List Set。這個接口的設計目的是但願能最大程度抽象出元素的操做。
接口定義:
public interface Collection<E> extends Iterable<E> {
...
}複製代碼
泛型即該Collection中元素對象的類型,繼承的Iterable是定義的一個遍歷操做接口,採用hasNext next的方式進行遍歷。具體實現仍是放在具體類中去實現。
咱們能夠看下定義的幾個重要的接口方法
add(E e)
Ensures that this collection contains the specified element
clear()
Removes all of the elements from this collection (optional operation).
contains(Object o)
Returns true if this collection contains the specified element.
isEmpty()
Returns true if this collection contains no elements.
iterator()
Returns an iterator over the elements in this collection.
remove(Object o)
Removes a single instance of the specified element from this collection, if it is present (optional operation).
retainAll(Collection<?> c)
Retains only the elements in this collection that are contained in the specified collection (optional operation).(**ps:這個平時卻是沒注意,感受挺好用的接口,保留指定的集合**)
size()
Returns the number of elements in this collection.
toArray()
Returns an array containing all of the elements in this collection.
toArray(T[] a)
Returns an array containing all of the elements in this collection; the runtime type of the returned array is that of the specified array.(**ps:這個接口也能夠mark下**)
...複製代碼
上面定義的接口就表明了Collection這一類容器最基本的操做,包括了插入,移除,查詢等,會發現都是對單個元素的操做,Collection這類集合即元素對象的存儲。其中有2個接口平時沒用過可是以爲頗有用
Java官方文檔對Map的解釋
An object that maps keys to values. A map cannot contain duplicate keys; each key can map to at most one value.
This interface takes the place of the Dictionary class, which was a totally abstract class rather than an interface.
The Map interface provides three collection views, which allow a map's contents to be viewed as a set of keys, collection of values, or set of key-value mappings. The order of a map is defined as the order in which the iterators on the map's collection views return their elements. Some map implementations, like the TreeMap class, make specific guarantees as to their order; others, like the HashMap class, do not.
大概意思就是
一個保存鍵值映射的對象。 映射Map中不能包含重複的key,每個key最多對應一個value。
這個接口替代了原來的一個抽象類Dictionary。
Map集合提供3種遍歷訪問方法,1.得到全部key的集合而後經過key訪問value。2.得到value的集合。3.得到key-value鍵值對的集合(key-value鍵值對實際上是一個對象,裏面分別有key和value)。 Map的訪問順序取決於Map的遍歷訪問方法的遍歷順序。 有的Map,好比TreeMap能夠保證訪問順序,可是有的好比HashMap,沒法保證訪問順序。
接口定義以下:
public interface Map<K,V> {
...
interface Entry<K,V> {
K getKey();
V getValue();
...
}
}複製代碼
泛型分別表明key和value的類型。這時候注意到還定義了一個內部接口Entry,其實每個鍵值對都是一個Entry的實例關係對象,因此Map實際其實就是Entry的一個Collection,而後Entry裏面包含key,value。再設定key不重複的規則,天然就演化成了Map。(我的理解)
下面介紹下定義的3個遍歷Map的方法。
Set keySet()
會返回全部key的Set集合,由於key不能夠重複,因此返回的是Set格式,而不是List格式。(以後會說明Set,List區別。這裏先告訴一點Set集合內元素是不能夠重複的,而List內是能夠重複的) 獲取到全部key的Set集合後,因爲Set是Collection類型的,因此能夠經過Iterator去遍歷全部的key,而後再經過get方法獲取value。以下
Map<String,String> map = new HashMap<String,String>();
map.put("01", "zhangsan");
map.put("02", "lisi");
map.put("03", "wangwu");
Set<String> keySet = map.keySet();//先獲取map集合的全部鍵的Set集合
Iterator<String> it = keySet.iterator();//有了Set集合,就能夠獲取其迭代器。
while(it.hasNext()) {
String key = it.next();
String value = map.get(key);//有了鍵能夠經過map集合的get方法獲取其對應的值。
System.out.println("key: "+key+"-->value: "+value);//得到key和value值
}複製代碼
Collection values()
直接獲取values的集合,沒法再獲取到key。因此若是隻須要value的場景能夠用這個方法。獲取到後使用Iterator去遍歷全部的value。以下
Map<String,String> map = new HashMap<String,String>();
map.put("01", "zhangsan");
map.put("02", "lisi");
map.put("03", "wangwu");
Collection<String> collection = map.values();//返回值是個值的Collection集合
System.out.println(collection);複製代碼
Set< Map.Entry< K, V>> entrySet()
是將整個Entry對象做爲元素返回全部的數據。而後遍歷Entry,分別再經過getKey和getValue獲取key和value。以下
Map<String,String> map = new HashMap<String,String>();
map.put("01", "zhangsan");
map.put("02", "lisi");
map.put("03", "wangwu");
//經過entrySet()方法將map集合中的映射關係取出(這個關係就是Map.Entry類型)
Set<Map.Entry<String, String>> entrySet = map.entrySet();
//將關係集合entrySet進行迭代,存放到迭代器中
Iterator<Map.Entry<String, String>> it = entrySet.iterator();
while(it.hasNext()) {
Map.Entry<String, String> me = it.next();//獲取Map.Entry關係對象me
String key = me.getKey();//經過關係對象獲取key
String value = me.getValue();//經過關係對象獲取value
}複製代碼
經過以上3種遍歷方式咱們能夠知道,若是你只想獲取key,建議使用keySet。若是隻想獲取value,建議使用values。若是key value但願遍歷,建議使用entrySet。(雖然經過keySet能夠得到key再間接得到value,可是效率沒entrySet高,不建議使用這種方法)
在Collection這個集成鏈中,咱們介紹List、Set和Queue。其中會重點介紹List和Set以及幾個經常使用實現class。Queue平時實在沒用過。
先簡單概述下List和Set。他們2個是繼承Collection的子接口,就是說他們也都是負責存儲單個元素的容器。可是最大的區別以下
Java文檔中介紹
An ordered collection (also known as a sequence). The user of this interface has precise control over where in the list each element is inserted. The user can access elements by their integer index (position in the list), and search for elements in the list.
Unlike sets, lists typically allow duplicate elements. More formally, lists typically allow pairs of elements e1 and e2 such that e1.equals(e2), and they typically allow multiple null elements if they allow null elements at all. It is not inconceivable that someone might wish to implement a list that prohibits duplicates, by throwing runtime exceptions when the user attempts to insert them, but we expect this usage to be rare.
...
The List interface provides a special iterator, called a ListIterator, that allows element insertion and replacement, and bidirectional access in addition to the normal operations that the Iterator interface provides. A method is provided to obtain a list iterator that starts at a specified position in the list.
大概意思是
一個有序的Collection(或者叫作序列)。使用這個接口能夠精確掌控元素的插入,還能夠根據index獲取相應位置的元素。
不像Set,list容許重複元素的插入。有人但願本身實現一個list,禁止重複元素,而且在重複元素插入的時候拋出異常,可是咱們不建議這麼作。
...
List提供了一種特殊的iterator遍歷器,叫作ListIterator。這種遍歷器容許遍歷時插入,替換,刪除,雙向訪問。 而且還有一個重載方法容許從一個指定位置開始遍歷。
而後咱們再看下List接口新增的接口,會發現add,get這些都多了index參數,說明在原來Collection的基礎上,List是一個能夠指定索引,有序的容器。在這注意如下添加的2個新Iteractor方法。
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);複製代碼
咱們再看ListIterator的代碼
public interface ListIterator<E> extends Iterator<E> {
// Query Operations
boolean hasNext();
E next();
boolean hasPrevious();
E previous();
int previousIndex();
void remove();
void set(E e);
void add(E e);
}複製代碼
一個集合在遍歷過程當中進行插入刪除操做很容易形成錯誤,特別是無序隊列,是沒法在遍歷過程當中進行這些操做的。可是List是一個有序集合,因此在這實現了一個ListIteractor,能夠在遍歷過程當中進行元素操做,而且能夠雙向訪問。
這個是以前開發中一直沒有發現的,好東西。mark
以上就是List的基本概念和規則,下面咱們介紹2個經常使用List的實現類,ArrayList和LinkedList。
就Java文檔的解釋,整理出如下幾點特色:
而後咱們來簡單看下ArrayList源碼實現。這裏只寫部分源碼分析。
全部元素都是保存在一個Object數組中,而後經過size控制長度。
transient Object[] elementData;
private int size;複製代碼
這時候看下add的代碼分析
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
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;
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);
}複製代碼
其實在每次add的時候會判斷數據長度,若是不夠的話會調用Arrays.copyOf,複製一份更長的數組,並把前面的數據放進去。
咱們再看下remove的代碼是如何實現的。
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把須要刪除index後面的都往前移一位而後再把最後一個去掉。
PS:終於發現之前學習的數據結構用到用場了。O。O
LinkedList是一個鏈表維護的序列容器。和ArrayList都是序列容器,一個使用數組存儲,一個使用鏈表存儲。
數組和鏈表2種數據結構的對比:
如上LinkedList和ArrayList的區別也就在此。根據使用場景選擇更加適合的List。
下面簡單展現LinkedList的部分源碼解析。
首先是鏈表的節點的定義,很是簡單的一個雙向鏈表。
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;
}
}複製代碼
而後每一個LinkedList中會持有鏈表的頭指針和尾指針
transient int size = 0;
transient Node<E> first;
transient Node<E> last;複製代碼
列舉最基本的插入和刪除的鏈表操做
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
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++;
}
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++;
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
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
next.prev = null;
size--;
modCount++;
return element;
}
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 = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
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;
}複製代碼
上面6個方法就是鏈表的核心,頭尾中間插入,頭尾中間刪除。其餘對外的調用都是圍繞這幾個方法進行操做的
同時LinkedList還實現了Deque接口,Deque接口是繼承Queue的。因此LinkedList還支持隊列的pop,push,peek操做。
List實現 | 使用場景 | 數據結構 |
---|---|---|
ArrayList | 數組形式訪問List鏈式集合數據,元素可重複,訪問元素較快 | 數組 |
LinkedList | 鏈表方式的List鏈式集合,元素可重複,元素的插入刪除較快 | 雙向鏈表 |
Set的核心概念就是集合內全部元素不重複。在Set這個子接口中沒有在Collection特別實現什麼額外的方法,應該只是定義了一個Set概念。下面咱們來看Set的幾個經常使用的實現HashSet、LinkedHashSet、TreeSet
HashSet的核心概念。Java文檔中描述
This class implements the Set interface, backed by a hash table (actually a HashMap instance). It makes no guarantees as to the iteration order of the set; in particular, it does not guarantee that the order will remain constant over time. This class permits the null element.
大概意思是
HashSet實現了Set接口,基於HashMap進行存儲。遍歷時不保證順序,而且不保證下次遍歷的順序和以前同樣。HashSet中容許null元素。
進入到HashSet源碼中咱們發現,全部數據存儲在
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();複製代碼
意思就是HashSet的集合其實就是HashMap的key的集合,而後HashMap的val默認都是PRESENT。HashMap的定義便是key不重複的集合。使用HashMap實現,這樣HashSet就不須要再實現一遍。
因此全部的add,remove等操做其實都是HashMap的add、remove操做。遍歷操做其實就是HashMap的keySet的遍歷,舉例以下
...
public Iterator<E> iterator() {
return map.keySet().iterator();
}
public boolean contains(Object o) {
return map.containsKey(o);
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public void clear() {
map.clear();
}
...複製代碼
LinkedHashSet的核心概念相對於HashSet來講就是一個能夠保持順序的Set集合。HashSet是無序的,LinkedHashSet會根據add,remove這些操做的順序在遍歷時返回固定的集合順序。這個順序不是元素的大小順序,而是能夠保證2次遍歷的順序是同樣的。
相似HashSet基於HashMap的源碼實現,LinkedHashSet的數據結構是基於LinkedHashMap。過多的就不說了。
TreeSet便是一組有次序的集合,若是沒有指定排序規則Comparator,則會按照天然排序。(天然排序即e1.compareTo(e2) == 0做爲比較)
注意:TreeSet內的元素必須實現Comparable接口。
TreeSet源碼的算法即基於TreeMap,具體算法在說明TreeMap的時候進行解釋。
Set實現 | 使用場景 | 數據結構 |
---|---|---|
HashSet | 無序的、無重複的數據集合 | 基於HashMap |
LinkedSet | 維護次序的HashSet | 基於LinkedHashMap |
TreeSet | 保持元素大小次序的集合,元素須要實現Comparable接口 | 基於TreeMap |
HashMap就是最基礎最經常使用的一種Map,它無序,以散列表的方式進行存儲。以前提到過,HashSet就是基於HashMap,只使用了HashMap的key做爲單個元素存儲。
HashMap的訪問方式就是繼承於Map的最基礎的3種方式,詳細見上。在這裏我具體分析一下HashMap的底層數據結構的實現。
在看HashMap源碼前,先理解一下他的存儲方式-散列表(哈希表)。像以前提到過的用數組存儲,用鏈表存儲。哈希表是使用數組和鏈表的組合的方式進行存儲。(具體哈希表的概念自行搜索)以下圖就是HashMap採用的存儲方法。
hash獲得數值,放到數組中,若是遇到衝突則以鏈表方式掛在下方。
HashMap的存儲定義是
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}複製代碼
數組table存放元素,若是遇到衝突下掛到衝突元素的next鏈表上。
在這咱們能夠看下get核心方法和put核心方法的源碼
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}複製代碼
上面代碼中看出先根據hash值和數組長度做且運算得出下標索引。若是存在判斷hash值是否徹底一致,若是不徹底一致則next鏈表向下找一致的hash值。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}複製代碼
上面是put的核心源碼,即查找hash值所在索引是否有元素,沒有的話new一個Node直接放在table中。若是已經有Node了,就遍歷該Node的next,將新元素放到最後。
HashMap的遍歷,是從數組遍歷第一個非空的元素,而後再根據這個元素訪問其next下的全部Node。由於第一個元素不是必定從數組的0開始,因此HashMap是無序遍歷。
LinkedHashMap相對於HashMap來講區別是,LinkedHashMap遍歷的時候具備順序,能夠保存插入的順序,(還能夠設置最近訪問的元素也放在前面,即LRU)
其實LinkedHashMap的存儲仍是跟HashMap同樣,採用哈希表方法存儲,只不過LinkedHashMap多維護了一份head,tail鏈表。
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;複製代碼
即在建立新Node的時候將新Node放到最後,這樣遍歷的時候再也不像HashMap同樣,從數組開始判斷第一個非空元素,而是直接從表頭進行遍歷。這樣即知足有序遍歷。
TreeMap平時用的很少,TreeMap會實現SortMap接口,定義一個排序規則,這樣當遍歷TreeMap的時候,會根據規定的排序規則返回元素。
WeakHashMap,此種Map的特色是,當除了自身有對key的引用外,此key沒有其餘引用那麼此map會自動丟棄此值,
舉例:聲明瞭兩個Map對象,一個是HashMap,一個是WeakHashMap,同時向兩個map中放入a、b兩個對象,當HashMap remove掉a 而且將a、b都指向null時,WeakHashMap中的a將自動被回收掉。出現這個情況的緣由是,對於a對象而言,當HashMap remove掉而且將a指向null後,除了WeakHashMap中還保存a外已經沒有指向a的指針了,因此WeakHashMap會自動捨棄掉a,而對於b對象雖然指向了null,但HashMap中還有指向b的指針,因此
WeakHashMap將會保留。
WeakHashMap用的也很少,在這簡單說起。
Map實現 | 使用場景 | 數據結構 |
---|---|---|
HashMap | 哈希表存儲鍵值對,key不重複,無序 | 哈希散列表 |
LinkedHashMap | 是一個能夠記錄插入順序和訪問順序的HashMap | 存儲方式是哈希散列表,可是維護了頭尾指針用來記錄順序 |
TreeMap | 具備元素排序功能 | 紅黑樹 |
WeakHashMap | 弱鍵映射,映射以外無引用的鍵,能夠被垃圾回收 | 哈希散列表 |
以上就是對於Java集合的完整分析和源碼解析。其中ArrayList、HashMap使用較多,當考慮到效率時記得有Linded系列集合和WeakHashMap。Over~~
更多文章關注個人公衆號