JAVA集合框架探究(一)
集合框架是日常開發中使用最多的,但是我對它還一知半解。在具體應該選擇哪個容器使用時往往不能
確定,因爲對它的實現細節不夠了解。所以準備通過查看文檔和源碼的方式對每個集合框架加深理解。
首先會從總體框架上進行梳理,然後再具體到每個集合類進行分析。
文章目錄
一、概述
一般存放一系列相同的已知對象時,我們會使用數組,但開發過程中,大部分情況下對象的數量都無法確定。所以無法使用數組的形式去存儲,這時就需要使用集合的形式存儲對象,集合總體來說就是存儲一系列相同對象的容器,根據不同的特性(如順序、是否重複等)又分爲不同的類型。
集合和數組的區別
- 數組長度是固定的,需要在初始化時指定。集合不需要指定長度。
- 數組可以保存對象和基本數組類型,集合只能保存對象的引用,基本數據類型需要經過裝箱後才能存儲。
JAVA的集合類根據用途,大致可以分爲兩種。一種是保存元素的序列,並且提供了序列中增刪元素的方法,另一種是保存鍵值對對象,像字典的形式一樣可以通過一個對象來查找映射表中的另一個對象。這兩種類型也分別是由兩個接口派生,Collecion和Map。相關類圖比較複雜,爲了方便理解,一個接口一個接口來分析。
1、Collection
先看一張Collection接口。
可以看到Collection接口裏面的方法:
Collection接口是最基本的集合接口。我們平時使用的ArrayList、HashSet等集合類分別實現了List、Set、Queue接口,而Collection是這些接口的抽象,包含了它們的通用方法。
對Collection的遍歷有三種方式,第一種是foreach,第二種因爲Collection繼承了迭代接口Iterable,所以可以使用迭代器進行遍歷。第三種是JDK8的新特性,對集合的流式操作(Stream),這是對集合功能的增強,寫法使用lambda表達式,可以以非常簡單的語法對集合進行各種聚合和批量處理操作,感興趣的話可以瞭解一下。
Collection接口沒有實現類,只有派生的三個接口:Set、Queue、List。
- List:List顧名思義,可以存放有序的元素,它可以通過索引去查找某個元素。
- Set:Set與List的區別是其中的元素是不可重複的,也是無序的。
2、List
List相關類圖。
List接口除了繼承的Collection接口裏的方法,又添加了一些方法,主要可以分爲以下幾類。
-
搜索相關方法。
// Search Operations int indexOf(Object o); int lastIndexOf(Object o);
在集合中查找指定元素的位置。
-
位置相關方法。
// Positional Access Operations E get(int index); E set(int index, E element); void add(int index, E element); E remove(int index);
如get、set、add、remove方法,參數都是index,因爲list是有序集合,可以像數組一樣根據位置進行元素操作。
-
ListIterator。List迭代器。
// List Iterators ListIterator<E> listIterator();
List迭代器比Iterator增加了添加元素、向前遍歷、定位索引、修改元素等方法,在List子類中有相關實現。
-
範圍操作。
List<E> subList(int fromIndex, int toIndex);
3、Set
Set相關類圖。
Set接口方法沒有對Collection接口進行擴展,在此不再贅述。
4、Queue
Queue相關類圖。
Queue是隊列,是一種先入先出的結構(FIFO),在數據結構學習中經常使用。
Queue接口除了繼承Collection的方法,還定義了隊列的常規操作方法。如下:
offer(E e); //入隊操作
E remove(); //移除隊頭元素
E poll(); //移除隊頭元素
E element(); //獲取隊頭元素
E peek(); //獲取隊頭的元素
可以看到remove/poll、element/peek方法作用是相同的,那有什麼區別呢?在註釋裏寫了這點,This method differs from {@link #poll poll} only in that it throws an exception if this queue is empty.當隊列爲空時remove會拋出異常,poll會返回空。element/peek的區別也是這樣。element()方法會拋出異常。
5、Map
Map相關類圖。
Map接口中除了我們經常使用的put、get、remove等方法,有幾個方法比較有意思。
// Views
Collection<V> values();
其中,values()返回所有的value,使用了Collection,所以我們在使用values()方法時返回的是個Collection。值是可重複的。
Set<K> keySet();
keySet()返回值是所有的key,這樣可以遍歷Set獲取key,再通過get獲取所有的value。因爲使用Set存儲,所以key是不可重複的。
Set<Map.Entry<K, V>> entrySet();
entrySet()在遍歷Map時經常使用,它返回的是包含映射關係的Set。其中Entry是Map接口中的一個內部接口,有如下方法:
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
entry是存放在Set中,所以entry是不可重複的。
另外,還有HashTable繼承於Dictionary類,也實現了Map接口,但Dictionary類已經標記爲廢棄了,所以不再研究。
二、AbstractCollection源碼解析
從上面List和Set的類圖中可以看出,所有實現類幾乎都繼承自AbstractCollection這個抽象類,AbstractCollection是Collection唯一的直接實現類,實現了Collection接口裏的大部分方法。在此列舉幾個比較重要的方法。
1. contains(Object o)
public boolean contains(Object o) {
Iterator<E> it = iterator();
if (o==null) {
while (it.hasNext())
if (it.next()==null)
return true;
} else {
while (it.hasNext())
if (o.equals(it.next()))
return true;
}
return false;
}
可以看到遍歷方式是使用迭代器,並且在元素爲空時也可以查找。
2. toArray()
public Object[] toArray() {
// Estimate size of array; be prepared to see more or fewer elements
Object[] r = new Object[size()];
Iterator<E> it = iterator();
for (int i = 0; i < r.length; i++) {
if (! it.hasNext()) // fewer elements than expected
return Arrays.copyOf(r, i);
r[i] = it.next();
}
return it.hasNext() ? finishToArray(r, it) : r;
}
可以看到需要先調用size()方法創建一個數量和集合數量相同的數組,然後遍歷將集合元素引用複製到數組,如果集合中元素比數組大小少,則調用Arrays.copyOf()方法來截取數組並返回新數組,如果集合中元素比數組大小多,則會調用finishToArray調整數組大小並返回新數組。這裏對數組大小和集合數量進行比較是爲了考慮在toArray()期間修改了原集合元素的情況。
private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
int i = r.length;
while (it.hasNext()) {
int cap = r.length;
if (i == cap) {
int newCap = cap + (cap >> 1) + 1;
// overflow-conscious code
if (newCap - MAX_ARRAY_SIZE > 0)
newCap = hugeCapacity(cap + 1);
r = Arrays.copyOf(r, newCap);
}
r[i++] = (T)it.next();
}
// trim if overallocated
return (i == r.length) ? r : Arrays.copyOf(r, i);
}
此方法的核心是對數組擴容,當數組元素滿了時使用Arrays.copyOf方法對數組擴容。擴容的大小爲newCap = cap + (cap >> 1) + 1。使用移位運算符,相當於增加了原大小的一半再加一,並且如果調整後帶下超過MAX_ARRAY_SIZE數組最大值時,要使用hugeCapacity()方法調整大小。
數組最大值爲:
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
數組最大值是int最大值-8的原因在註釋裏寫明瞭,有些虛擬機會在數組中保留一些頭關鍵字,爲了防止內存溢出所以小一些。
再看一下hugeCapacity()方法
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError
("Required array size too large");
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
遍歷完成之後再使用Arrays.copyOf截取數組。
3. toArray(T[] a)
public <T> T[] toArray(T[] a) {
// Estimate size of array; be prepared to see more or fewer elements
int size = size();
T[] r = a.length >= size ? a :
(T[])java.lang.reflect.Array
.newInstance(a.getClass().getComponentType(), size);
Iterator<E> it = iterator();
for (int i = 0; i < r.length; i++) {
if (! it.hasNext()) { // fewer elements than expected
if (a == r) {
r[i] = null; // null-terminate
} else if (a.length < i) {
return Arrays.copyOf(r, i);
} else {
System.arraycopy(r, 0, a, 0, i);
if (a.length > i) {
a[i] = null;
}
}
return a;
}
r[i] = (T)it.next();
}
// more elements than expected
return it.hasNext() ? finishToArray(r, it) : r;
}
這個方法和無參toArray()方法的區別是有一個數組作爲參數,作用是將集合中的元素填入數組中。
- 首先比較參數數組a和集合大小,使用其中較大地值來構造數組r(a長度較大時直接使用a)。
- 使用迭代器進行遍歷,將元素轉爲數組類型存入數組。
- 在集合元素和數組長度不一致時,可以看到有兩種情況(一和三是一種結果)。如果數組長度較大,則剩餘位置都設置null。如果集合長度大於數組,則Arrays.copyOf進行截取。
- 最後如果數組已滿,則和上面的方法一樣,通過finishToArray()進行擴容。
4.add()
public boolean add(E e) {
throw new UnsupportedOperationException();
}
add方法會拋出異常。
5.抽象方法
public abstract Iterator<E> iterator();
public abstract int size();
二、AbstractList源碼解析
AbstractList實現了List接口,繼承自AbstractCollection,是所有List集合類的父類,我們來看一下它有哪些方法。
1、沒有實現的方法
public boolean add(E e) {
add(size(), e);
return true;
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
abstract public E get(int index);
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
這幾個方法都是直接拋出異常。
2、indexOf()
public int indexOf(Object o) {
ListIterator<E> it = listIterator();
if (o==null) {
while (it.hasNext())
if (it.next()==null)
return it.previousIndex();
} else {
while (it.hasNext())
if (o.equals(it.next()))
return it.previousIndex();
}
return -1;
}
public int lastIndexOf(Object o) {
ListIterator<E> it = listIterator(size());
if (o==null) {
while (it.hasPrevious())
if (it.previous()==null)
return it.nextIndex();
} else {
while (it.hasPrevious())
if (o.equals(it.previous()))
return it.nextIndex();
}
return -1;
}
indexOf和lastIndexOf,都使用了listIterator進行遍歷,並且可以查詢Null的元素。
因爲listIterator是雙向迭代器,所以lastIndexOf可以從後向前進行遍歷。
3、addAll()
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
boolean modified = false;
for (E e : c) {
add(index++, e);
modified = true;
}
return modified;
}
private void rangeCheckForAdd(int index) {
if (index < 0 || index > size())
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
addAll方法首先檢查index是否越界,然後再挨個添加元素。
4. iterator()
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
/**
* Index of element to be returned by subsequent call to next.
*/
int cursor = 0;
/**
* Index of element returned by most recent call to next or
* previous. Reset to -1 if this element is deleted by a call
* to remove.
*/
int lastRet = -1;
/**
* The modCount value that the iterator believes that the backing
* List should have. If this expectation is violated, the iterator
* has detected concurrent modification.
*/
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size();
}
public E next() {
checkForComodification();
try {
int i = cursor;//當前標記位
E next = get(i);//通過列表的get函數獲得元素
lastRet = i;//記錄老的標記位
cursor = i + 1;//標記位+1
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
iterator()是AbstractCollection中沒有實現的抽象方法,在AbstractList中實現了該方法,Itr是單向迭代器,繼承了Iterator接口。其中
- cursor指迭代器當前位置。
- lastRet指迭代器上次位置。
- expectedModCount指迭代期間集合被修改的次數。
- next()方法中首先通過get方法獲取值,然後cursor標誌加一,以此進行迭代。
- 發生IndexOutOfBoundsException異常時,首先會通過checkForComodification()檢查迭代期間集合是否發生了變化,如果是則會先拋出併發修改異常。
- remove()方法中會先判斷lastRet是否小於0,所以remove只能刪除已經迭代過的元素。
5.listIterator()
public ListIterator<E> listIterator() {
return listIterator(0);
}
public ListIterator<E> listIterator(final int index) {
rangeCheckForAdd(index);
return new ListItr(index);
}
private class ListItr extends Itr implements ListIterator<E> {
// 通過索引初始化迭代器
ListItr(int index) {
cursor = index;
}
// 是否有前一個元素
public boolean hasPrevious() {
return cursor != 0;
}
// 返回前一個元素
public E previous() {
checkForComodification();
try {
int i = cursor - 1;
E previous = get(i);
lastRet = cursor = i;
return previous;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.set(lastRet, e);
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
AbstractList.this.add(i, e);
lastRet = -1;
cursor = i + 1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
listIterator是Iterator的功能增強版,從方法上就可以看出。listIterator除了無參還有一個有參方法,以index爲參數,提供的是通過索引位置初始化的迭代器。
- 以索引位置初始化迭代器的方式是把cursor初始爲index.
- 判斷有沒有前一個元素的方式是判斷cursor是否爲0。
- add新元素後會使lastRet失效。
6.subList()
subList方法是返回原列表的一部分,也就是list的子列表.
public List<E> subList(int fromIndex, int toIndex) {
return (this instanceof RandomAccess ?
new RandomAccessSubList<>(this, fromIndex, toIndex) :
new SubList<>(this, fromIndex, toIndex));
}
代碼中可以看到,如果List實現了RandomAccess接口,則返回RandomAccessSubList,否則返回SubList。這兩個類的源碼都在AbstractList類中,後面會進行分析。其中會說明爲什麼sublist方法只是返回原list的一部分的映射。