計算機程式的思維邏輯 (52) - 抽象容器類

swiftma發表於2016-12-04

本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結

計算機程式的思維邏輯 (52) - 抽象容器類

38節51節,我們介紹的都是具體的容器類,上節我們提到,所有具體容器類其實都不是從頭構建的,它們都繼承了一些抽象容器類。這些抽象類提供了容器介面的部分實現,方便了Java具體容器類的實現,理解它們有助於進一步理解具體容器類。

此外,通過繼承抽象類,自定義的類也可以更為容易的實現容器介面。為什麼需要實現容器介面呢?至少有兩個原因:

  • 容器類是一個大家庭,它們之間可以方便的協作,比如很多方法的引數和返回值都是容器介面物件,實現了容器介面,就可以方便的參與進這種協作。
  • Java有一個類Collections,提供了很多針對容器介面的通用演算法和功能,實現了容器介面,就可以直接利用Collections中的演算法和功能。

那,具體都有哪些抽象類?它們都提供了哪些基礎功能?如何進行擴充套件?下面就來探討這些問題。

我們先來看都有哪些抽象類,以及它們與之前介紹的容器類的關係。

抽象容器類

抽象容器類與之前介紹的介面和具體容器類的關係如下圖所示:

計算機程式的思維邏輯 (52) - 抽象容器類
虛線框表示介面,有Collection, List, Set, Queue, Deque和Map。

有六個抽象容器類:

  • AbstractCollection: 實現了Collection介面,被抽象類AbstractList, AbstractSet, AbstractQueue繼承,ArrayDeque也繼承自AbstractCollection (圖中未畫出)。
  • AbstractList:父類是AbstractCollection,實現了List介面,被ArrayList, AbstractSequentialList繼承。
  • AbstractSequentialList:父類是AbstractList,被LinkedList繼承。
  • AbstractMap:實現了Map介面,被TreeMap, HashMap, EnumMap繼承。
  • AbstractSet:父類是AbstractCollection,實現了Set介面,被HashSet, TreeSet和EnumSet繼承。
  • AbstractQueue:父類是AbstractCollection,實現了Queue介面,被PriorityQueue繼承。

下面,我們分別來介紹這些抽象類。

AbstractCollection

功能說明

AbstractCollection提供了Collection介面的基礎實現,具體來說,它實現瞭如下方法:

public boolean addAll(Collection<? extends E> c)
public boolean contains(Object o)
public boolean containsAll(Collection<?> c)
public boolean isEmpty()
public boolean remove(Object o)
public boolean removeAll(Collection<?> c)
public boolean retainAll(Collection<?> c)
public void clear()
public Object[] toArray()
public <T> T[] toArray(T[] a)
public String toString() 
複製程式碼

AbstractCollection又不知道資料是怎麼儲存的,它是如何實現這些方法的呢?它依賴於如下更為基礎的方法:

public boolean add(E e)
public abstract int size();
public abstract Iterator<E> iterator();
複製程式碼

add方法的預設實現是:

public boolean add(E e) {
    throw new UnsupportedOperationException();
}
複製程式碼

丟擲"操作不支援"異常,如果子類集合是不可被修改的,這個預設實現就可以了,否則,必須重寫add方法。addAll方法的實現就是迴圈呼叫add方法。

size方法是抽象方法,子類必須重寫。isEmpty方法就是檢查size方法的返回值是否為0。toArray方法依賴size方法的返回值分配陣列大小。

iterator方法也是抽象方法,它返回一個實現了迭代器介面的物件,子類必須重寫。我們知道,迭代器定義了三個方法:

boolean hasNext();
E next();
void remove();
複製程式碼

如果子類集合是不可被修改的,迭代器不用實現remove方法,否則,三個方法都必須實現。

AbstractCollection中的大部分方法都是基於迭代器的方法實現的,比如contains方法,其程式碼為:

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;
}
複製程式碼

通過迭代器方法迴圈進行比較。再比如retailAll方法,其程式碼為:

public boolean retainAll(Collection<?> c) {
    boolean modified = false;
    Iterator<E> it = iterator();
    while (it.hasNext()) {
        if (!c.contains(it.next())) {
            it.remove();
            modified = true;
        }
    }
    return modified;
}
複製程式碼

也是通過迭代器方法進行迴圈,通過迭代器的remove方法刪除不在引數容器c中的每一個元素。

除了介面中的方法,Collection介面文件建議,每個Collection介面的實現類都應該提供至少兩個標準的構造方法,一個是預設構造方法,另一個接受一個Collection型別的引數。

擴充套件例子

具體如何通過繼承AbstractCollection來實現自定義容器呢?我們通過一個簡單的例子來說明。我們使用在泛型第一節自己實現的動態陣列容器類DynamicArray來實現一個簡單的Collection。

