【java集合梳理】— 從集合接口框架說起

(一) java集合分類

之前大概分爲三種,SetListMap三種,JDK5之後,增加Queue.主要由CollectionMap兩個接口衍生出來,同時Collection接口繼承Iterable接口,所以我們也可以說java裏面的集合類主要是由IterableMap兩個接口以及他們的子接口或者其實現類組成。我們可以認爲Collection接口定義了單列集合的規範,每次只能存儲一個元素,而Map接口定義了雙列集合的規範,每次能存儲一對元素。

  • Iterable接口:主要是實現遍歷功能
    • Collection接口: 允許重複
      • Set接口:無序,元素不可重複,訪問元素只能通過元素本身來訪問。
      • List接口:有序且可重複,可以根據元素的索引來訪問集合中的元素。
      • Queue接口:隊列集合
  • Map接口:映射關係,簡單理解爲鍵值對<Key,Value>,Key不可重複,與Collection接口關係不大,只是個別函數使用到。
    整個接口框架關係如下(來自百度百科):

(1) Iterable接口

1. 內部定義的方法

java集合最源頭的接口,實現這個接口的作用主要是集合對象可以通過迭代器去遍歷每一個元素。

源碼如下:

// 返回一個內部元素爲T類型的迭代器(JDK1.5只有這個接口)
Iterator<T> iterator();

// 遍歷內部元素,action意思爲動作,指可以對每個元素進行操作(JDK1.8添加)
default void forEach(Consumer<? super T> action) {}

// 創建並返回一個可分割迭代器(JDK1.8添加),分割的迭代器主要是提供可以並行遍歷元素的迭代器,可以適應現在cpu多核的能力,加快速度。
default Spliterator<T> spliterator() {
    return Spliterators.spliteratorUnknownSize(iterator(), 0);
}

從上面可以看出,foreach迭代以及可分割迭代,都加了default關鍵字,這個是Java 8 新的關鍵字,以前接口的所有接口,具體子類都必須實現,而對於deafult關鍵字標識的方法,其子類可以不用實現,這也是接口規範發生變化的一點。
下面我們分別展示三個接口的調用:

1.1 iterator方法

public static void iteratorHasNext(){
    List<String> list=new ArrayList<String>();
    list.add("Jam");
    list.add("Jane");
    list.add("Sam");
    // 返回迭代器
    Iterator<String> iterator=list.iterator();
    // hashNext可以判斷是否還有元素
    while(iterator.hasNext()){
        //next()作用是返回當前指針指向的元素,之後將指針移向下個元素
        System.out.println(iterator.next());
    }
}

當然也可以使用for-each loop方式遍歷

for (String item : list) {
    System.out.println(item);
}

但是實際上,這種寫法在class文件中也是會轉成迭代器形式,這只是一個語法糖。class文件如下:

public class IterableTest {
    public IterableTest() { }
    public static void main(String[] args) {
        iteratorHasNext();
    }
    public static void iteratorHasNext() {
        List<String> list = new ArrayList();
        list.add("Jam");
        list.add("Jane");
        list.add("Sam");
        Iterator<String> iterator = list.iterator();
        Iterator var2 = list.iterator();
        while(var2.hasNext()) {
            String item = (String)var2.next();
            System.out.println(item);
        }
    }
}

需要注意的一點是,迭代遍歷的時候,如果刪除或者添加元素,都會拋出修改異常,這是由於快速失敗【fast-fail】機制。

    public static void iteratorHasNext(){
        List<String> list=new ArrayList<String>();
        list.add("Jam");
        list.add("Jane");
        list.add("Sam");
        for (String item : list) {
            if(item.equals("Jam")){
                list.remove(item);
            }
            System.out.println(item);
        }
    }

從下面的錯誤我們可以看出,第一個元素是有被打印出來的,也就是remove操作是成功的,只是遍歷到第二個元素的時候,迭代器檢查,發現被改變了,所以拋出了異常。

