JAVA集合框架探究(一)

JAVA集合框架探究(一)

集合框架是日常開發中使用最多的,但是我對它還一知半解。在具體應該選擇哪個容器使用時往往不能
確定,因爲對它的實現細節不夠了解。所以準備通過查看文檔和源碼的方式對每個集合框架加深理解。
首先會從總體框架上進行梳理,然後再具體到每個集合類進行分析。

一、概述

一般存放一系列相同的已知對象時,我們會使用數組,但開發過程中,大部分情況下對象的數量都無法確定。所以無法使用數組的形式去存儲,這時就需要使用集合的形式存儲對象,集合總體來說就是存儲一系列相同對象的容器,根據不同的特性(如順序、是否重複等)又分爲不同的類型。

集合和數組的區別

  • 數組長度是固定的,需要在初始化時指定。集合不需要指定長度。
  • 數組可以保存對象和基本數組類型,集合只能保存對象的引用,基本數據類型需要經過裝箱後才能存儲。

JAVA的集合類根據用途,大致可以分爲兩種。一種是保存元素的序列,並且提供了序列中增刪元素的方法,另一種是保存鍵值對對象,像字典的形式一樣可以通過一個對象來查找映射表中的另一個對象。這兩種類型也分別是由兩個接口派生,Collecion和Map。相關類圖比較複雜,爲了方便理解,一個接口一個接口來分析。

1、Collection

先看一張Collection接口。

可以看到Collection接口裏面的方法:

Collection接口是最基本的集合接口。我們平時使用的ArrayList、HashSet等集合類分別實現了List、Set、Queue接口,而Collection是這些接口的抽象,包含了它們的通用方法。

對Collection的遍歷有三種方式,第一種是foreach,第二種因爲Collection繼承了迭代接口Iterable,所以可以使用迭代器進行遍歷。第三種是JDK8的新特性,對集合的流式操作(Stream),這是對集合功能的增強,寫法使用lambda表達式,可以以非常簡單的語法對集合進行各種聚合和批量處理操作,感興趣的話可以瞭解一下。

Collection接口沒有實現類,只有派生的三個接口:Set、Queue、List。

  • List:List顧名思義,可以存放有序的元素,它可以通過索引去查找某個元素。
  • Set:Set與List的區別是其中的元素是不可重複的,也是無序的。

2、List

List相關類圖。

List接口除了繼承的Collection接口裏的方法,又添加了一些方法,主要可以分爲以下幾類。

  • 搜索相關方法。

     // Search Operations
    
    int indexOf(Object o);
    
    int lastIndexOf(Object o);
    

    在集合中查找指定元素的位置。

  • 位置相關方法。

    // Positional Access Operations
        
    E get(int index);
    
    E set(int index, E element);
    
    void add(int index, E element);
    
    E remove(int index);	
    

    如get、set、add、remove方法,參數都是index,因爲list是有序集合,可以像數組一樣根據位置進行元素操作。

  • ListIterator。List迭代器。

    // List Iterators
    
    ListIterator<E> listIterator();
    

    List迭代器比Iterator增加了添加元素、向前遍歷、定位索引、修改元素等方法,在List子類中有相關實現。

  • 範圍操作。

    List<E> subList(int fromIndex, int toIndex);
    

3、Set

Set相關類圖。

Set接口方法沒有對Collection接口進行擴展,在此不再贅述。

4、Queue

Queue相關類圖。

Queue是隊列,是一種先入先出的結構(FIFO),在數據結構學習中經常使用。
Queue接口除了繼承Collection的方法,還定義了隊列的常規操作方法。如下:

	offer(E e);  //入隊操作
  
    E remove(); //移除隊頭元素  
  
    E poll();  //移除隊頭元素  
  
    E element(); //獲取隊頭元素  
  
    E peek();  //獲取隊頭的元素