DynamicArray當時沒有實現根據索引新增和刪除的方法,我們先來補充一下,補充程式碼為:

public class DynamicArray<E> {
    //... ..
    public E remove(int index) {
        E oldValue = get(index);
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index + 1, elementData, index,
                    numMoved);
        elementData[--size] = null;
        return oldValue;
    }
    
    public void add(int index, E element) {
        ensureCapacity(size + 1);  
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
}
複製程式碼

基於DynamicArray,我們實現一個簡單的迭代器類DynamicArrayIterator,程式碼為:

public class DynamicArrayIterator<E>  implements Iterator<E>{
    DynamicArray<E> darr;
    int cursor;      
    int lastRet = -1;
    
    public DynamicArrayIterator(DynamicArray<E> darr){
        this.darr = darr;
    }
    
    @Override
    public boolean hasNext() {
         return cursor != darr.size();
    }

    @Override
    public E next() {
        int i = cursor;
        if (i >= darr.size())
            throw new NoSuchElementException();
        cursor = i + 1;
        lastRet = i;
        return darr.get(i);
    }

    @Override
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        darr.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
    }
}    
複製程式碼

程式碼很簡單,就不解釋了,為簡單起見,我們沒有實現實際容器類中的有關檢測結構性變化的邏輯。

基於DynamicArray和DynamicArrayIterator,通過繼承AbstractCollection,我們來實現一個簡單的容器類MyCollection,程式碼為:

public class MyCollection<E> extends AbstractCollection<E> {
    DynamicArray<E> darr;
    
    public MyCollection(){
        darr = new DynamicArray<>();
    }
    
    public MyCollection(Collection<? extends E> c){
        this();
        addAll(c);
    }

    @Override
    public Iterator<E> iterator() {
        return new DynamicArrayIterator<>(darr);
    }

    @Override
    public int size() {
        return darr.size();
    }

    @Override
    public boolean add(E e) {
        darr.add(e);
        return true;
    }
}      
複製程式碼

程式碼很簡單,就是按建議提供了兩個構造方法,並重寫了size, add和iterator方法,這些方法內部使用了DynamicArray和DynamicArrayIterator。

AbstractList

功能說明

AbstractList提供了List介面的基礎實現,具體來說,它實現瞭如下方法:

public boolean add(E e)
public boolean addAll(int index, Collection<? extends E> c)
public void clear()
public boolean equals(Object o)
public int hashCode()
public int indexOf(Object o)
public Iterator<E> iterator()
public int lastIndexOf(Object o)
public ListIterator<E> listIterator()
public ListIterator<E> listIterator(final int index)
public List<E> subList(int fromIndex, int toIndex)
複製程式碼

AbstractList是怎麼實現這些方法的呢?它依賴於如下更為基礎的方法:

public abstract int size();
abstract public E get(int index);
public E set(int index, E element)
public void add(int index, E element)
public E remove(int index)
複製程式碼

size方法與AbstractCollection一樣,也是抽象方法,子類必須重寫。get方法根據索引index獲取元素,它也是抽象方法,子類必須重寫。

set/add/remove方法都是修改容器內容,它們不是抽象方法,但預設實現都是丟擲異常UnsupportedOperationException。如果子類容器不可被修改,這個預設實現就可以了。如果可以根據索引修改內容,應該重寫set方法。如果容器是長度可變的,應該重寫add和remove方法。

與AbstractCollection不同,繼承AbstractList不需要實現迭代器類和相關方法,AbstractList內部實現了兩個迭代器類,一個實現了Iterator介面,另一個實現了ListIterator介面,它們是基於以上的這些基礎方法實現的,邏輯比較簡單,就不贅述了。

擴充套件例子

具體如何擴充套件AbstractList呢?我們來看個例子,也通過DynamicArray來實現一個簡單的List,程式碼為:

public class MyList<E> extends AbstractList<E> {
    private DynamicArray<E> darr;
    
    public MyList(){
        darr = new DynamicArray<>();
    }
    
    public MyList(Collection<? extends E> c){
        this();
        addAll(c);
    }
    
    @Override
    public E get(int index) {
        return darr.get(index);
    }

    @Override
    public int size() {
        return darr.size();
    }

    @Override
    public E set(int index, E element) {
        return darr.set(index, element);
    }

    @Override
    public void add(int index, E element) {
        darr.add(index, element);
    }

    @Override
    public E remove(int index) {
        return darr.remove(index);
    }
}    
複製程式碼

程式碼很簡單,就是按建議提供了兩個構造方法,並重寫了size, get, set, add和remove方法,這些方法內部使用了DynamicArray。