Jam
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at IterableTest.iteratorHasNext(IterableTest.java:15)
	at IterableTest.main(IterableTest.java:7)

1.2 forEach方法

其實就是把對每一個元素的操作當成了一個對象傳遞進來,對每一個元素進行處理。

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
```java
當然像ArrayList自然也是有自己的實現的,那我們就可以使用這樣的寫法,簡潔優雅。forEach方法在java8中參數是`java.util.function.Consumer`,可以稱爲**消費行爲**或者說**動作**類型。
```java
list.forEach(x -> System.out.print(x));

同時,我們只要實現Consumer接口,就可以自定義動作,如果不自定義,默認迭代順序是按照元素的順序。

public class ConsumerTest {
    public static void main(String[] args) {
        List<String> list=new ArrayList<String>();
        list.add("Jam");
        list.add("Jane");
        list.add("Sam");
        MyConsumer myConsumer = new MyConsumer();
        Iterator<String> it = list.iterator();
        list.forEach(myConsumer);
    }
    static class MyConsumer implements Consumer<Object> {
        @Override
        public void accept(Object t) {
            System.out.println("自定義打印:" + t);
        }

    }

}

輸出的結果:

自定義打印:Jam
自定義打印:Jane
自定義打印:Sam

1.3 spliterator方法

這是一個爲了並行遍歷數據元素而設計的迭代方法,返回的是Spliterator,是專門並行遍歷的迭代器。以發揮多核時代的處理器性能,java默認在集合框架中提供了一個默認的Spliterator實現,底層也就是Stream.isParallel()實現的,我們可以看一下源碼:

    // stream使用的就是spliterator
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, 0);
    }
    public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
        Objects.requireNonNull(spliterator);
        return new ReferencePipeline.Head<>(spliterator,
                                            StreamOpFlag.fromCharacteristics(spliterator),
                                            parallel);
    }

使用的方法如下:

    public static void spliterator(){
        List<String> list = Arrays.asList("1", "2", "3","4","5","6","7","8","9","10");
        // 獲取可迭代器
        Spliterator<String> spliterator = list.spliterator();
        // 一個一個遍歷
        System.out.println("tryAdvance: ");
        spliterator.tryAdvance(item->System.out.print(item+" "));
        spliterator.tryAdvance(item->System.out.print(item+" "));
        System.out.println("\n-------------------------------------------");

        // 依次遍歷剩下的
        System.out.println("forEachRemaining: ");
        spliterator.forEachRemaining(item->System.out.print(item+" "));
        System.out.println("\n------------------------------------------");

        // spliterator1:0~10
        Spliterator<String> spliterator1 = list.spliterator();
        // spliterator1:6~10 spliterator2:0~5
        Spliterator<String> spliterator2 = spliterator1.trySplit();
        // spliterator1:8~10 spliterator3:6~7
        Spliterator<String> spliterator3 = spliterator1.trySplit();
        System.out.println("spliterator1: ");
        spliterator1.forEachRemaining(item->System.out.print(item+" "));
        System.out.println("\n------------------------------------------");
        System.out.println("spliterator2: ");
        spliterator2.forEachRemaining(item->System.out.print(item+" "));
        System.out.println("\n------------------------------------------");
        System.out.println("spliterator3: ");
        spliterator3.forEachRemaining(item->System.out.print(item+" "));
    }
  • tryAdvance() 一個一個元素進行遍歷
  • forEachRemaining() 順序地分塊遍歷
  • trySplit()進行分區形成另外的 Spliterator,使用在並行操作中,分出來的是前面一半,就是不斷把前面一部分分出來

結果如下:

tryAdvance: 
1 2 
-------------------------------------------
forEachRemaining: 
3 4 5 6 7 8 9 10 
------------------------------------------
spliterator1: 
8 9 10 
------------------------------------------
spliterator2: 
1 2 3 4 5 
------------------------------------------
spliterator3: 
6 7 

