您好,登錄后才能下訂單哦!
前面一章節,我們介紹了集合的類圖,那么本節將學習Collection 接口中最常用的子類ArrayList類,本章分為下面幾部分講解(說明本章采用的JDK1.6源碼進行分析,因為個人認為雖然JDK1.8進行了部分改動,但萬變不離其宗,仍然采用的JDK1.6的引子進行的優化,因此學會了1.6對于1.8也就理解了)。
一、ArrayList 的常見功能
在分析ArrayList的源碼前,我們先看下ArrayList的常見的功能:
package study.collection; import java.util.ArrayList; import java.util.Date; import java.util.List; public class TestDemo01 { public static void main(String[] args) { List list = new ArrayList(); //ArrayList:底層實現時數組,線程不安全,效率高。所以,查詢快。修改、插入、刪除慢。 //LinkedList:底層實現是鏈表,線程不安全,效率高。所以,查詢慢。修改、插入、刪除快。 //Vector:線程安全的,效率低。 list.add("aaa"); list.add("aaa"); list.add(new Date()); list.add(new Dog()); list.add(1234); //注意,list集合中只能添加引用類型,這里包裝類的:自動裝箱! list.remove(new String("aaa")); System.out.println(list.size()); for(int i=0;i<list.size();i++){ System.out.println(list.get(i)); } list.set(3, new String("3333")); list.add(4, new String("3333")); System.out.println(list.isEmpty()); list.remove(new Dog()); //hashcode和equals System.out.println(list.size()); List list2 = new ArrayList(); list2.add("bbb"); list2.add("ccc"); list.add(list2); //跟順序的操作 String str = (String) list.get(0); System.out.println(str); list.set(1, "ababa"); list.remove(0); } } class Dog { }
從上述可以看到了,ArrayList 接口中除了繼承自父類Collection 接口中的方法外,還實現了List接口中擴充的和索引相關的方法,這都源于其底層為數組結構。
二、ArrayList 的重要的屬性
上面的部分列舉了ArrayList中常見的一些功能方法,那么這些方法又是如何使用的呢,下面我們將進行源碼的剖析,在剖析前,我們可以自己思考下,我們知道ArrayList 是一個動態擴展的集合,之所以動態擴展的原因或者說比數組強的地方肯定就在于數組的長度是固定的,不能擴展,這是數組的最大缺陷,所以才有了集合,那么ArrayList,那么其底層肯定也采用的是數組結構,因為它叫ArrayList嘛,那么其重要的屬性之一,必然是定義了一個數組。如下:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8683452581122892189L; /** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. */ private transient Object[] elementData; /** * The size of the ArrayList (the number of elements it contains). * * @serial */ private int size;
ArrayList就是一個以數組形式實現的集合,其元素的功能為:
private transient Object[] elementData; //ArrayList是基于數組的一個實現,elementData就是底層的數組
private int size; //ArrayList里面元素的個數,這里要注意一下,size是按照調用add、remove方法的次數進行自增或者自減的,所以add了一個null進入ArrayList,size也會加1
三、ArrayList 的構造方法分析
在分析完上面的屬性后,我們緊接著來看下ArrayList的構造方法:
/** *構造一個具有指定容量的list */ public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } /** * 構造一個初始容量為10的list,也就說當我們經常采用new ArrayList()的時候,實際分配的大小就為10 */ public ArrayList() { this(10); } /** *構造一個包含指定元素的list,這些元素的是按照Collection的迭代器返回的順序排列的 */ public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); size = elementData.length; // c.toArray might (incorrectly) not return Object[] (see 6260652) 這里是因為toArray可能不一定是Object類型的,因此判斷如果不是就進行了拷貝轉換操作 if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); }
可以看到,ArrayList 提供了三個構造方法,分別的含義,已經注釋到代碼上面了,那么想一下指定容量的構造方法的意義,既然默認為10就可以那么為什么還要提供一個可以指定容量大小的構造方法呢?思考下,下面會說到。
四、ArrayList 的常見方法分析
1.add 添加元素
添加的方法,共有四個,下面我們分別分析下其功能源碼:
/** *添加一個元素 */ public boolean add(E e) { // 進行擴容檢查 ensureCapacity(size + 1); // Increments modCount!! //將e增加至list的數據尾部,容量+1 elementData[size++] = e; return true; } /** *在指定位置添加一個元素 */ public void add(int index, E element) { // 判斷索引是否越界 if (index > size || index < 0) throw new IndexOutOfBoundsException( "Index: "+index+", Size: "+size); // 進行擴容檢查 ensureCapacity(size+1); // Increments modCount!! // 對數組進行復制處理,目的就是空出index的位置插入element,并將index后的元素位移一個位置 System.arraycopy(elementData, index, elementData, index + 1, size - index); // 將指定的index位置賦值為element elementData[index] = element; // list容量+1 size++; } /** *增加一個集合元素 */ public boolean addAll(Collection<? extends E> c) { //將c轉換為數組 Object[] a = c.toArray(); int numNew = a.length; //擴容檢查 ensureCapacity(size + numNew); // Increments modCount //將c添加至list的數據尾部 System.arraycopy(a, 0, elementData, size, numNew); //更新當前容器大小 size += numNew; return numNew != 0; } /** * 在指定位置,增加一個集合元素 */ 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); // Increments modCount // 計算需要移動的長度(index之后的元素個數) int numMoved = size - index; // 數組復制,空出第index到index+numNum的位置,即將數組index后的元素向右移動numNum個位置 if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, numMoved); // 將要插入的集合元素復制到數組空出的位置中 System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; } /** * 數組容量檢查,不夠時則進行擴容 */ public void ensureCapacity( int minCapacity) { modCount++; // 當前數組的長度 int oldCapacity = elementData.length; // 最小需要的容量大于當前數組的長度則進行擴容 if (minCapacity > oldCapacity) { Object oldData[] = elementData; // 新擴容的數組長度為舊容量的1.5倍+1 int newCapacity = (oldCapacity * 3)/2 + 1; // 如果新擴容的數組長度還是比最小需要的容量小,則以最小需要的容量為長度進行擴容 if (newCapacity < minCapacity) newCapacity = minCapacity; // minCapacity is usually close to size, so this is a win: // 進行數據拷貝,Arrays.copyOf底層實現是System.arrayCopy() elementData = Arrays.copyOf(elementData, newCapacity); } }
2.remove 刪除
/** * 根據索引位置刪除元素 */ public E remove(int index) { // 數組越界檢查 RangeCheck(index); modCount++; // 取出要刪除位置的元素,供返回使用 E oldValue = (E) elementData[index]; // 計算數組要復制的數量 int numMoved = size - index - 1; // 數組復制,就是將index之后的元素往前移動一個位置 if (numMoved > 0) System. arraycopy(elementData, index+1, elementData, index, numMoved); // 將數組最后一個元素置空(因為刪除了一個元素,然后index后面的元素都向前移動了,所以最后一個就沒用了),好讓gc盡快回收 // 不要忘了size減一 elementData[--size ] = null; // Let gc do its work return oldValue; } /** * 根據元素內容刪除,只刪除匹配的第一個 */ public boolean remove(Object o) { // 對要刪除的元素進行null判斷 // 對數據元素進行遍歷查找,知道找到第一個要刪除的元素,刪除后進行返回,如果要刪除的元素正好是最后一個那就慘了,時間復雜度可達O(n) 。。。 if (o == null) { for (int index = 0; index < size; index++) // null值要用==比較 if (elementData [index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) // 非null當然是用equals比較了 if (o.equals(elementData [index])) { fastRemove(index); return true; } } return false; } /* * Private remove method that skips bounds checking and does not * return the value removed. */ private void fastRemove(int index) { modCount++; // 原理和之前的add一樣,還是進行數組復制,將index后的元素向前移動一個位置,不細解釋了, int numMoved = size - index - 1; if (numMoved > 0) System. arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size ] = null; // Let gc do its work } /** * 數組越界檢查 */ private void RangeCheck(int index) { if (index >= size ) throw new IndexOutOfBoundsException( "Index: "+index+", Size: " +size); }
分析:
增加和刪除方法到這里就解釋完了,代碼是很簡單,主要需要特別關心的就兩個地方:1.數組擴容,2.數組復制,這兩個操作都是極費效率的,最慘的情況下(添加到list第一個位置,刪除list最后一個元素或刪除list第一個索引位置的元素)時間復雜度可達O(n)。
還記得上面那個坑嗎(為什么提供一個可以指定容量大小的構造方法 )?看到這里是不是有點明白了呢,簡單解釋下:如果數組初試容量過小,假設默認的10個大小,而我們使用ArrayList的主要操作時增加元素,不斷的增加,一直增加,不停的增加,會出現上面后果?那就是數組容量不斷的受挑釁,數組需要不斷的進行擴容,擴容的過程就是數組拷貝System.arraycopy的過程,每一次擴容就會開辟一塊新的內存空間和數據的復制移動,這樣勢必對性能造成影響。那么在這種以寫為主(寫會擴容,刪不會縮容)場景下,提前預知性的設置一個大容量,便可減少擴容的次數,提高了性能。
數組擴容伴隨著開辟新建的內存空間以創建新數組然后進行數據復制,而數組復制不需要開辟新內存空間,只需將數據進行復制。
上面講增加元素可能會進行擴容,而刪除元素卻不會進行縮容,如果在已刪除為主的場景下使用list,一直不停的刪除而很少進行增加,那么會出現什么情況?再或者數組進行一次大擴容后,我們后續只使用了幾個空間,會出現上面情況?當然是空間浪費啦啦啦,怎么辦呢?
/** * 將底層數組的容量調整為當前實際元素的大小,來釋放空間。 */ public void trimToSize() { modCount++; // 當前數組的容量 int oldCapacity = elementData .length; // 如果當前實際元素大小 小于 當前數組的容量,則進行縮容 if (size < oldCapacity) { elementData = Arrays.copyOf( elementData, size ); }
3.更新 set
/** * 將指定位置的元素更新為新元素 */ public E set( int index, E element) { // 數組越界檢查 RangeCheck(index); // 取出要更新位置的元素,供返回使用 E oldValue = (E) elementData[index]; // 將該位置賦值為行的元素 elementData[index] = element; // 返回舊元素 return oldValue; }
4.查找
/** * 查找指定位置上的元素 */ public E get( int index) { RangeCheck(index); return (E) elementData [index]; }
由于ArrayList使用數組實現,更新和查找直接基于下標操作,變得十分簡單。
5.是否包含.
/** * Returns <tt>true</tt> if this list contains the specified element. * More formally, returns <tt>true</tt> if and only if this list contains * at least one element <tt>e</tt> such that * <tt>(o==null ? e==null : o.equals(e))</tt>. * * @param o element whose presence in this list is to be tested * @return <tt> true</tt> if this list contains the specified element */ public boolean contains(Object o) { return indexOf(o) >= 0; } /** * Returns the index of the first occurrence of the specified element * in this list, or -1 if this list does not contain the element. * More formally, returns the lowest index <tt>i</tt> such that * <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>, * or -1 if there is no such index. */ public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData [i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData [i])) return i; } return -1; } /** * Returns the index of the last occurrence of the specified element * in this list, or -1 if this list does not contain the element. * More formally, returns the highest index <tt>i</tt> such that * <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>, * or -1 if there is no such index. */ public int lastIndexOf(Object o) { if (o == null) { for (int i = size-1; i >= 0; i--) if (elementData [i]==null) return i; } else { for (int i = size-1; i >= 0; i--) if (o.equals(elementData [i])) return i; } return -1; }
contains主要是檢查indexOf,也就是元素在list中出現的索引位置也就是數組下標,再看indexOf和lastIndexOf代碼是不是很熟悉,沒錯,和public booleanremove(Object o) 的代碼一樣,都是元素null判斷,都是循環比較。
6.容量判斷
/** * Returns the number of elements in this list. * * @return the number of elements in this list */ public int size() { return size ; } /** * Returns <tt>true</tt> if this list contains no elements. * * @return <tt> true</tt> if this list contains no elements */ public boolean isEmpty() { return size == 0; }
說明:modCount 是干什么的,怎么到處都在操作這個變量,還有遍歷呢,為啥不講?由于iterator遍歷相對比較復雜,而且iterator 是GoF經典設計模式比較重要的一個,之后會對iterator單獨分析。
五、transient 分析 和 ArrayList的優缺點
1.ArrayList的優缺點
1、ArrayList底層以數組實現,是一種隨機訪問模式,再加上它實現了RandomAccess接口,因此查找也就是get的時候非常快
2、ArrayList在順序添加一個元素的時候非常方便,只是往數組里面添加了一個元素而已
不過ArrayList的缺點也十分明顯:
1、刪除元素的時候,涉及到一次元素復制,如果要復制的元素很多,那么就會比較耗費性能
2、插入元素的時候,涉及到一次元素復制,如果要復制的元素很多,那么就會比較耗費性能
因此,ArrayList比較適合順序添加、隨機訪問的場景。
2.ArrayList和Vector的區別
ArrayList是線程非安全的,這很明顯,因為ArrayList中所有的方法都不是同步的,在并發下一定會出現線程安全問題。那么我們想要使用ArrayList并且讓它線程安全怎么辦?一個方法是用Collections.synchronizedList方法把你的ArrayList變成一個線程安全的List,比如:
List<String> synchronizedList = Collections.synchronizedList(list); synchronizedList.add("aaa"); synchronizedList.add("bbb"); for (int i = 0; i < synchronizedList.size(); i++) { System.out.println(synchronizedList.get(i)); }
另一個方法就是Vector,它是ArrayList的線程安全版本,其實現90%和ArrayList都完全一樣,區別在于:
1、Vector是線程安全的,ArrayList是線程非安全的
2、Vector可以指定增長因子,如果該增長因子指定了,那么擴容的時候會每次新的數組大小會在原數組的大小基礎上加上增長因子;如果不指定增長因子,那么就給原數組大小
3.為什么ArrayList的elementData是用transient修飾的?
private transient Object[] elementData;
為什么elementData是使用transient修飾的呢?我們看一下ArrayList的定義:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
看到ArrayList實現了Serializable接口,這意味著ArrayList是可以被序列化的,用transient修飾elementData意味著我不希望elementData數組被序列化。這是為什么?因為序列化ArrayList的時候,ArrayList里面的elementData未必是滿的,比方說elementData有10的大小,但是我只用了其中的3個,那么是否有必要序列化整個elementData呢?顯然沒有這個必要,因此ArrayList中重寫了writeObject方法:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount = modCount; s.defaultWriteObject(); // Write out array length s.writeInt(elementData.length); // Write out all elements in the proper order. for (int i=0; i<size; i++) s.writeObject(elementData[i]); if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }
每次序列化的時候調用這個方法,先調用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData不去序列化它,然后遍歷elementData,只序列化那些有的元素,這樣:
1、加快了序列化的速度
2、減小了序列化之后的文件大小
六、自己編寫一個MyArrayList
package study.collection; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.ListIterator; public class MyArrayList implements List { //底層用一個數組接收 private Object[] elementData; //用于記錄集合的元素個數 private int size; /** * 無參數構造,默認為大小10 */ public MyArrayList() { this(10); } /** * 初始化帶容量的構造方法 * @param cap */ public MyArrayList(int cap) { super(); if(cap < 0) { throw new IllegalArgumentException("Illegal cap: "+ cap); } elementData = new Object[cap]; } @Override public boolean add(Object e) { //1.添加之前確認集合中的大小是夠,因此擴容判斷 ensureCapacity(size +1); //添加元素一個一個添加,因此size+1; //2.填充元素 elementData[size++] = e; return true; } /** * 擴容判斷,因為只要添加元素,就需要判斷容器的大小是否滿足 * @param i */ private void ensureCapacity(int minCapacity) { //擴容前,需要獲取當前的數組元素的大小 int oldCapacity = elementData.length; //只有當前的容量不滿足,則擴容處理 if(oldCapacity < minCapacity) { //新大小的比例,采用原來大小的1.5倍 int newCapacity = (oldCapacity * 3)/2 + 1; //如果新算出來的大小不滿足當前需要擴容的大小,則就以用戶需要的為主,如果以滿足則以算出來的最佳大小為主 if(newCapacity < minCapacity) { newCapacity = minCapacity; } //比例算好后,開始執行數組的拷貝操作 Arrays.copyOf(elementData, newCapacity,Object[].class); } } @Override public void add(int index, Object element) { //添加指定位置,首先需要做的就是確保索引滿足要求,如果要添加的索引超過了元素個數中的大小 if(index > size || index < 0) { throw new IndexOutOfBoundsException( "Index: "+index+", Size: "+size); } //如果沒有超過,那么就需要開始添加元素,這個時候同樣需要判斷擴容 ensureCapacity(size +1); //添加元素一個一個添加,因此size+1; //接著需要做的事情是需要將原來位于index 位置的元素,向后面移動 //首先判斷,index的后面是否還有元素 int modNum = size - index; if(modNum > 0) { System.arraycopy(elementData, index, elementData, index+1, size - index); } //如果沒有元素或者已經拷貝完成,則直接在對應的索引處放置元素即可 elementData[index] = element; //元素個數加+1 size++; } @Override public boolean addAll(Collection c) { //添加集合元素,首先需要將集合轉換為數組,計算出數組的大小 Object[] a = c.toArray(); //計算出需要的長度 int numNew = a.length; //開始擴容判斷 ensureCapacity(size +numNew); //添加元素的個數為numNew //開始數組拷貝 System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; } @Override public boolean addAll(int index, Collection c) { //首先索引的正確性 if (index > size || index < 0) throw new IndexOutOfBoundsException( "Index: " + index + ", Size: " + size); //添加的元素集合轉換為數組,計算要拷貝的長度,準備擴容 Object[] a = c.toArray(); int numNew = a.length; ensureCapacity(size + numNew); // Increments modCount //因為是指定位置擴容,因此需要判斷下index后面是否有元素 int numMoved = size - index; //如果大于0,說明要先空出位置來給a數組 if(numMoved > 0) { System.arraycopy(elementData, index, elementData, index+1, size-index); } //空出為位置后,然后將集合的元素放到空出的位置上面 System.arraycopy(a, 0, elementData,index, numNew); size += numNew; return numNew != 0; } @Override public void clear() { for (int i = 0; i < size; i++) elementData[i] = null; size = 0; } @Override public boolean contains(Object o) { return indexOf(o) >= 0; } @Override public boolean containsAll(Collection c) { //迭代器暫不去實現 return false; } @Override public Object get(int index) { //對于數組而言,根據索引獲取元素非常簡單,但需要先檢查inde的合法性,避免越界 RangeCheck(index); return elementData[index]; } private void RangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException( "Index: "+index+", Size: "+size); } @Override public int indexOf(Object o) { //循環遍歷,找出元素,注意是equals比較 if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1; } @Override public boolean isEmpty() { return size == 0; } @Override public Iterator iterator() { //涉及迭代器,暫不去關注 return null; } @Override public int lastIndexOf(Object o) { //反向獲取,則反向循環 if (o == null) { for (int i = size-1; i >= 0; i--) if (elementData[i]==null) return i; } else { for (int i = size-1; i >= 0; i--) if (o.equals(elementData[i])) return i; } return -1; } @Override public ListIterator listIterator() { //涉及迭代器,暫不去關注 return null; } @Override public ListIterator listIterator(int index) { //涉及迭代器,暫不去關注 return null; } @Override 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) { int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; } @Override public Object remove(int index) { //合法性檢查 RangeCheck(index); //取出原來老的元素,以便返回 Object oldValue = elementData[index]; //需要開始拷貝數組,因為刪除了索引處的元素,那么則需要向前移動元素 //需要看后面有沒有移動的元素,-1 是減去當前這個刪除的元素 int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved);//從index+1 開始拷貝到 index 處 elementData[--size] = null; //元素個數減去一,同事最后一個位置置空,等待垃圾回收 return oldValue; } @Override public boolean removeAll(Collection c) { ////涉及迭代器,暫不去關注 return false; } @Override public boolean retainAll(Collection c) { ////涉及迭代器,暫不去關注 return false; } @Override public Object set(int index, Object element) { RangeCheck(index); Object oldValue = elementData[index]; elementData[index] = element; return oldValue; } @Override public int size() { return size; } @Override public List subList(int fromIndex, int toIndex) { ////涉及迭代器,暫不去關注 return null; } @Override public Object[] toArray() { return Arrays.copyOf(elementData, size); } @Override public Object[] toArray(Object[] a) { if (a.length < size) // Make a new array of a's runtime type, but my contents: return Arrays.copyOf(elementData, size, a.getClass()); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; } //測試 public static void main(String[] args) { MyArrayList list = new MyArrayList(); list.add("333"); list.add("444"); list.add("5"); list.add("344433"); list.add("333"); list.add("333"); System.out.println(list.size()); // System.out.println(list.get(6)); list.remove("444"); System.out.println(list.size()); } }
說明:其他關于JDK1.6 和 1.7 和 1.8 的區別可以看:
JDK1.8、JDK1.7、JDK1.6區別看這里
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。