JAVA 持有對象——容器初探(持續補充)

引言

若是一個程序只包含固定數量的且其生命週期都是已知對象,那麼這是一個很是簡單的程序——《think in java》java

瞭解容器前,先提出一個問題,ArrayList和LinkedList誰的處理速度更快呢?編程

一 持有對象的方式

在Java中,咱們可使用數組來保存一組對象。可是,數組是固定大小的,在通常狀況下,咱們寫程序時並不知道將須要多少個對象,所以數組固定大小對於編程有些受限。數組

java類庫中提供了一套至關完整的容器類來解決這個問題,其中基本類型有ListQueueSetMap,這些對象類型被稱爲集合類。可是,Java類庫中使用了Collection來指代集合類中的子集{List,Queue,Set},因此集合類也被稱爲容器。容器提供了完善的方法來保存對象。安全

二 類型安全的容器

java採用泛型保證咱們不會向容器中插入不正確的類型,可是java的泛型只存在於程序源碼中,在通過編譯器編譯就會將類型擦除。舉一個例子:函數

//通過編譯前
List<String> list = new ArrayList<>();
list.add("ok");
System.out.println(list.get(0));
 
//通過編譯後
List list = new ArrayList();
list.add("ok");
System.out.println((String)list.get(0));

這樣作的好處是:在編寫程序的時候,不會將其餘非導出類型的對象添加到容器中。優化

三 List

數組存儲多個對象的緣由是它提早聲明瞭能存儲多少對象。那容器又是如何實現存儲不定多對象的呢?this

//ArrayList部分源碼
 
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
 
private transient Object[] elementData;
private int size;
 
public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
}
public ArrayList() {
    super();
    this.elementData = EMPTY_ELEMENTDATA;
}
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
 
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
 
    ensureExplicitCapacity(minCapacity);
}
 
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
 
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
 
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);
}

咱們能夠看到,在ArrayList類中有一個elementData數組。當使用無參構造函數時數組長度默認爲空,當向ArrayList加入對象時,會調用一個方法來判斷數組是否能放下這個對象。當數組爲空時設置數組長度爲10並申請相應大小空間,當數組已滿時,最少從新申請原數組大小1.5倍的空間(除非達到int類型最大值-8)。而在LinkedList中卻沒有采用這種方式,而是採用鏈表方式。spa

//LinkedList add方法
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

在LinkedList中,他的add方法調用了linkLast方法,直接在鏈表後邊加入一個新的節點。操作系統

四 Set

Set類型不保存重複的元素。判斷對象元素是否相等採用的是equals方法,因此在存入自定義的對象時,若是重寫equals方法依賴於可變屬性,將會致使一些問題。線程

五 Map

Map類型是可以將對象映射到其餘對象的一種容器,有區別於List的get方法。HashSet類中包含了一個HashMap對象,HashSet的實現依靠HashMap。

HashMap的實現採用了數組鏈表的方式,即數組的每個位置都存放的是鏈表頭。查找會先經過key的hash找到對應數組下標,再在該數組下標所對應的鏈表中找到是否有對應對象,查找方式爲equals方法。
HashMap若是存儲的鍵值對大於設定值,會自動進行擴容而且對已經存入的鍵值對進行重排序,一次擴容的大小是當前數組長度的兩倍。

//HashMap擴容
public V put(K key, V value) {
    //other...
    addEntry(hash, key, value, i);
    return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
   //other...
}

六 Queue

隊列是一種典型的先進先出的容器,LinkedList實現了Queue接口。PriorityQueue實現了優先級隊列。ArrayDeque是一個用數組實現雙端隊列的類,咱們來看一下ArrayDeque類中的一些方法。

//ArrayDeque構造方法
    public ArrayDeque() {
        elements = (E[]) new Object[16];
    }
    public ArrayDeque(int numElements) {
        allocateElements(numElements);
    }
    private void allocateElements(int numElements) {
        int initialCapacity = MIN_INITIAL_CAPACITY;
        if (numElements >= initialCapacity) {
            initialCapacity = numElements;
            initialCapacity |= (initialCapacity >>>  1);
            initialCapacity |= (initialCapacity >>>  2);
            initialCapacity |= (initialCapacity >>>  4);
            initialCapacity |= (initialCapacity >>>  8);
            initialCapacity |= (initialCapacity >>> 16);
            initialCapacity++;

            if (initialCapacity < 0)   // Too many elements, must back off
                initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
        }
        elements = (E[]) new Object[initialCapacity];
    }