AbstractSequentialList

功能說明

AbstractSequentialList是AbstractList的子類,也提供了List介面的基礎實現,具體來說,它實現瞭如下方法:

public void add(int index, E element)
public boolean addAll(int index, Collection<? extends E> c)
public E get(int index)
public Iterator<E> iterator()
public E remove(int index)
public E set(int index, E element)
複製程式碼

可以看出,它實現了根據索引位置進行操作的get/set/add/remove方法,它是怎麼實現的呢?它是基於ListIterator介面的方法實現的,在AbstractSequentialList中,listIterator方法被重寫為了一個抽象方法:

public abstract ListIterator<E> listIterator(int index)
複製程式碼

子類必須重寫該方法,並實現迭代器介面。

我們來看段具體的程式碼,看get/set/add/remove是如何基於ListIterator實現的,get方法程式碼為:

public E get(int index) {
    try {
        return listIterator(index).next();
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}
複製程式碼

程式碼很簡單,其他方法也都類似,就不贅述了

注意與AbstractList相區別,可以說,雖然AbstractSequentialList是AbstractList的子類,但實現邏輯和用法上,與AbstractList正好相反

  • AbstractList需要具體子類重寫根據索引操作的方法get/set/add/remove,它提供了迭代器,但迭代器是基於這些方法實現的。它假定子類可以高效的根據索引位置進行操作,適用於內部是隨機訪問型別的儲存結構(如陣列),比如ArrayList就繼承自AbstractList。
  • AbstractSequentialList需要具體子類重寫迭代器,它提供了根據索引操作的方法get/set/add/remove,但這些方法是基於迭代器實現的。它適用於內部是順序訪問型別的儲存結構(如連結串列),比如LinkedList就繼承自AbstractSequentialList。

擴充套件例子

具體如何擴充套件AbstractSequentialList呢?我們還是以DynamicArray舉例來說明,在實際應用中,如果內部儲存結構類似DynamicArray,應該繼承AbstractList,這裡主要是演示其用法。

擴充套件AbstractSequentialList需要實現ListIterator,前面介紹的DynamicArrayIterator只實現了Iterator介面,通過繼承DynamicArrayIterator,我們實現一個新的實現了ListIterator介面的類DynamicArrayListIterator,程式碼如下:

public class DynamicArrayListIterator<E>  
    extends DynamicArrayIterator<E> implements ListIterator<E>{
    
    public DynamicArrayListIterator(int index, DynamicArray<E> darr){
        super(darr);
        this.cursor = index;
    }
    
    @Override
    public boolean hasPrevious() {
        return cursor > 0;
    }

    @Override
    public E previous() {
        if (!hasPrevious())
            throw new NoSuchElementException();
        cursor--;
        lastRet = cursor;
        return darr.get(lastRet);
    }

    @Override
    public int nextIndex() {
        return cursor;
    }

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

    @Override
    public void set(E e) {
        if(lastRet==-1){
             throw new IllegalStateException();
        }
        darr.set(lastRet, e);
    }

    @Override
    public void add(E e) {
        darr.add(cursor, e);
        cursor++;
        lastRet = -1;
    }
}      
複製程式碼

邏輯比較簡單,就不解釋了,有了DynamicArrayListIterator,我們看基於AbstractSequentialList的List實現,程式碼如下:

public class MySeqList<E> extends AbstractSequentialList<E> {
    private DynamicArray<E> darr;
    
    public MySeqList(){
        darr = new DynamicArray<>();
    }
    
    public MySeqList(Collection<? extends E> c){
        this();
        addAll(c);
    }
    
    @Override
    public ListIterator<E> listIterator(int index) {
        return new DynamicArrayListIterator<>(index, darr);
    }

    @Override
    public int size() {
        return darr.size();
    }
}    
複製程式碼

程式碼很簡單,就是按建議提供了兩個構造方法,並重寫了size和listIterator方法,迭代器的實現是DynamicArrayListIterator。

AbstractMap

功能說明

AbstractMap提供了Map介面的基礎實現,具體來說,它實現瞭如下方法:

public void clear()
public boolean containsKey(Object key)
public boolean containsValue(Object value)
public boolean equals(Object o)
public V get(Object key)
public int hashCode()
public boolean isEmpty()
public Set<K> keySet()
public void putAll(Map<? extends K, ? extends V> m)
public V remove(Object key)
public int size()
public String toString()
public Collection<V> values()
複製程式碼

AbstractMap是如何實現這些方法的呢?它依賴於如下更為基礎的方法:

public V put(K key, V value)
public abstract Set<Entry<K,V>> entrySet();
複製程式碼

putAll就是迴圈呼叫put。put方法的預設實現是丟擲異常UnsupportedOperationException,如果Map是允許寫入的,則需要重寫該方法。

其他方法都基於entrySet,entrySet是一個抽象方法,子類必須重寫,它返回所有鍵值對的Set檢視,這個Set實現類不應該支援add或remove方法,但如果Map是允許刪除的,這個Set的迭代器實現類,即entrySet().iterator()的返回物件,必須實現迭代器的remove方法,這是因為AbstractMap的remove方法是通過entrySet().iterator().remove()實現的。

除了提供基礎方法的實現,AbstractMap類內部還定義了兩個公有的靜態內部類,表示鍵值對:

AbstractMap.SimpleEntry implements Entry<K,V>
AbstractMap.SimpleImmutableEntry implements Entry<K,V>
複製程式碼

SimpleImmutableEntry用於表示只讀的鍵值對,而SimpleEntry用於表示可寫的。

Map介面文件建議,每個Map介面的實現類都應該提供至少兩個標準的構造方法,一個是預設構造方法,另一個接受一個Map型別的引數。

擴充套件例子

具體如何擴充套件AbstractMap呢?我們定義一個簡單的Map實現類MyMap,內部還是用DynamicArray:

public class MyMap<K, V> extends AbstractMap<K, V> {
    private DynamicArray<Map.Entry<K, V>> darr;
    private Set<Map.Entry<K, V>> entrySet = null;

    public MyMap() {
        darr = new DynamicArray<>();
    }

    public MyMap(Map<? extends K, ? extends V> m) {
        this();
        putAll(m);
    }

    @Override
    public Set<Entry<K, V>> entrySet() {
        Set<Map.Entry<K, V>> es = entrySet;
        return es != null ? es : (entrySet = new EntrySet());
    }

    @Override
    public V put(K key, V value) {
        for (int i = 0; i < darr.size(); i++) {
            Map.Entry<K, V> entry = darr.get(i);
            if ((key == null && entry.getKey() == null)
                    || (key != null && key.equals(entry.getKey()))) {
                V oldValue = entry.getValue();
                entry.setValue(value);
                return oldValue;
            }
        }
        Map.Entry<K, V> newEntry = new AbstractMap.SimpleEntry<>(key, value);
        darr.add(newEntry);
        return null;
    }

    class EntrySet extends AbstractSet<Map.Entry<K, V>> {
        public Iterator<Map.Entry<K, V>> iterator() {
            return new DynamicArrayIterator<Map.Entry<K, V>>(darr);
        }

        public int size() {
            return darr.size();
        }
    }
}
複製程式碼

我們定義了兩個構造方法,實現了put和entrySet方法。

put方法先通過迴圈查詢是否已存在對應的鍵,如果存在,修改值,否則新建一個鍵值對(型別為AbstractMap.SimpleEntry)並新增。

entrySet返回的型別是一個內部類EntrySet,它繼承自AbstractSet,重寫了size和iterator方法,iterator方法中,返回的是迭代器型別是DynamicArrayIterator,它支援remove方法。

AbstractSet

AbstractSet提供了Set介面的基礎實現,它繼承自AbstractCollection,增加了equals和hashCode方法的預設實現。Set介面要求容器內不能包含重複元素,AbstractSet並沒有實現該約束,子類需要自己實現。

擴充套件AbstractSet與AbstractCollection是類似的,只是需要實現無重複元素的約束,比如,add方法內需要檢查元素是否已經新增過了。具體實現比較簡單,我們就不贅述了。

AbstractQueue

AbstractQueue提供了Queue介面的基礎實現,它繼承自AbstractCollection,實現瞭如下方法:

public boolean add(E e)
public boolean addAll(Collection<? extends E> c)
public void clear()
public E element()
public E remove()
複製程式碼

這些方法是基於Queue介面的其他方法實現的,包括:

E peek();
E poll();
boolean offer(E e);
複製程式碼

擴充套件AbstractQueue需要實現這些方法,具體邏輯也比較簡單,我們就不贅述了。

小結

本節介紹了Java容器類中的抽象類AbstractCollection, AbstractList, AbstractSequentialList, AbstractSet, AbstractQueue以及AbstractMap,介紹了它們與容器介面和具體類的關係,對每個抽象類,介紹了它提供的基礎功能,是如何實現的,並舉例說明了如何進行擴充套件。

前面我們提到,實現了容器介面,就可以方便的參與到容器類這個大家庭中進行相互協作,也可以方便的利用Collections這個類實現的通用演算法和功能。

但Collections都實現了哪些演算法和功能?都有什麼用途?如何使用?內部又是如何實現的?有何參考價值?讓我們下一節來探討。


未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。

計算機程式的思維邏輯 (52) - 抽象容器類

相關文章