還有一些其他的用法在這裏就不列舉了,主要是trySplit()之後,可以用於多線程遍歷。理想的時候,可以平均分成兩半,有利於並行計算,但是不是一定平分的。

2. Collection接口 extend Iterable

Collection接口可以算是集合類的一個根接口之一,一般不能夠直接使用,只是定義了一個規範,定義了添加,刪除等管理數據的方法。繼承Collection接口的有ListSet,Queue,不過Queue定義了自己的一些接口,相對來說和其他的差異比較大。

2.1 內部定義的方法

源碼如下:

boolean add(Object o)    //添加元素

boolean remove(Object o)  //移除元素

boolean addAll(Collection c) //批量添加

boolean removeAll(Collection c)  //批量移除

void retainAll(Collection c)   // 移除在c中不存在的元素

void clear()  //清空集合

int size()   //集合大小

boolean isEmpty()    //是否爲空

boolean contains(Object o)    //是否包含在集合中

boolean containsAll(Collection c)    //是否包含所有的元素

Iterator<E> iterator()    // 獲取迭代器

Object[] toArray()	  // 轉成數組

default boolean removeIf(Predicate<? super E> filter) {} // 刪除集合中複合條件的元素,刪除成功返回true

boolean equals(Object o)

int hashCode()

default Spliterator<E> spliterator() {} //獲取可分割迭代器

default Stream<E> stream() {}   //獲取流

default Stream<E> parallelStream() {} //獲取並行流

裏面獲取並行流的方法parallelStream(),其實就是通過默認的ForkJoinPool(主要用來使用分治法(Divide-and-Conquer Algorithm)來解決問題),提高多線程任務的速度。我們可以使用ArrayList來演示一下平行處理能力。例如下面的例子,輸出的順序就不一定是1,2,3…,可能是亂序的,這是因爲任務會被分成多個小任務,任務執行是沒有特定的順序的。

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
list.parallelStream()
       .forEach(out::println);

2.2 繼承Collection的主要接口

Collection
List-有順序,可重複
LinkedList-使用鏈表實現,線程不安全
ArrayList-數組實現,線程不安全
Vector-數組實現,線程安全
Stack-堆棧,先進後出
Set-不可重複,內部排序
HashSet-hash表存儲
LinkHashSet-鏈表維護插入順序
TreeSet-二叉樹實現,排序
Queue-隊列,先進先出
2.2.1 List extend Collection

繼承於Collection接口,有順序,取出的順序與存入的順序一致,有索引,可以根據索引獲取數據,允許存儲重複的元素,可以放入爲null的元素。
最常見的三個實現類就是ArrayListVector,LinkedListArrayListVector都是內部封裝了對數組的操作,唯一不同的是,Vector是線程安全的,而ArrayList不是,理論上ArrayList操作的效率會比Vector好一些。

裏面是接口定義的方法:

int size();  //獲取大小

boolean isEmpty();  //判斷是否爲空

boolean contains(Object o);  //是否包含某個元素

Iterator<E> iterator(); //獲取迭代器

Object[] toArray();  // 轉化成爲數組(對象)

<T> T[] toArray(T[] a);  // 轉化爲數組(特定位某個類)

boolean add(E e); //添加

boolean remove(Object o);  //移除元素

boolean containsAll(Collection<?> c); // 是否包含所有的元素

boolean addAll(Collection<? extends E> c); //批量添加

boolean addAll(int index, Collection<? extends E> c); //批量添加,指定開始的索引

boolean removeAll(Collection<?> c); //批量移除

boolean retainAll(Collection<?> c); //將c中不包含的元素移除

default void replaceAll(UnaryOperator<E> operator) {}//替換

default void sort(Comparator<? super E> c) {}// 排序

void clear();//清除所有的元素

boolean equals(Object o);//是否相等