可以看到remove/poll、element/peek方法作用是相同的,那有什麼區別呢?在註釋裏寫了這點,This method differs from {@link #poll poll} only in that it throws an exception if this queue is empty.當隊列爲空時remove會拋出異常,poll會返回空。element/peek的區別也是這樣。element()方法會拋出異常。

5、Map

Map相關類圖。

Map接口中除了我們經常使用的put、get、remove等方法,有幾個方法比較有意思。

	// Views

    Collection<V> values();

其中,values()返回所有的value,使用了Collection,所以我們在使用values()方法時返回的是個Collection。值是可重複的。

 Set<K> keySet();

keySet()返回值是所有的key,這樣可以遍歷Set獲取key,再通過get獲取所有的value。因爲使用Set存儲,所以key是不可重複的。

Set<Map.Entry<K, V>> entrySet();

entrySet()在遍歷Map時經常使用,它返回的是包含映射關係的Set。其中Entry是Map接口中的一個內部接口,有如下方法:

        K getKey();

        V getValue();

        V setValue(V value);

        boolean equals(Object o);

entry是存放在Set中,所以entry是不可重複的。

另外,還有HashTable繼承於Dictionary類,也實現了Map接口,但Dictionary類已經標記爲廢棄了,所以不再研究。

二、AbstractCollection源碼解析

從上面List和Set的類圖中可以看出,所有實現類幾乎都繼承自AbstractCollection這個抽象類,AbstractCollection是Collection唯一的直接實現類,實現了Collection接口裏的大部分方法。在此列舉幾個比較重要的方法。

1. contains(Object o)

    public boolean contains(Object o) {
        Iterator<E> it = iterator();
        if (o==null) {
            while (it.hasNext())
                if (it.next()==null)
                    return true;
        } else {
            while (it.hasNext())
                if (o.equals(it.next()))
                    return true;
        }
        return false;
    }

可以看到遍歷方式是使用迭代器,並且在元素爲空時也可以查找。

2. toArray()

    public Object[] toArray() {
        // Estimate size of array; be prepared to see more or fewer elements
        Object[] r = new Object[size()];
        Iterator<E> it = iterator();
        for (int i = 0; i < r.length; i++) {
            if (! it.hasNext()) // fewer elements than expected
                return Arrays.copyOf(r, i);
            r[i] = it.next();
        }
        return it.hasNext() ? finishToArray(r, it) : r;
    }

可以看到需要先調用size()方法創建一個數量和集合數量相同的數組,然後遍歷將集合元素引用複製到數組,如果集合中元素比數組大小少,則調用Arrays.copyOf()方法來截取數組並返回新數組,如果集合中元素比數組大小多,則會調用finishToArray調整數組大小並返回新數組。這裏對數組大小和集合數量進行比較是爲了考慮在toArray()期間修改了原集合元素的情況。

    private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
        int i = r.length;
        while (it.hasNext()) {
            int cap = r.length;
            if (i == cap) {
                int newCap = cap + (cap >> 1) + 1;
                // overflow-conscious code
                if (newCap - MAX_ARRAY_SIZE > 0)
                    newCap = hugeCapacity(cap + 1);
                r = Arrays.copyOf(r, newCap);
            }
            r[i++] = (T)it.next();
        }
        // trim if overallocated
        return (i == r.length) ? r : Arrays.copyOf(r, i);
    }

此方法的核心是對數組擴容,當數組元素滿了時使用Arrays.copyOf方法對數組擴容。擴容的大小爲newCap = cap + (cap >> 1) + 1。使用移位運算符,相當於增加了原大小的一半再加一,並且如果調整後帶下超過MAX_ARRAY_SIZE數組最大值時,要使用hugeCapacity()方法調整大小。
數組最大值爲:

    /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

數組最大值是int最大值-8的原因在註釋裏寫明瞭,有些虛擬機會在數組中保留一些頭關鍵字,爲了防止內存溢出所以小一些。
再看一下hugeCapacity()方法

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError
                ("Required array size too large");
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

遍歷完成之後再使用Arrays.copyOf截取數組。

3. toArray(T[] a)

	public <T> T[] toArray(T[] a) {
        // Estimate size of array; be prepared to see more or fewer elements
        int size = size();
        T[] r = a.length >= size ? a :
                  (T[])java.lang.reflect.Array
                  .newInstance(a.getClass().getComponentType(), size);
        Iterator<E> it = iterator();

        for (int i = 0; i < r.length; i++) {
            if (! it.hasNext()) { // fewer elements than expected
                if (a == r) {
                    r[i] = null; // null-terminate
                } else if (a.length < i) {
                    return Arrays.copyOf(r, i);
                } else {
                    System.arraycopy(r, 0, a, 0, i);
                    if (a.length > i) {
                        a[i] = null;
                    }
                }
                return a;
            }
            r[i] = (T)it.next();
        }
        // more elements than expected
        return it.hasNext() ? finishToArray(r, it) : r;
    }

這個方法和無參toArray()方法的區別是有一個數組作爲參數,作用是將集合中的元素填入數組中。

  • 首先比較參數數組a和集合大小,使用其中較大地值來構造數組r(a長度較大時直接使用a)。
  • 使用迭代器進行遍歷,將元素轉爲數組類型存入數組。
  • 在集合元素和數組長度不一致時,可以看到有兩種情況(一和三是一種結果)。如果數組長度較大,則剩餘位置都設置null。如果集合長度大於數組,則Arrays.copyOf進行截取。
  • 最後如果數組已滿,則和上面的方法一樣,通過finishToArray()進行擴容。

4.add()

	public boolean add(E e) {
        throw new UnsupportedOperationException();
    }

add方法會拋出異常。

5.抽象方法

    public abstract Iterator<E> iterator();

    public abstract int size();

二、AbstractList源碼解析

AbstractList實現了List接口,繼承自AbstractCollection,是所有List集合類的父類,我們來看一下它有哪些方法。

