Java ArrayList使用總結(jié)
提起ArrayList,相信很多小伙伴都用過,而且還不少用。但在幾年之前,我在一場面試中,面試官要求說出ArrayList的擴容機制。很顯然,那個時候的我并沒有關(guān)注這些,從而錯過了一次機會。不過好在我還算比較喜歡搞事情的,所以今天這篇文章也算是填坑吧。
看完這邊文章你將了解到:
- ArrayList底層實現(xiàn)
- ArrayList為什么允許null值
- ArrayList為什么可重復
- ArrayList查詢效率和插入效率對比
類圖
下圖是ArrayList的類圖結(jié)構(gòu)
ArrayList繼承于 AbstractList,實現(xiàn)了 List, RandomAccess, Cloneable, java.io.Serializable 這些接口。
這里逐個分析一下這里接口的意義:
- RandomAccess是一個標志接口,表明實現(xiàn)這個這個接口的 List集合是支持快速隨機訪問的。有興趣可以看看Collections類中哪個方法用到了這個標志性接口。
- 實現(xiàn) Cloneable接口并覆蓋了方法clone(),能被克隆。
- 實現(xiàn)了java.io.Serializable 接口,這意味著ArrayList支持序列化,能通過序列化去傳輸(請注意,ArrayList的序列化是有點小特殊的,后面會講解)。
源碼解析
成員變量
在正式進入源碼分析之前,我們有必要先看看它的成員變量都有哪些,這里列舉比較重要的成員變量:
private int size; // 實際元素個數(shù) transient Object[] elementData; //真正保存元素的數(shù)組 private static final int DEFAULT_CAPACITY = 10;//默認的初始容量大小
構(gòu)造方法
我們有三種初始化辦法:無參數(shù)直接初始化、指定大小初始化、指定初始數(shù)據(jù)初始化,源碼如下:
//1、無參數(shù)直接初始化,數(shù)組大小為空 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } //2、指定初始數(shù)據(jù)初始化 public ArrayList(Collection<? extends E> c){ //elementData是保存數(shù)組的容器,默認為null elementData=c.toArray(); //如果給定的集合(c)數(shù)據(jù)有值 if((size=elementData.length)!=0){ //c.toArray might(incorrectly)not return Object[](see 6260652) //如果集合元素類型不是Object類型,我們會轉(zhuǎn)成Object if(elementData.getClass()!=Object[].class){ elementData=Arrays.copyOf(elementData,size,Object].class); } }else{ //給定集合(c)無值,則默認空數(shù)組 this.elementData=EMPTY_ELEMENTDATA } } //3、指定初始容量 public ArrayList(int initialCapacity) { //指定的初始容量大于0,將elementData初始化為指定大小的數(shù)組 if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { //否則初始化成一個空數(shù)組 this.elementData = EMPTY_ELEMENTDATA; } }
除過源碼中注釋外,補充幾點:
- ArrayList無參構(gòu)造器初始化時,默認大小是空數(shù)組,并不是大家常說的10,10是在第一次add的時候擴容的數(shù)組值。
- 使用方式二進行創(chuàng)建對象時,如果入?yún)⑷萜鞅4娴膶ο蟛皇荗bject,則轉(zhuǎn)換為Object。
DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA又是什么鬼?它其實是定義在成員變量的兩個空數(shù)組,
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; private static final Object[] EMPTY_ELEMENTDATA = {};
很明顯問題來了,既然都是空數(shù)組,為什么要聲明兩個?一個不行嗎?讀者請先思考一下,帶著疑問往下看。
新增和擴容實現(xiàn)
通過構(gòu)造方法可以很清楚的看到,ArrayList的確是基于數(shù)組的,但動態(tài)又從何說起?
新增時就是給數(shù)組中添加元素,主要分為兩步走:
- 判斷是否需要擴容,如果需要擴容執(zhí)行擴容操作;
- 直接賦值。
對應源碼如下:
public boolean add(E e) { //確保數(shù)組大小是否足夠,不夠執(zhí)行擴容,size為當前數(shù)組元素個數(shù),判斷size+1是因為后面還要size++ ensureCapacityInternal(size + 1); //1 elementData[size++] = e;//2 return true; }
我們先來看一下擴容部分的源碼:
private void ensureCapacityInternal(int minCapacity) { //先調(diào)用calculateCapacity計算容量 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity(Object[] elementData, int minCapacity) { //如果當前數(shù)組還是個空數(shù)組,也就是他用過無參構(gòu)造去初始化的 //那么直接返回DEFAULT_CAPACITY,即10 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { modCount++; // 如果當前容量已經(jīng)大于當前數(shù)組的長度了,說明需要去擴容了 if (minCapacity - elementData.length > 0) //擴容 grow(minCapacity); } private void grow(int minCapacity){ int oldCapacity = elementData.length; //oldCapacity>>1是把oldCapacity除以2的意思 int newCapacity=oldCapacity+(oldCapacity>>1); //如果擴容后的值<我們的期望值,擴容后的值就等于我們的期望值 if(newCapacity-minCapacity<0) newCapacity = minCapacity; //如果擴容后的值>jvm所能分配的數(shù)組的最大值,那么就用Integer的最大值 if(newCapacity-MAX_ARRAY_SIZE>0) elementData=Arrays.copyOf(elementData,newCapacity); }
注釋相對來說已經(jīng)比較詳細了,這里需要注意以下幾點:
- 上面有個問題是為什么需要聲明兩個空數(shù)組。我們在看到上面源碼的時候有一個方法為calculateCapacity,這個方法內(nèi)部邏輯只有在通過無參構(gòu)造初始化ArrayList的時候才會改變將要返回的minCapacity。而返回的這個值將會決定下面的數(shù)組是否需要擴容。如果我們通過指定大小的方式初始化ArrayList并指定大小為0,這說明我們需要的就是一個空的ArrayList,不需要去擴容,你細品;
- 新增時,沒有對值進行校驗,所以新增值可以為null,且沒有做重復值判斷,所以元素可以重復;
- ArrayList中的數(shù)組的最大值是Integer.MAX_VALUE,超過這個值,JVM就不會給數(shù)組分配內(nèi)存空間了;
- 擴容是原來容量大小+容量大小的一半,簡單說就是擴容后的大小是原來容量的1.5倍。
擴容完成之后,就是簡單的賦值了,賦值時并沒有加鎖,所以是線程不安全的。
擴容的本質(zhì)
在grow方法的最后,擴容是通過Arrays.copyOf(elementData,newCapacity);這行代碼實現(xiàn)的。這個方法實際上調(diào)用的方法是我們經(jīng)常使用的System.arraycopy:
/** *@param src 被拷貝的數(shù)組 *@param srcPos 從數(shù)組那里開始 *@param dest 目標數(shù)組 *@param destPos從目標數(shù)組那個索引位置開始拷貝 *@param length 拷貝的長度 *此方法是沒有返回值的,通過dest的引用進行傳值 */ public static native void arraycopy(Object src, int srcPos,Object dest, int destPos,int length);
這個方法是一個native方法,雖然不能看到方法內(nèi)部的具體實現(xiàn),但通過參數(shù)也可以管中窺豹。這個方法會移動元素。所以說數(shù)組如果要擴容,需要重新分配一塊更大的空間,再把數(shù)據(jù)全部復制過去,時間復雜度 O(N);而且你如果想在數(shù)組中間進行插入和刪除,每次必須搬移后面的所有數(shù)據(jù)以保持連續(xù),時間復雜度 O(N)。由于數(shù)組又是一塊連續(xù)的內(nèi)存空間,能夠根據(jù)索引快速訪問元素。
上面也就解釋了一開始那個問題:ArrayList為什么插入慢,查詢快。
刪除
ArrayList有多種刪除方法,這里以根據(jù)值刪除的方式進行說明(其他原理類似):
public boolean remove(Object o) { //如果要刪除的值是null,刪除第一個是null的值 if(o==null){ for(int index=0;index<size;index++) if(elementData[index]==null){ fastRemove(index) return true; } }else{ //如果要刪除的值不為null,找到第一個和要刪除的值相等的刪除 for(int index=0;index<size;index++) //這里是根據(jù) equals來判斷值相等的,相等后再根據(jù)索引位置進行刪除 //所以根據(jù)對象刪除時,一般來說,如果你確定要刪除的是某一類的業(yè)務對象,則需要重寫equals if(o.equals(elementData[index]){ fastRemove(index) return true; } } return false }
核心其實是fastRemove方法:
private void fastRemove(int index){ //記錄數(shù)組的結(jié)構(gòu)要發(fā)生變動了 nodCount++; //numMoved表示刪除index位置的元素后,需要從index后移動多少個元素到前面去 //減1的原因,是因為size從1開始算起,index從0開始算起 int numMoved=size-index-1; if(numMoved>0) //從index+1位置開始被拷貝,拷貝的起始位置是index,長度是numMoved System.arraycopy(elementData, index+1, elementData, index, numMoved); //數(shù)組最后一個位置賦值null,幫助GC(沒有引用則自動回收了) elementData[--size] = null; }
從源碼中,我們可以看出,某一個元素被刪除后,為了維護數(shù)組結(jié)構(gòu),我們都會把數(shù)組后面的元素往前移動,同時釋放最后一個引用,便于回收。
總結(jié)
本文主要從ArrayList的源碼入手,分別從初始化、新增、擴容、刪除四個方面展開學習。我們發(fā)現(xiàn)ArrayList內(nèi)部其實就是圍繞了一個數(shù)組,在數(shù)組容量不足時將數(shù)組擴容至更大,所以也就自然被稱作基于動態(tài)數(shù)組。
微信搜索Java成神之路或掃描下方二維碼,發(fā)現(xiàn)更多Java有趣知識,讓我們一起成長!
以上就是Java ArrayList使用總結(jié)的詳細內(nèi)容,更多關(guān)于Java ArrayList使用的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java基于命令模式實現(xiàn)郵局發(fā)信功能詳解
這篇文章主要介紹了Java基于命令模式實現(xiàn)郵局發(fā)信功能,較為詳細的分析了命令行模式的概念、原理并結(jié)合實例形式分析了Java使用命令行模式實現(xiàn)郵局發(fā)信功能的相關(guān)操作技巧與注意事項,需要的朋友可以參考下2018-04-04Java Swing中的文本框(JTextField)與文本區(qū)(JTextArea)使用實例
這篇文章主要介紹了Java Swing中的文本框(JTextField)與文本區(qū)(JTextArea)使用實例,Swing是一個用于開發(fā)Java應用程序用戶界面的開發(fā)工具包,需要的朋友可以參考下2014-10-10Java Swing中的表格(JTable)和樹(JTree)組件使用實例
這篇文章主要介紹了Java Swing中的表格(JTable)和樹(JTree)組件使用實例,本文同時講解了表格和樹的基本概念、常用方法、代碼實例,需要的朋友可以參考下2014-10-10SpringBoot擴展點EnvironmentPostProcessor實例詳解
這篇文章主要介紹了SpringBoot擴展點EnvironmentPostProcessor的相關(guān)知識,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-04-04Java實現(xiàn)生產(chǎn)者消費者問題與讀者寫者問題詳解
這篇文章主要介紹了Java實現(xiàn)生產(chǎn)者消費者問題與讀者寫者問題詳解,小編覺得挺不錯的,這里分享給大家,供需要的親朋好友參考。2017-11-11