int hashCode(); //計算獲取hash值

E get(int index); //通過索引獲取元素

E set(int index, E element);//修改元素

void add(int index, E element);//在指定位置插入元素

E remove(int index);//根據索引移除某個元素

int indexOf(Object o);  //根據對象獲取索引

int lastIndexOf(Object o); //獲取對象元素的最後一個元素

ListIterator<E> listIterator(); // 獲取List迭代器

ListIterator<E> listIterator(int index); // 根據索引獲取當前的位置的迭代器

List<E> subList(int fromIndex, int toIndex); //截取某一段數據

default Spliterator<E> spliterator(){} //獲取可切分迭代器

上面的方法都比較簡單,值得一提的是裏面出現了ListIterator,這是一個功能更加強大的迭代器,繼承於Iterator,只能用於List類型的訪問,拓展功能例如:通過調用listIterator()方法獲得一個指向List開頭的ListIterator,也可以調用listIterator(n)獲取一個指定索引爲n的元素的ListIterator,這是一個可以雙向移動的迭代器。
操作數組索引的時候需要注意,由於List的實現類底層很多都是數組,所以索引越界會報錯IndexOutOfBoundsException
說起List的實現子類:

  • ArrayList:底層存儲結構是數組結構,增加刪除比較慢,查找比較快,是最常用的List集合。線程不安全。
  • LinkedList:底層是鏈表結構,增加刪除比較快,但是查找比較慢。線程不安全。
  • Vector:和ArrayList差不多,但是是線程安全的,即同步。
2.2.2 Set extend Collection

Set接口,不允許放入重複的元素,也就是如果相同,則只存儲其中一個。

下面是源碼方法:

int size(); //獲取大小

boolean isEmpty();  //是否爲空
 
boolean contains(Object o); //是否包含某個元素

Iterator<E> iterator(); //獲取迭代器

Object[] toArray(); //轉化成爲數組

<T> T[] toArray(T[] a); //轉化爲特定類的數組

boolean add(E e);   //添加元素

boolean remove(Object o);   //移除元素

boolean containsAll(Collection<?> c);   //是否包含所有的元素

boolean addAll(Collection<? extends E> c);  //批量添加

boolean retainAll(Collection<?> c); //移除所有不存在於c集合中的元素

boolean removeAll(Collection<?> c); //移除所有在c集合中存在的元素

void clear();   //清空集合

boolean equals(Object o);   //是否相等

int hashCode(); //計算hashcode

default Spliterator<E> spliterator() {}     //獲取可分割迭代器
        

主要的子類:

  • HashSet
    • 允許空值
    • 通過HashCode方法計算獲取hash值,確定存儲位置,無序。
  • LinkedHashSet
    • HashSet的子類
    • 有順序
  • TreeSet
    • 如果無參數構建Set,則需要實現Comparable方法。
    • 亦可以創建時傳入比較方法,用於排序。
2.2.3 Queue extend Collection

隊列接口,在Collection接口的接觸上添加了增刪改查接口定義,一般默認是先進先出,即FIFO,除了優先隊列和棧,優先隊列是自己定義了排序的優先順序,隊列中不允許放入null元素。

下面是源碼:

boolean add(E e);   //插入一個元素到隊列,失敗時返回IllegalStateException (如果隊列容量不夠)

boolean offer(E e); //插入一個元素到隊列,失敗時返回false

E remove(); //移除隊列頭的元素並移除

E poll();   //返回並移除隊列的頭部元素,隊列爲空時返回null

E element();    //返回隊列頭元素

E peek();   //返回隊列頭部的元素,隊列爲空時返回null

主要的子接口以及實現類有:

  • Deque(接口):Queue的子接口,雙向隊列,可以從兩邊存取
    • ArrayDeque:Deque的實現類,底層用數組實現,數據存貯在數組中
  • AbstractQueue:Queue的子接口,僅實現了add、remove和element三個方法
    • PriorityQueue:按照默認或者自己定義的順序來排序元素,底層使用堆(完全二叉樹)實現,使用動態數組實現,
  • BlockingQueue: 在java.util.concurrent包中,阻塞隊列,滿足當前無法處理的操作。

