本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html
![]()
從本節開始,咱們探討Java中的容器類,所謂容器,顧名思義就是容納其餘數據的,計算機課程中有一門課叫數據結構,能夠粗略對應於Java中的容器類,咱們不會介紹全部數據結構的內容,但會介紹Java中的主要實現,並分析其基本原理和主要實現代碼。java
前幾節在介紹泛型的時候,咱們本身實現了一個簡單的動態數組容器類DynaArray,本節,咱們介紹Java中真正的動態數組容器類ArrayList。程序員
咱們先來看它的基本用法。算法
ArrayList是一個泛型容器,新建ArrayList須要實例化泛型參數,好比:編程
ArrayList<Integer> intList = new ArrayList<Integer>();
ArrayList<String> strList = new ArrayList<String>();
複製代碼
add方法添加元素到末尾設計模式
ArrayList<Integer> intList = new ArrayList<Integer>();
intList.add(123);
intList.add(456);
ArrayList<String> strList = new ArrayList<String>();
strList.add("老馬");
strList.add("編程");
複製代碼
判斷是否爲空數組
public boolean isEmpty() 複製代碼
獲取長度微信
public int size() 複製代碼
public E get(int index) 複製代碼
如:數據結構
ArrayList<String> strList = new ArrayList<String>();
strList.add("老馬");
strList.add("編程");
for(int i=0; i<strList.size(); i++){
System.out.println(strList.get(i));
}
複製代碼
public int indexOf(Object o) 複製代碼
若是找到,返回索引位置,不然返回-1。併發
從後往前找
public int lastIndexOf(Object o) 複製代碼
是否包含指定元素
public boolean contains(Object o) 複製代碼
相同的依據是equals方法返回true。若是傳入的元素爲null,則找null的元素。
刪除指定位置的元素
public E remove(int index) 複製代碼
返回值爲被刪對象。
刪除指定對象
public boolean remove(Object o) 複製代碼
與indexOf同樣,比較的依據的是equals方法,若是o爲null,則刪除值爲null的元素。另外,remove只刪除第一個相同的對象,也就是說,即便ArrayList中有多個與o相同的元素,也只會刪除第一個。返回值爲boolean類型,表示是否刪除了元素。
刪除全部元素
public void clear() 複製代碼
在指定位置插入元素
public void add(int index, E element) 複製代碼
index爲0表示插入最前面,index爲ArrayList的長度表示插到最後面。
修改指定位置的元素內容
public E set(int index, E element) 複製代碼
能夠看出,ArrayList的基本用法是比較簡單的,它的基本原理也是比較簡單的,原理與咱們在前面幾節介紹的DynaArray相似,內部有一個數組elementData,通常會有一些預留的空間,有一個整數size記錄實際的元素個數,以下所示:
private transient Object[] elementData;
private int size;
複製代碼
咱們暫時能夠忽略transient這個關鍵字。各類public方法內部操做的基本都是這個數組和這個整數,elementData會隨着實際元素個數的增多而從新分配,而size則始終記錄實際的元素個數。
雖然基本思路是簡單的,但內部代碼有一些比較晦澀,咱們來看下add方法的代碼:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
複製代碼
它首先調用ensureCapacityInternal確保數組容量是夠的,ensureCapacityInternal的代碼是:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
複製代碼
它先判斷數組是否是空的,若是是空的,則首次至少要分配的大小爲DEFAULT_CAPACITY
,DEFAULT_CAPACITY
的值爲10,接下來調用ensureExplicitCapacity,代碼爲:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
複製代碼
modCount++是什麼意思呢?modCount表示內部的修改次數,modCount++固然就是增長修改次數,爲何要記錄修改次數呢?咱們待會解釋。
若是須要的長度大於當前數組的長度,則調用grow方法。這段代碼前面有個註釋:overflow-conscious code,翻譯一下,大意就是代碼考慮了溢出這種狀況,溢出是什麼意思呢?咱們解釋下,假設a,b都是int,下面兩行代碼是不同的:
1 if(a>b)
2 if(a-b>0)
複製代碼
爲何呢?考慮a=Integer.MAX_VALUE, b=Integer.MIN_VALUE
:
a>b爲true
但因爲溢出,a-b的結果爲-1
反之,再考慮a=Integer.MIN_VALUE, b=Integer.MAX_VALUE
:
a>b爲false
但因爲溢出,a-b的結果爲1。
不過,在a, b都爲正數且數值沒有那麼大的狀況下,通常也沒有溢出問題,爲便於理解,在後續的分析中,咱們將忽略溢出問題。
接下來,看grow方法:
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);
}
複製代碼
排除邊緣狀況,長度增加的主要代碼爲:
int newCapacity = oldCapacity + (oldCapacity >> 1);
複製代碼
右移一位至關於除2,因此,newCapacity至關於oldCapacity的1.5倍。
咱們再來看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;
}
複製代碼
它也增長了modCount,而後計算要移動的元素個數,從index日後的元素都往前移動一位,實際調用System.arraycopy方法移動元素。elementData[--size] = null;
這行代碼將size減一,同時將最後一個位置設爲null,設爲null後就再也不引用原來對象,若是原來對象也再也不被其餘對象引用,就能夠被垃圾回收。
其餘方法大可能是比較簡單的,咱們就不贅述了。整體而言,內部操做要考慮各類狀況,代碼有一些晦澀複雜,但接口通常都是簡單直接的,這就是使用容器類的好處了,這也是計算機程序中的基本思惟方式,封裝複雜操做,提供簡單接口。
理解了ArrayList的基本用法和原理,接下來,咱們來看一個常見的操做 - 迭代,好比說,循環打印ArrayList中的每一個元素,ArrayList支持foreach語法,好比:
ArrayList<Integer> intList = new ArrayList<Integer>();
intList.add(123);
intList.add(456);
intList.add(789);
for(Integer a : intList){
System.out.println(a);
}
複製代碼
固然,這種循環也可使用以下代碼實現:
for(int i=0; i<intList.size(); i++){
System.out.println(intList.get(i));
}
複製代碼
不過,foreach看上去更爲簡潔,並且,它適用於各類容器,更爲通用。
這種foreach語法背後是怎麼實現的呢?其實,編譯器會將它轉換爲相似以下代碼:
Iterator<Integer> it = intList.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
複製代碼
接來下,咱們解釋一下其中的代碼。
ArrayList實現了Iterable接口,Iterable表示可迭代,它的定義爲:
public interface Iterable<T> {
Iterator<T> iterator();
}
複製代碼
定義很簡單,就是要求實現iterator方法。iterator方法的聲明爲:
public Iterator<E> iterator() 複製代碼
它返回一個實現了Iterator接口的對象,Iterator接口的定義爲:
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
複製代碼
hasNext()判斷是否還有元素未訪問,next()返回下一個元素,remove()刪除最後返回的元素,只讀訪問的基本模式就相似於:
Iterator<Integer> it = intList.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
複製代碼
咱們待會再看迭代中間要刪除元素的狀況。
只要對象實現了Iterable接口,就可使用foreach語法,編譯器會轉換爲調用Iterable和Iterator接口的方法。
初次見到Iterable和Iterator,可能會比較容易混淆,咱們再澄清一下:
除了iterator(),ArrayList還提供了兩個返回Iterator接口的方法:
public ListIterator<E> listIterator() public ListIterator<E> listIterator(int index) 複製代碼
ListIterator擴展了Iterator接口,增長了一些方法,向前遍歷、添加元素、修改元素、返回索引位置等,添加的方法有:
public interface ListIterator<E> extends Iterator<E> {
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
void set(E e);
void add(E e);
}
複製代碼
listIterator()方法返回的迭代器從0開始,而listIterator(int index)
方法返回的迭代器從指定位置index開始,好比,從末尾往前遍歷,代碼爲:
public void reverseTraverse(List<Integer> list){
ListIterator<Integer> it = list.listIterator(list.size());
while(it.hasPrevious()){
System.out.println(it.previous());
}
}
複製代碼
關於迭代器,有一種常見的誤用,就是在迭代的中間調用容器的刪除方法,好比要刪除一個整數ArrayList中全部小於100的數,直覺上,代碼能夠這麼寫:
public void remove(ArrayList<Integer> list){
for(Integer a : list){
if(a<=100){
list.remove(a);
}
}
}
複製代碼
但,運行時會拋出異常:
java.util.ConcurrentModificationException
複製代碼
發生了併發修改異常,爲何呢?迭代器內部會維護一些索引位置相關的數據,要求在迭代過程當中,容器不能發生結構性變化,不然這些索引位置就失效了。所謂結構性變化就是添加、插入和刪除元素,只是修改元素內容不算結構性變化。
如何避免異常呢?可使用迭代器的remove方法,以下所示:
public static void remove(ArrayList<Integer> list){
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
if(it.next()<=100){
it.remove();
}
}
}
複製代碼
迭代器如何知道發生告終構性變化,並拋出異常?它本身的remove方法爲什麼又可使用呢?咱們須要看下迭代器的工做原理。
咱們來看下ArrayList中iterator方法的實現,代碼爲:
public Iterator<E> iterator() {
return new Itr();
}
複製代碼
新建了一個Itr對象,Itr是一個成員內部類,實現了Iterator接口,聲明爲:
private class Itr implements Iterator<E> 複製代碼
它有三個實例成員變量,爲:
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
複製代碼
cursor表示下一個要返回的元素位置,lastRet表示最後一個返回的索引位置,expectedModCount表示指望的修改次數,初始化爲外部類當前的修改次數modCount,回顧一下,成員內部類能夠直接訪問外部類的實例變量。
每次發生結構性變化的時候modCount都會增長,而每次迭代器操做的時候都會檢查expectedModCount是否與modCount相同,這樣就能檢測出結構性變化。
咱們來具體看下,它是如何實現Iterator接口中的每一個方法的,先看hasNext(),代碼爲:
public boolean hasNext() {
return cursor != size;
}
複製代碼
cursor與size比較,比較直接,看next()方法:
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
複製代碼
首先調用了checkForComodification,它的代碼爲:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
複製代碼
因此,next()前面部分主要就是在檢查是否發生告終構性變化,若是沒有變化,就更新cursor和lastRet的值,以保持其語義,而後返回對應的元素。
remove的代碼爲:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
複製代碼
它調用了ArrayList的remove方法,但同時更新了cursor, lastRet和expectedModCount的值,因此它能夠正確刪除。
不過,須要注意的是,調用remove方法前必須先調用next,好比,經過迭代器刪除全部元素,直覺上,能夠這麼寫:
public static void removeAll(ArrayList<Integer> list){
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
it.remove();
}
}
複製代碼
實際運行,會拋出異常:
java.lang.IllegalStateException
複製代碼
正確寫法是:
public static void removeAll(ArrayList<Integer> list){
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
it.next();
it.remove();
}
}
複製代碼
固然,若是隻是要刪除全部元素,ArrayList有現成的方法clear()。
listIterator()的實現使用了另外一個內部類ListItr,它繼承自Itr,基本思路相似,咱們就不贅述了。
爲何要經過迭代器這種方式訪問元素呢?直接使用size()/get(index)語法不也能夠嗎?在一些場景下,確實沒有什麼差異,二者均可以。不過,foreach語法更爲簡潔一些,更重要的是,迭代器語法更爲通用,它適用於各類容器類。
此外,迭代器表示的是一種關注點分離的思想,將數據的實際組織方式與數據的迭代遍歷相分離,是一種常見的設計模式。須要訪問容器元素的代碼只須要一個Iterator接口的引用,不須要關注數據的實際組織方式,可使用一致和統一的方式進行訪問。
而提供Iterator接口的代碼瞭解數據的組織方式,能夠提供高效的實現。在ArrayList中, size/get(index)語法與迭代器性能是差很少的,但在後續介紹的其餘容器中,則不必定,好比LinkedList,迭代器性能就要高不少。
從封裝的思路上講,迭代器封裝了各類數據組織方式的迭代操做,提供了簡單和一致的接口。
Java的各類容器類有一些共性的操做,這些共性以接口的方式體現,咱們剛剛介紹的Iterable接口就是,此外,ArrayList還實現了三個主要的接口Collection, List和RandomAccess,咱們逐個來看下。
Collection表示一個數據集合,數據間沒有位置或順序的概念,接口定義爲:
public interface Collection<E> extends Iterable<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
}
複製代碼
這些方法中,除了兩個toArray方法和幾個xxxAll()方法外,其餘咱們已經介紹過了。
這幾個xxxAll()方法的含義基本也是能夠顧名思義的,addAll添加,removeAll刪除,containsAll檢查是否包含了參數容器中的全部元素,只有全包含才返回true,retainAll只保留參數容器中的元素,其餘元素會進行刪除。
有一個抽象類AbstractCollection對這幾個方法都提供了默認實現,實現的方式就是利用迭代器方法逐個操做,好比說,咱們看removeAll方法,代碼爲:
public boolean removeAll(Collection<?> c) {
boolean modified = false;
Iterator<?> it = iterator();
while (it.hasNext()) {
if (c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
複製代碼
代碼比較簡單,就不解釋了。ArrayList繼承了AbstractList,而AbstractList又繼承了AbstractCollection,ArrayList對其中一些方法進行了重寫,以提供更爲高效的實現,具體咱們就不介紹了。
關於toArray方法,咱們待會再介紹。
List表示有順序或位置的數據集合,它擴展了Collection,增長的主要方法有:
boolean addAll(int index, Collection<? extends E> c);
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
List<E> subList(int fromIndex, int toIndex);
複製代碼
這些方法都與位置有關,容易理解,就不介紹了。
RandomAccess的定義爲:
public interface RandomAccess {
}
複製代碼
沒有定義任何代碼。這有什麼用呢?這種沒有任何代碼的接口在Java中被稱之爲標記接口,用於聲明類的一種屬性。
這裏,實現了RandomAccess接口的類表示能夠隨機訪問,可隨機訪問就是具有相似數組那樣的特性,數據在內存是連續存放的,根據索引值就能夠直接定位到具體的元素,訪問效率很高。下節咱們會介紹LinkedList,它就不能隨機訪問。
有沒有聲明RandomAccess有什麼關係呢?主要用於一些通用的算法代碼中,它能夠根據這個聲明而選擇效率更高的實現。好比說,Collections類中有一個方法binarySearch,在List中進行二分查找,它的實現代碼就根據list是否實現了RandomAccess而採用不一樣的實現機制,以下所示:
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
複製代碼
ArrayList還有兩個構造方法
public ArrayList(int initialCapacity) public ArrayList(Collection<? extends E> c) 複製代碼
第一個方法以指定的大小initialCapacity初始化內部的數組大小,代碼爲:
this.elementData = new Object[initialCapacity];
複製代碼
在事先知道元素長度的狀況下,或者,預先知道長度上限的狀況下,使用這個構造方法能夠避免從新分配和拷貝數組。
第二個構造方法以一個已有的Collection構建,數據會新拷貝一份。
ArrayList中有兩個方法能夠返回數組
public Object[] toArray()
public <T> T[] toArray(T[] a)
複製代碼
第一個方法返回是Object數組,代碼爲:
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
複製代碼
第二個方法返回對應類型的數組,若是參數數組長度足以容納全部元素,就使用該數組,不然就新建一個數組,好比:
ArrayList<Integer> intList = new ArrayList<Integer>();
intList.add(123);
intList.add(456);
intList.add(789);
Integer[] arrA = new Integer[3];
intList.toArray(arrA);
Integer[] arrB = intList.toArray(new Integer[0]);
System.out.println(Arrays.equals(arrA, arrB));
複製代碼
輸出爲true,表示兩種方式都是能夠的。
Arrays中有一個靜態方法asList能夠返回對應的List,以下所示:
Integer[] a = {1,2,3};
List<Integer> list = Arrays.asList(a);
複製代碼
須要注意的是,這個方法返回的List,它的實現類並非本節介紹的ArrayList,而是Arrays類的一個內部類,在這個內部類的實現中,內部用的的數組就是傳入的數組,沒有拷貝,也不會動態改變大小,因此對數組的修改也會反映到List中,對List調用add/remove方法會拋出異常。
要使用ArrayList完整的方法,應該新建一個ArrayList,以下所示:
List<Integer> list = new ArrayList<Integer>(Arrays.asList(a));
複製代碼
ArrayList還提供了兩個public方法,能夠控制內部使用的數組大小,一個是:
public void ensureCapacity(int minCapacity) 複製代碼
它能夠確保數組的大小至少爲minCapacity,若是不夠,會進行擴展。若是已經預知ArrayList須要比較大的容量,調用這個方法能夠減小ArrayList內部分配和擴展的次數。
另外一個方法是:
public void trimToSize() 複製代碼
它會從新分配一個數組,大小恰好爲實際內容的長度。調用這個方法能夠節省數組佔用的空間。
後續咱們會介紹各類容器類和數據組織方式,之因此有各類不一樣的方式,是由於不一樣方式有不一樣特色,而不一樣特色有不一樣適用場合。考慮特色時,性能是其中一個很重要的部分,但性能不是一個簡單的高低之分,對於一種數據結構,有的操做性能高,有的操做性能可能就比較低。
做爲程序員,就是要理解每種數據結構的特色,根據場合的不一樣,選擇不一樣的數據結構。
對於ArrayList,它的特色是:內部採用動態數組實現,這決定了:
本文詳細介紹了ArrayList,ArrayList是平常開發中最經常使用的類之一。咱們介紹了ArrayList的用法、基本實現原理、迭代器及其實現、Collection/List/RandomAccess接口、ArrayList與數組的相互轉換,最後咱們分析了ArrayList的特色。
ArrayList的插入和刪除的性能比較低,下一節,咱們來看另外一個一樣實現了List接口的容器類,LinkedList,它的特色能夠說與ArrayList正好相反。
未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。