Java中ArrayList實現(xiàn)原理及基本方法
1、ArrayList的數(shù)據(jù)結(jié)構(gòu)
ArrayList是開發(fā)中非常常用的數(shù)據(jù)存儲容器之一,其底層是 數(shù)組 實現(xiàn)的
我們可以在集合中存儲任意類型的數(shù)據(jù),ArrayList是線程不安全的,擅長隨機訪問元素,插入和刪除較慢 。
ArrayList的底層數(shù)據(jù)結(jié)構(gòu)就是一個 數(shù)組 ,數(shù)組元素的類型為Object類型,對ArrayList的所有操作底層都是基于數(shù)組的。
2、ArrayList是線程不安全的
對ArrayList進行添加元素的操作的時候是分兩個步驟進行的:
- 先在
object[size]
的位置上存放需要添加的元素; - 將
size
的值增加1。
由于這個過程在多線程的環(huán)境下是不能保證具有原子性 的,因此ArrayList在多線程的環(huán)境下是線程不安全的。
在單線程運行的情況下,如果Size = 0,添加一個元素后,此元素在位置 0,而且Size=1;
而如果是在多線程情況下,比如有兩個線程,線程 A 先將元素存放在位置0。
但是此時 CPU 調(diào)度線程A暫停,線程 B 得到運行的機會。
線程B也向此ArrayList 添加元素,因為此時 Size 仍然等于 0
(注意:我們假設(shè)的是添加一個元素是要兩個步驟哦,而線程A僅僅完成了步驟1),所以線程B也將 元素存放在位置0。
然后線程A和線程B都繼續(xù)運行,都增 加 Size 的值。
那好,現(xiàn)在我們來看看ArrayList 的情況,元素實際上只有一個,存放在位置 0,而Size卻等于 2。這就是“線程不安全”了。
如果非要在多線程的環(huán)境下使用ArrayList,就需要保證它的線程安全性,通常有兩種解決辦法:
- 使用synchronized關(guān)鍵字;
- 可以用Collections類中的靜態(tài)方法synchronizedList(),對ArrayList進行調(diào)用即可。
3、ArrayList的實現(xiàn)
對于ArrayList而言,它實現(xiàn)List接口、底層使用數(shù)組保存所有元素。其操作基本上是對數(shù)組的操作。
下面我們來分析ArrayList的源代碼:
1) 私有屬性:
ArrayList定義只定義類兩個私有屬性:
/** *ArrayList 的元素存儲在其中的數(shù)組緩沖區(qū)。 *ArrayList 的容量就是這個數(shù)組緩沖區(qū)的長度。添加第一個元素時,任何帶有 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList 都將擴展為 DEFAULT_CAPACITY(10)。 */ transient Object[] elementData; // ArrayList 的大小 private int size;
Java的serialization提供了一種持久化對象實例的機制。
當持久化對象時,可能有一個特殊的對象數(shù)據(jù)成員,我們不想用serialization機制來保存它。
為了在一個特定對象的一個域上關(guān)閉serialization,可以在這個域前加上關(guān)鍵字transient。
有點抽象,看個例子應(yīng)該能明白:
public class UserInfo implements Serializable { private static final long serialVersionUID = 996890129747019948L; private String name; private transient String psw; public UserInfo(String name, String psw) { this.name = name; this.psw = psw; } public String toString() { return "name=" + name + ", psw=" + psw; } } public class TestTransient { public static void main(String[] args) { UserInfo userInfo = new UserInfo("張三", "123456"); System.out.println(userInfo); try { // 序列化,被設(shè)置為transient的屬性沒有被序列化 ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("UserInfo.out")); o.writeObject(userInfo); o.close(); } catch (Exception e) { e.printStackTrace(); } try { // 重新讀取內(nèi)容 ObjectInputStream in = new ObjectInputStream(new FileInputStream("UserInfo.out")); UserInfo readUserInfo = (UserInfo) in.readObject(); // 讀取后psw的內(nèi)容為null System.out.println(readUserInfo.toString()); } catch (Exception e) { e.printStackTrace(); } } }
被標記為transient的屬性在對象被序列化的時候不會被保存?;氐紸rrayList的分析中
2) 構(gòu)造方法:
ArrayList提供了三種方式的構(gòu)造器,可以構(gòu)造一個默認初始容量為10的空列表、構(gòu)造一個指定初始容量的空列表以及構(gòu)造一個包含指定collection的元素的列表,這些元素按照該collection的迭代器返回它們的順序排列的。
/** * 構(gòu)造一個具有指定初始容量的空列表。 */ 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); } } /** * 構(gòu)造一個默認初始容量為10的空列表 */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * 構(gòu)造一個包含指定集合元素的列表,按照集合的迭代器返回的順序。 */ public ArrayList(Collection<? extends E> c) { Object[] a = c.toArray(); if ((size = a.length) != 0) { if (c.getClass() == ArrayList.class) { elementData = a; } else { elementData = Arrays.copyOf(a, size, Object[].class); } } else { // 替換為空數(shù)組。 elementData = EMPTY_ELEMENTDATA; } }
3) 元素存儲:
ArrayList提供了set(int index, E element)、add(E e)、add(int index, E element)、addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)這些添加元素的方法。
下面我們一一講解:
// 用指定的元素替代此列表中指定位置上的元素,并返回以前位于該位置上的元素。 public E set(int index, E element) { RangeCheck(index); E oldValue = (E) elementData[index]; elementData[index] = element; return oldValue; } // 將指定的元素添加到此列表的尾部。 public boolean add(E e) { // 增加modCount ensureCapacity(size + 1); elementData[size++] = e; return true; } // 將指定的元素插入此列表中的指定位置。 // 如果當前位置有元素,則向右移動位于該位置的元素及所有后續(xù)元素(其索引加1)。 public void add(int index, E element) { // if (index > size || index < 0) // throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); rangeCheckForAdd(index); // 如果數(shù)組長度不足,將進行擴容。 ensureCapacity(size + 1); // 增加modCount // 將 elementData中從Index位置開始、長度為size-index的元素, // 拷貝到從下標為index+1位置開始的新的elementData數(shù)組中。 // 即將當前位于該位置的元素以及所有后續(xù)元素右移一個位置。 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } // 按照指定collection的迭代器所返回的元素順序,將該collection中的所有元素添加到此列表的尾部。 public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); int numNew = a.length; ensureCapacity(size + numNew); // Increments modCount System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; } // 從指定的位置開始,將指定collection中的所有元素插入到此列表中。 public boolean addAll(int index, Collection<? extends E> c) { if (index > size || index < 0) throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); Object[] a = c.toArray(); int numNew = a.length; ensureCapacity(size + numNew); // 增加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; }
ArrayList是基于數(shù)組實現(xiàn)的,屬性中也看到了數(shù)組,具體是怎么實現(xiàn)的呢?
比如就這個添加元素的方法,如果數(shù)組大,則在將某個位置的值設(shè)置為指定元素即可,如果數(shù)組容量不夠了呢?
看到add(E e)中先調(diào)用了ensureCapacity(size+1)方法,之后將元素的索引賦給elementData[size], 而后size自增。
例如初次添加時,size為0,add將elementData[0]賦值為e,然后size設(shè)置為1(類似執(zhí)行以下兩條語句elementData[0]=e;size=1)。
將元素的索引賦給elementData[size]不是會出現(xiàn)數(shù)組越界的情況嗎?這里關(guān)鍵就在ensureCapacity(size+1)中了。
4) 元素讀?。?/h3>
// 返回此列表中指定位置的元素。
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
// 返回此列表中指定位置的元素。 public E get(int index) { rangeCheck(index); return elementData(index); }
5) 元素刪除:
ArrayList提供了根據(jù)下標或者指定對象兩種方式的刪除功能。
如下:
// 刪除此列表中指定位置的元素。將任何后續(xù)元素向左移動(從它們的索引中減去一個)。 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,保留將要被移除的元素,將移除位置之后的元素向前挪動一個位置,將list末尾元素置空(null),返回被移除的元素。
// 從此列表中刪除第一次出現(xiàn)的指定元素(如果存在)。如果列表不包含該元素,則保持不變。 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; } // 私有刪除方法,跳過邊界檢查并且不返回刪除的值。 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; }
首先通過代碼可以看到,當移除成功后返回true,否則返回 false。remove(Object o)中通過遍歷 element 尋找是否存在傳入對象,一旦找到就調(diào)用 fastRemove 移除對象。
為什么找到了元素就知道了index,不通過remove(index) 來移除元素呢?
因為 fastRemove 跳過了判斷邊界的處理,找到元素就相當于確定了 index 不會超過邊界,而且 fastRemove 并不返回被移除的元素。
// 從此列表中刪除索引介于兩者之間的所有元素 protected void removeRange(int fromIndex, int toIndex) { modCount++; int numMoved = size - toIndex; System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved); int newSize = size - (toIndex-fromIndex); for (int i = newSize; i < size; i++) { elementData[i] = null; } size = newSize; }
執(zhí)行過程是將 elementData 從 toIndex 位置開始的元素向前移動到 fromIndex,然后將 toIndex 位置之后的元素全部置空順便修改size。
這個方法是protected,及受保護的方法,為什么這個方法被定義為protected呢? 先看下面這個例子
ArrayList<Integer> ints = new ArrayList<Integer>(Arrays.asList(0, 1, 2, 3, 4, 5, 6)); ints.subList(2, 4).clear(); System.out.println(ints);
輸出結(jié)果是[0, 1, 4, 5, 6],結(jié)果是不是像調(diào)用了 removeRange(int fromIndex,int toIndex)!
6) 調(diào)整數(shù)組容量ensureCapacity:
從上面介紹的向ArrayList中存儲元素的代碼中,我們看到,每當向數(shù)組中添加元素時,都要去檢查添加后元素的個數(shù)是否會超出當前數(shù)組的長度,如果超出,數(shù)組將會進行擴容,以滿足添加數(shù)據(jù)的需求。
數(shù)組擴容通過一個公開的方法 ensureCapacity(int minCapacity) 來實現(xiàn)。
在實際添加大量元素前,我也可以使用ensureCapacity來手動增加ArrayList實例的容量,以減少遞增式再分配的數(shù)量。
public void ensureCapacity(int minCapacity) { modCount++; int oldCapacity = elementData.length; if (minCapacity > oldCapacity) { Object oldData[] = elementData; int newCapacity = (oldCapacity * 3) / 2 + 1; // 增加50%+1 if (newCapacity < minCapacity) newCapacity = minCapacity; // 通常接近 size elementData = Arrays.copyOf(elementData, newCapacity); } }
從上述代碼中可以看出,數(shù)組進行擴容時,會將老數(shù)組中的元素重新拷貝一份到新的數(shù)組中,每次數(shù)組 容量的增長大約是其原容量的1.5倍。
這種操作的代價是很高的,因此在實際使用時,我們應(yīng)該盡量避 免數(shù)組容量的擴張。
當我們可預(yù)知要保存的元素的多少時,要在構(gòu)造ArrayList實例時,就指定其容量, 以避免數(shù)組擴容的發(fā)生。
或者根據(jù)實際需求,通過調(diào)用ensureCapacity方法來手動增加ArrayList實例的容量。
// 為什么要用到 oldData[]Object oldData[] = elementData;
乍一看來后面并沒有用到關(guān)于oldData, 這句話顯得多此一舉!但是這是一個牽涉到內(nèi)存管理的類, 所以要 了解內(nèi)部的問題 。
而且為什么這一句還在if的內(nèi)部,這跟 elementData = Arrays.copyOf(elementData, newCapacity); 這句是有關(guān)系的,下面這句Arrays.copyOf的實現(xiàn)時新創(chuàng)建了newCapacity大小的內(nèi)存,然后把老的elementData放入。
好像也沒有用到oldData,有什么問題 呢。
問題就在于舊的內(nèi)存的引用是elementData, elementData指向了新的內(nèi)存塊,如果有一個局部變量oldData變量引用舊的內(nèi)存塊的話,在copy的過程中就會比較安全,因為這樣證明這塊老的內(nèi)存依 然有引用,分配內(nèi)存的時候就不會被侵占掉,然后copy完成后這個局部變量的生命期也過去了,然后釋放才是安全的。
不然在copy的的時候萬一新的內(nèi)存或其他線程的分配內(nèi)存侵占了這塊老的內(nèi)存,而 copy 還沒有結(jié)束,這將是個嚴重的事情。
ArrayList還給我們提供了將底層數(shù)組的容量調(diào)整為當前列表保存的實際元素的大小的功能。
它可以通過trimToSize方法來實現(xiàn)。
代碼如下:
// 將此ArrayList實例的容量修剪為列表的當前大小??梢允褂么瞬僮鱽碜钚』疉rrayList實例的存儲空間。 public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }
由于elementData的長度會被拓展,size標記的是其中包含的元素的個數(shù)。
所以會出現(xiàn)size很小但elementData.length很大的情況,將出現(xiàn)空間的浪費。
trimToSize將返回一個新的數(shù)組給elementData,元素內(nèi)容保持不變,length和size相同,節(jié)省空間。
7) 轉(zhuǎn)為靜態(tài)數(shù)組toArray
注意ArrayList的兩個轉(zhuǎn)化為靜態(tài)數(shù)組的toArray方法。
第一個,調(diào)用Arrays.copyOf將返回一個數(shù)組,數(shù)組內(nèi)容是size個elementData的元素,即拷貝
elementData從0至size-1位置的元素到新數(shù)組并返回。
// 返回一個數(shù)組,該數(shù)組以適當?shù)捻樞虬肆斜碇械乃性亍? public Object[] toArray() { return Arrays.copyOf(elementData, size);}
第二個,如果傳入數(shù)組的長度小于size,返回一個新的數(shù)組,大小為size,類型與傳入數(shù)組相同。
所傳入數(shù)組長度與size相等,則將elementData復(fù)制到傳入數(shù)組中并返回傳入的數(shù)組。
若傳入數(shù)組長度大于size,除了復(fù)制elementData外,還將把返回數(shù)組的第size個元素置為空。
/* 以適當?shù)捻樞蚍祷匾粋€包含此列表中所有元素的數(shù)組; 返回數(shù)組的運行時類型是指定數(shù)組的類型。 如果列表適合指定的數(shù)組,則在其中返回。 否則將使用指定數(shù)組的運行時類型和此列表的大小分配一個新數(shù)組。*/ public <T> T[] toArray(T[] a) { if (a.length < size) // 創(chuàng)建一個運行時類型的新數(shù)組 return (T[]) Arrays.copyOf(elementData, size, a.getClass()); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; }
Fail-Fast機制:
ArrayList也采用了快速失敗的機制,通過記錄modCount參數(shù)來實現(xiàn)。
在面對并發(fā)的修改時,迭代器很快就會完全失敗,而不是冒著在將來某個不確定時間發(fā)生任意不確定行為的風險
總結(jié)
關(guān)于ArrayList的源碼,給出幾點比較重要的總結(jié):
- 注意其三個不同的構(gòu)造方法。無參構(gòu)造方法構(gòu)造的ArrayList的容量默認為10,帶有Collection參數(shù)的構(gòu)造方法,將Collection轉(zhuǎn)化為數(shù)組賦給ArrayList的實現(xiàn)數(shù)組elementData。
- 注意擴充容量的方法ensureCapacity。ArrayList在每次增加元素(可能是1個,也可能是一組) 時,都要調(diào)用該方法來確保足夠的容量。當容量不足以容納當前的元素個數(shù)時,就設(shè)置新的容量為舊的 容量的1.5倍加1,如果設(shè)置后的新容量還不夠,則直接新容量設(shè)置為傳入的參數(shù)(也就是所需的容量),而后用Arrays.copyof()方法將元素拷貝到新的數(shù)組。從中可以看出,當容 量不夠時,每次增加元素,都要將原來的元素拷貝到一個新的數(shù)組中,非常之耗時,也因此建議在事先 能確定元素數(shù)量的情況下,才使用ArrayList,否則建議使用LinkedList。
- ArrayList的實現(xiàn)中大量地調(diào)用了 Arrays.copyOf() 和 System.arrayCopy() 方法。
我們有必要對這兩個方法的實現(xiàn)做下深入的了解。
先看Arrays.copyof()方法,它有很多個重載的方法,但實現(xiàn)思路都是一樣的,看看泛型版本的源碼:
復(fù)制指定的數(shù)組,用空值截斷或填充(如有必要),使副本具有指定的長度。
對于在原始數(shù)組和副本中都有效的所有索引,這兩個數(shù)組將包含相同的值。對于副本中有效但不是原始索引的任何索引,副本將包含null。
當且僅當指定的長度大于原始數(shù)組的長度時,此類索引才會存在。// 生成的數(shù)組與原始數(shù)組的類完全相同。
public static <T> T[] copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass()); }
很明顯調(diào)用了另一個copyOf方法,該方法有三個參數(shù),最后一個參數(shù)指明要轉(zhuǎn)換的數(shù)據(jù)的類型,其源碼 如下:
// newType類 public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { @SuppressWarnings("unchecked") T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength]: (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
這里可以很明顯地看出,該方法實際上是在其內(nèi)部又創(chuàng)建了一個長度為newlength的數(shù)組,調(diào)用
System.arrayCopy()方法,將原來數(shù)組中的元素復(fù)制到了新的數(shù)組中。
下面來看System.arrayCopy()方法。該方法被標記了native,調(diào)用了系統(tǒng)的C/C++代碼,在JDK中是看不到的,但在openJDK中可以看到其源碼。該函數(shù)實際上最終調(diào)用了C語言的memmove()函數(shù),因此它可以保證同一個數(shù)組內(nèi)元素的正確復(fù)制和移動,比一般的復(fù)制方法的實現(xiàn)效率要高很多,很適合用來批量處理數(shù)組。
Java強烈推薦在復(fù)制大量數(shù)組元素時用該方法,以取得更高的效率。
ArrayList基于數(shù)組實現(xiàn),可以通過下標索引直接查找到指定位置的元素,因此查找效率高,但每次插入或刪除元素,就要大量地移動元素,插入刪除元素的效率低。
在查找給定元素索引值等的方法中,源碼都將該元素的值分為null和不為null兩種情況處理, ArrayList中允許元素為null。
到此這篇關(guān)于Java中ArrayList實現(xiàn)原理及基本方法的文章就介紹到這了,更多相關(guān)ArrayList實現(xiàn)原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Boot連接超時導(dǎo)致502錯誤的實戰(zhàn)案例
這篇文章主要給大家介紹了關(guān)于Spring Boot連接超時導(dǎo)致502錯誤的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09使用IDEA創(chuàng)建SpringBoot項目的方法步驟
這篇文章主要介紹了使用IDEA創(chuàng)建SpringBoot項目的方法步驟,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05JWT在OpenFeign調(diào)用中進行令牌中繼詳解
Feign是一個聲明式的Web Service客戶端,是一種聲明式、模板化的HTTP客戶端。而OpenFeign是Spring Cloud 在Feign的基礎(chǔ)上支持了Spring MVC的注解,如@RequesMapping等等,這篇文章主要給大家介紹了關(guān)于JWT在OpenFeign調(diào)用中進行令牌中繼的相關(guān)資料,需要的朋友可以參考下2021-10-10Java常用正則表達式驗證工具類RegexUtils.java
相信大家對正則表達式一定都有所了解和研究,這篇文章主要為大家分享了Java 表單注冊常用正則表達式驗證工具類,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-11-11