1、沒有實現的方法

    public boolean add(E e) {
        add(size(), e);
        return true;
    }
	
	 public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    
    abstract public E get(int index);

    public E set(int index, E element) {
        throw new UnsupportedOperationException();
    }

    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    
    public E remove(int index) {
        throw new UnsupportedOperationException();
    }

這幾個方法都是直接拋出異常。

2、indexOf()

    public int indexOf(Object o) {
        ListIterator<E> it = listIterator();
        if (o==null) {
            while (it.hasNext())
                if (it.next()==null)
                    return it.previousIndex();
        } else {
            while (it.hasNext())
                if (o.equals(it.next()))
                    return it.previousIndex();
        }
        return -1;
    }

    public int lastIndexOf(Object o) {
        ListIterator<E> it = listIterator(size());
        if (o==null) {
            while (it.hasPrevious())
                if (it.previous()==null)
                    return it.nextIndex();
        } else {
            while (it.hasPrevious())
                if (o.equals(it.previous()))
                    return it.nextIndex();
        }
        return -1;
    }

indexOf和lastIndexOf,都使用了listIterator進行遍歷,並且可以查詢Null的元素。
因爲listIterator是雙向迭代器,所以lastIndexOf可以從後向前進行遍歷。

3、addAll()

    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
        boolean modified = false;
        for (E e : c) {
            add(index++, e);
            modified = true;
        }
        return modified;
    }
    
    private void rangeCheckForAdd(int index) {
        if (index < 0 || index > size())
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

addAll方法首先檢查index是否越界,然後再挨個添加元素。

4. iterator()

	  public Iterator<E> iterator() {
	        return new Itr();
	  }
     private class Itr implements Iterator<E> {
        /**
         * Index of element to be returned by subsequent call to next.
         */
        int cursor = 0;

        /**
         * Index of element returned by most recent call to next or
         * previous.  Reset to -1 if this element is deleted by a call
         * to remove.
         */
        int lastRet = -1;

        /**
         * The modCount value that the iterator believes that the backing
         * List should have.  If this expectation is violated, the iterator
         * has detected concurrent modification.
         */
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size();
        }

        public E next() {
            checkForComodification();
            try {
					int i = cursor;//當前標記位                
					E next = get(i);//通過列表的get函數獲得元素                
					lastRet = i;//記錄老的標記位                
					cursor = i + 1;//標記位+1
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

iterator()是AbstractCollection中沒有實現的抽象方法,在AbstractList中實現了該方法,Itr是單向迭代器,繼承了Iterator接口。其中

  • cursor指迭代器當前位置。
  • lastRet指迭代器上次位置。
  • expectedModCount指迭代期間集合被修改的次數。
  • next()方法中首先通過get方法獲取值,然後cursor標誌加一,以此進行迭代。
  • 發生IndexOutOfBoundsException異常時,首先會通過checkForComodification()檢查迭代期間集合是否發生了變化,如果是則會先拋出併發修改異常。
  • remove()方法中會先判斷lastRet是否小於0,所以remove只能刪除已經迭代過的元素。

5.listIterator()

 	public ListIterator<E> listIterator() {
        return listIterator(0);
    }

    public ListIterator<E> listIterator(final int index) {
        rangeCheckForAdd(index);

        return new ListItr(index);
    }
    
    private class ListItr extends Itr implements ListIterator<E> {
    	// 通過索引初始化迭代器
        ListItr(int index) {
            cursor = index;
        }
		 // 是否有前一個元素
        public boolean hasPrevious() {
            return cursor != 0;
        }

		// 返回前一個元素
        public E previous() {
            checkForComodification();
            try {
                int i = cursor - 1;
                E previous = get(i);
                lastRet = cursor = i;
                return previous;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        public int nextIndex() {
            return cursor;
        }

        public int previousIndex() {
            return cursor-1;
        }

        public void set(E e) {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.set(lastRet, e);
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        public void add(E e) {
            checkForComodification();

            try {
                int i = cursor;
                AbstractList.this.add(i, e);
                lastRet = -1;
                cursor = i + 1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
    }

listIterator是Iterator的功能增強版,從方法上就可以看出。listIterator除了無參還有一個有參方法,以index爲參數,提供的是通過索引位置初始化的迭代器。

  • 以索引位置初始化迭代器的方式是把cursor初始爲index.
  • 判斷有沒有前一個元素的方式是判斷cursor是否爲0。
  • add新元素後會使lastRet失效。

6.subList()

subList方法是返回原列表的一部分,也就是list的子列表.

    public List<E> subList(int fromIndex, int toIndex) {
        return (this instanceof RandomAccess ?
                new RandomAccessSubList<>(this, fromIndex, toIndex) :
                new SubList<>(this, fromIndex, toIndex));
    }

代碼中可以看到,如果List實現了RandomAccess接口,則返回RandomAccessSubList,否則返回SubList。這兩個類的源碼都在AbstractList類中,後面會進行分析。其中會說明爲什麼sublist方法只是返回原list的一部分的映射。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章