上邊的代碼是ArrayDeque的構造方法,能夠看到,當沒有定義大小時,ArrayDeque默認數組大小爲16,而定義大小後,會調用allocateElements方法,這個方法的做用是:當給定長度小於最小長度8時,使用最小長度。若大於等於最小長度,則找到比給定長度大的最小的2的冪數。爲何要是2的冪數呢?緣由有如下兩點:

  1. 操做系統分配內存的方法使用夥伴系統的話,每一塊的大小都是2的冪數,若是分配的內存大小爲2的冪數,能夠減小內存分配的時間。
    夥伴系統在百度百科中的解釋:http://baike.baidu.com/view/4...

  2. 在ArrayDeque的addFirst方法中不固定將頭放在數組的第一位,而是循環移位。使用2的冪數可以有效判斷頭部所在的地址。

  3. 一樣在第二點中,若是隊列滿了,數組擴充是將容量capacity值左移一位便可擴充一倍。

public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
        elements[head = (head - 1) & (elements.length - 1)] = e;
        if (head == tail)
            doubleCapacity();
    }
private void doubleCapacity() {
        assert head == tail;
        int p = head;
        int n = elements.length;
        int r = n - p; // number of elements to the right of p
        int newCapacity = n << 1;
        if (newCapacity < 0)
            throw new IllegalStateException("Sorry, deque too big");
        Object[] a = new Object[newCapacity];
        System.arraycopy(elements, p, a, 0, r);
        System.arraycopy(elements, 0, a, r, p);
        elements = (E[])a;
        head = 0;
        tail = n;
    }

七 List的選擇

在文章開頭提出了一個問題,數組實現的List快仍是鏈表實現的List快。模擬一下試試:

public static void add()
{
    long start = 0;
    long end = 0;
 
    List<Integer> alist = new ArrayList<>();
    List<Integer> llist = new LinkedList<>();
 
    System.out.println("ArrayList添加1000萬數據所需毫秒數");
    start = System.currentTimeMillis();
    for (int i=0; i<10000000; i++)
    {
        alist.add(i);
    }
    end = System.currentTimeMillis();
    System.out.println(end-start);
 
    System.out.println("LinkedList添加1000萬數據所需毫秒數");
    start = System.currentTimeMillis();
    for (int i=0; i<10000000; i++)
    {
        llist.add(i);
    }
    end = System.currentTimeMillis();
    System.out.println(end-start+"\n");
 
    System.out.println("ArrayList從1000萬數據刪除數據所需毫秒數");
    start = System.currentTimeMillis();
    alist.remove(0);
    alist.remove(2000000);
    alist.remove(4000000);
    alist.remove(6000000);
    alist.remove(8000000);
    alist.remove(9999994);
    end = System.currentTimeMillis();
    System.out.println(end - start);
 
    System.out.println("LinkedList從1000萬數據刪除數據所需毫秒數");
    start = System.currentTimeMillis();
    llist.remove(0);
    llist.remove(2000000);
    llist.remove(4000000);
    llist.remove(6000000);
    llist.remove(8000000);
    llist.remove(9999994);
    end = System.currentTimeMillis();
    System.out.println(end - start+"\n");
 
    System.out.println("ArrayList從1000萬數據查找數據所需毫秒數");
    start = System.currentTimeMillis();
    alist.contains(0);
    alist.contains(2000000);
    alist.contains(4000000);
    alist.contains(6000000);
    alist.contains(8000000);
    alist.contains(10000000);
    end = System.currentTimeMillis();
    System.out.println(end - start);
 
    System.out.println("LinkedList從1000萬數據查找數據所需毫秒數");
    start = System.currentTimeMillis();
    llist.contains(0);
    llist.contains(2000000);
    llist.contains(4000000);
    llist.contains(6000000);
    llist.contains(8000000);
    llist.contains(10000000);
    end = System.currentTimeMillis();
    System.out.println(end - start+"\n");
}

174519_U3te_1983603.png
能夠看到,不管在何種狀況下,數組實現的list都比鏈表快。當我在ArrayList構造方法中設置數組初始大小1000萬時,ArrayLIst添加數據的速度慢了下來,降到5000毫秒左右,因此通常狀況下不須要優化。

總結

簡單容器類圖:
174936_MQGl_1983603.png

174953_SrSG_1983603.png

  1. Java的容器分爲兩類,一類是Collection,一類是Map。collection中包含三種集合類型:Set,List,Queue。

  2. 若是想要set中的數據有序,請使用TreeSet。

  3. HashTableVector是線程安全的,可是不建議使用,請使用java.util.concurrent包下的容器。

  4. HashMap容許key/value值爲null

更多文章:http://blog.gavinzh.com

相關文章
相關標籤/搜索