(2) Map接口

  • 定義雙列集合的規範Map<K,V>,每次存儲一對元素,即key和value。
  • key的類型可以和value的類型相同,也可以不同,任意的引用類型都可以。
  • key是不允許重複的,但是value是可以重複的,所謂重複是指計算的hash值系統。

下面的源碼的方法:

V put(K key, V value);  // 添加元素

V remove(Object key);   // 刪除元素

void putAll(Map<? extends K, ? extends V> m); // 批量添加

void clear()  // 移除所有元素

V get(Object key);  // 通過key查詢元素

int size();    // 查詢集合大小

boolean isEmpty();    // 集合是否爲空

boolean containsKey(Object key);     // 是否包含某個key
    
boolean containsValue(Object value);     // 是否包含某個value

Set<K> keySet(); // 獲取所有key的set集合

Collection<V> values(); // 獲取所有的value的set集合

Set<Map.Entry<K, V>> entrySet();    // 返回鍵值對的set,每一個鍵值對是一個entry對象

boolean equals(Object o);   // 用於比較的函數

int hashCode(); // 計算hashcode

default V getOrDefault(Object key, V defaultValue) // 獲取key對應的Value,沒有則返回默認值()
            
default void forEach(BiConsumer<? super K, ? super V> action) {}  // 遍歷

default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {}  // 批量替換

// 缺少這個key的時候纔會添加進去
// 返回值是是key對應的value值,如果不存在,則返回的是剛剛放進去的value
default V putIfAbsent(K key, V value) {} 

default boolean remove(Object key, Object value) {}  // 移除元素

default boolean replace(K key, V oldValue, V newValue) {}   // 替換

default V replace(K key, V value) {}  //替換

// 和putIfAbsent有點像,只不過傳進去的mappingFunction是映射函數,也就是如果不存在key對應的value,將會執行函數,函數返回值會被當成value添加進去,同時返回新的value值
default V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction) {} 
// 和computeIfAbsent方法相反,只有key存在的時候,纔會執行函數,並且返回
default V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {}

// 不管如何都會執行映射函數,返回value            
default V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {}
    
default V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remappingFunction) {}

值得注意的是,Map裏面定義了一個Entry類,其實就是定義了一個存儲數據的類型,一個entry就是一個<key,value>.
Map的常用的實現子類:

  • HashMap:由數組和鏈表組成,線程不安全,無序。
  • LinkedHashMap:如果我們需要是有序的,那麼就需要它,時間和空間效率沒有HashMap那麼高,底層是維護一條雙向鏈表,保證了插入的順序。
  • ConcurrentHashMap:線程安全,1.7JDK使用鎖分離,每一段Segment都有自己的獨立鎖,相對來說效率也比較高。JDK1.8拋棄了Segment,使用Node數組+鏈表和紅黑樹實現,在線程安全控制上使用Synchronize和CAS,可以認爲是優化的線程安全的HashMap。
  • HashTable:對比與HashMap主要是使用關鍵字synchronize,加上同步鎖,線程安全。

(二)總結

這些集合原始接口到底是什麼?爲什麼需要?

我想,這些接口其實都是一種規則/規範的定義,如果不這麼做也可以,所有的子類自己實現,但是從迭代以及維護的角度來說,這就是一種抽象或者分類,比如定義了Iterator接口,某一些類就可以去繼承或者實現,那就得遵守這個規範/契約。可以有所拓展,每個子類的拓展不一樣,所以每個類就各有所長,但是都有一箇中心,就是原始的集合接口。比如實現Map接口的所有類的中心思想都不變,只是各有所長,各分千秋,形成了大千集合世界。

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