集合詳解(二)----ArrayList原始碼剖析(JDK1.7)

weixin_34127717發表於2016-07-25

ArrayList


    ArrayList是List類的一個典型的實現,是基於陣列實現的List類,因此,ArrayList封裝了一個動態的、可變長度的Object[]陣列。ArrayList是通過initialCapacity引數來設定陣列長度的,當向ArrayList新增的資料超出了ArrayList的長度之後,initialCapacity會自動增加。

    

私有屬性

    ArrayList定義了兩個私有屬性:

//elementData儲存ArrayList內的元素,size表示它包含的元素的數量。
private transient Object[] elementData;
private int size;

    其中有一個關鍵字:transient:Java的serialization提供了一種持久化物件例項的機制。當持久化物件時,可能有一個特殊的物件資料成員,我們不想用serialization機制來儲存它。為了在一個特定物件的一個域上關閉serialization,可以在這個域前加上關鍵字transient。

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 {  
             // 序列化,被設定為transient的屬性沒有被序列化  
             ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(  
                     "UserInfo.out"));  
             o.writeObject(userInfo);  
             o.close();  
         } catch (Exception e) {  
             // TODO: handle exception  
             e.printStackTrace();  
         }  
         try {  
             // 重新讀取內容  
             ObjectInputStream in = new ObjectInputStream(new FileInputStream(  
                     "UserInfo.out"));  
             UserInfo readUserInfo = (UserInfo) in.readObject();  
             //讀取後psw的內容為null  
             System.out.println(readUserInfo.toString());  
         } catch (Exception e) {  
             // TODO: handle exception  
             e.printStackTrace();  
         }  
     }  
 }

    被標記為transient的屬性在物件被序列化的時候不會被儲存。

構造方法

    ArrayList提供了三種方式的構造器。

 // ArrayList帶容量大小的建構函式。
 public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

    //ArrayList無引數構造引數,預設容量10
    public ArrayList() {
        super();
        this.elementData = EMPTY_ELEMENTDATA;
    }

     // 建立一個包含collection的ArrayList   
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray(); //呼叫toArray()方法把collection轉換成陣列 
        size = elementData.length; //把陣列的長度賦值給ArrayList的size屬性
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }

    在這有一個地方需要注意下,就是在JDK1.6中無引數的構造方法是這麼寫的:

  // ArrayList無參建構函式。預設容量是10。    
    public ArrayList() {    
        this(10);    
    }   

    在1.7前,會預設在記憶體中直接分配10個空間,但是在1.7有了改變,會先在記憶體中分配一個物件的記憶體空間,但是這個物件是沒有長度的。但是在你進行新增的時候,預設的會去拿物件的預設大小來作比較。

    

ArrayList的動態擴容(核心)

    當ArrayList進行add操作的時候,如果新增的元素超出了陣列的長度,怎麼辦?

 public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    add方法會去呼叫下面的方法,根據傳入的最小需要容量minCapacity來和陣列的容量長度對比,若minCapactity大於或等於陣列容量,則需要進行擴容。

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        //超出了陣列可容納的長度,需要進行動態擴充套件
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    擴容的時候會去呼叫grow()方法來進行動態擴容,在grow中採用了位運算,我們知道位運算的速度遠遠快於整除運算:


private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

//這才是動態擴充套件的精髓,看到這個方法,ArrayList瞬間被打回原形
private void grow(int minCapacity) {
     int oldCapacity = elementData.length;
     //首先得到陣列的舊容量,然後進行oldCapacity + (oldCapacity >> 1),將oldCapacity 右移一位,其效果相當於oldCapacity /2,整句的結果就是設定新陣列的容量擴充套件為原來陣列的1.5倍
     int newCapacity = oldCapacity + (oldCapacity >> 1);
     //再判斷一下新陣列的容量夠不夠,夠了就直接使用這個長度建立新陣列, 
     //不夠就將陣列長度設定為需要的長度
     if (newCapacity - minCapacity < 0)
         newCapacity = minCapacity;
     //判斷有沒超過最大限制,如果超出限制則呼叫hugeCapacity
     if (newCapacity - MAX_ARRAY_SIZE > 0)
         newCapacity = hugeCapacity(minCapacity);
     //將原來陣列的值copy新陣列中去, ArrayList的引用指向新陣列
     //這兒會新建立陣列,如果資料量很大,重複的建立的陣列,那麼還是會影響效率,
     //因此鼓勵在合適的時候通過構造方法指定預設的capaticy大小
     elementData = Arrays.copyOf(elementData, newCapacity);
 }
private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

    有一點需要注意的是,容量擴充,是建立一個新的陣列,然後將舊陣列上的陣列copy到新陣列,這是一個很大的消耗,所以在我們使用ArrayList時,最好能預計資料的大小,在第一次建立時就申請夠記憶體。

    看一下JDK1.6的動態擴容的實現原理:

public void ensureCapacity(int minCapacity) {
      modCount++;
     int oldCapacity = elementData.length;
     if (minCapacity > oldCapacity) {
         Object oldData[] = elementData;
         int newCapacity = (oldCapacity * 3)/2 + 1;
             if (newCapacity < minCapacity)
         newCapacity = minCapacity;
             // minCapacity is usually close to size, so this is a win:
             elementData = Arrays.copyOf(elementData, newCapacity);
     }
     }

    從程式碼上,我們可以看出區別:
    第一:在容量進行擴充套件的時候,其例項如整除運算將容量擴充套件為原來的1.5倍加1,而jdk1.7是利用位運算,從效率上,jdk1.7就要快於jdk1.6。
    第二:在算出newCapacity時,其沒有和ArrayList所定義的MAX_ARRAY_SIZE作比較,為什麼沒有進行比較呢,原因是jdk1.6沒有定義這個MAX_ARRAY_SIZE最大容量,也就是說,其沒有最大容量限制的,但是jdk1.7做了一個改進,進行了容量限制。

相關文章