Java集合類之介面學習
一、前言
在Java中使用介面能規範實現該介面的類該實現的功能,介紹Java集合類的介面有助於對Java集合整體、對不同場景該使用什麼樣的集合有個明確的認識,對於學習Java開發的人來說,Java標準庫集合的學習是必經之路,所以自今天起,我打算每天從原始碼和文件兩個方向來了解以及在這裡記錄學習的過程。
PS:在學習之前我往往會在網際網路中四處收集資料,這可能是個壞習慣…當我看到別人部落格中對Java集合總結的特別詳細時,我自己就會產生惰性不想"重複造輪子"再對其總結一次。但別人理解透徹了我看一遍是達不到同樣的效果的,所以我還是打算自己總結一次,也順便鍛鍊鍛鍊表達能力。另外,別人的總結我也會把連結放在這裡,如果有更好的博主,希望大家能推薦推薦:
Java學習部落格1:
二、Java集合的介紹
Java的集合框架可以分為兩大支系,分別為Collection(收集器\集合)和Map(對映),並不是因為它們是完全不相干的東西,而是因為它們在資料儲存和查詢這一方面的確有很大的差別,所以集合框架分成了兩個介面來實現。 先開局一張介面繼承關係圖: 沒錯,這張圖就是Java核心技術卷1第十版中P352頁的內容,這張圖中只涵蓋了最常用基本的集合介面,在之後的學習過程中也會圍繞以上介面的實現來進行。首先對該圖中所有的介面一一介紹: PS:在此之後Java對並行支援更加完善之後新增了Concurrent*系列介面,這些介面暫不在本次學習的內容中。
2.1、遍歷工具介面
Iterable介面學習
public interface Iterable<T>{
Iterator\<T> iterator(); //返回一個泛型迭代器
default void forEach(Consumer<? super T> action); //引數為一個函式介面.用於函式式遍歷可迭代集
default Spliterator\<T> spliterator(); //可分割迭代器,用於並行遍歷
}
小結:可以看出該介面的職能是:繼承該介面的類必須能返回自己的迭代器,由於之後JDK更新出現了lambda表示式,對並行的支援更加完善等,該介面又多瞭如上兩個預設方法,其中forEach通用性很強,實現該介面的類一般可以直接使用預設方法。但可分割迭代器的分割能力較差,一般得重新實現一遍以提供更好的效能。
Iterator介面學習
public interface Iterator<E> {
//是否有下一個元素
boolean hasNext();
//返回下一個元素,如果到達末尾則返回NoSushElementException異常
E next();
//預設方法是丟擲異常,需要重新實現,作用是刪除上一個next返回的元素
default void remove()
/*
*預設方法,引數為函式式介面,表示接受單個輸入引數且不返回結果的操作
*該方法作用為:遍歷集合內的剩餘元素,也就是當前迭代器後面的元素
*/
default void forEachRemaining(Consumer<? super E> action)
}
小結:要注意Iterable和Iterator的區別,一個是"可迭代的"另一個是"迭代器",他們之間比較類似的方法就是forEach和forEachRemaining了,引數一致,行為幾乎也是一致。但他們是有區別的。在Iterable中是對"可迭代的"元素進行全體遍歷直到結束或發生異常,在"迭代器"中該方法是對該迭代器之後的元素進行遍歷。 不能直接連續呼叫remove方法,remove方法的實現和next方法具有依賴性,如果要刪除連續的兩個元素得在每次呼叫remove之前呼叫next方法來移動迭代器。 還需要注意的一點是,進行for-each迴圈時若直接使用物件的remove方法可能丟擲異常,在使用迭代器進行元素遍歷時若涉及到元素列表的更改應該使用迭代器的remove方法來更改元素列表。
ListIterator介面學習
//該介面繼承於Iterator並且有如下 新增 方法:
public interface ListIterator<E> extends Iterator<E> {
//是否有上一個元素
boolean hasPrevious();
//返回上一個元素,如果到達初始則返回NoSushElementException異常
E previous();
//返回後續呼叫 next 時返回元素的索引,如果列表迭代器位於列表的末尾,則返回列表大小
int nextIndex();
//返回後續呼叫 previous 時返回元素的索引,如果列表迭代器位於列表的開頭,則返回-1
int previousIndex();
//作用更新:用於刪除上次呼叫next或previous方法時所返回的元素
void remove();
/*
*將上次呼叫next方法或previous方法所返回的元素進行替換
*如果next或previous後對錶結構修改後呼叫該方法則返回異常
*/
void set(E e);
//在迭代器位置之前插入元素
void add(E e);
}
小結:列表迭代器的適用範圍比迭代器更小,它是明確規定用於列表這種線性資料結構的,所以它在用於線性資料結構時對於原始迭代器來說擁有更完整精確的迭代器方法。 對比Iterator多了向前遍歷(previous)的功能,以及獲取前後元素索引的功能,remove方法也隨之變成了刪除上次迭代器返回的元素。還增加了設定元素和新增元素的方法。
RandomAccess介面學習
public interface RandomAccess {
}
小結:該介面屬於標記介面,能高效進行隨機訪問的Collection實現應該實現該標機介面。 小結:一般底層使用陣列實現的集合類都需要實現該介面以便讓遍歷或查詢的實現能更加高效
2.2 、集合介面
Collection介面學習
//該介面繼承於 Iterable<E>並且有如下新增方法:
public interface Collection<E> extends Iterable<E> {
------查詢類--------------------------
int size(); //返回集合中元素的個數
boolean isEmpty(); //判斷集合是否為空
boolean contains(Object o); //判斷是否包含該元素
boolean equals(Object o); //判斷集合是否相等
int hashCode(); //返回該集合的hash值
boolean containsAll(Collection<?> c); //如果傳參集合中包含指定集合的所有元素,則返回true。
\--------------------------------------
------修改類--------------------------
boolean add(E e); //新增元素,成功更改集合內容則返回true
boolean remove(Object o); //刪除元素,成功則返回true
void clear(); //從此集合中刪除所有元素
boolean addAll(Collection<? extends E> c); //將傳參集合所有元素追加至該集合
boolean removeAll(Collection<?> c); //將此集合中包含傳參集合中的元素全部刪除
boolean retainAll(Collection<?> c); //僅保留此集合中包含在引數集合中的元素
default boolean removeIf(Predicate<? super E> filter) //根據引數描述的條件刪除元素
\--------------------------------------
------轉換類--------------------------
Object[] toArray(); //返回為Object陣列
<T> T[] toArray(T[] a); //傳入型別陣列引數,將集合返回到型別陣列
default Stream<E> stream() //返回集合的序列流
default Stream<E> parallelStream() //返回集合的並行流
default Spliterator<E> spliterator() // 預設方法,繼承於Iterable,在該介面中對其預設實現進行了修改
\--------------------------------------
}
小結:該介面是後續所有具體集合的父介面,所以它所擁有的特徵即是之後絕大部分集合所都必須擁有的特徵,它所需實現的方法可以分類如上,前兩類都是比較好理解和實踐的分類,需要特別說明的就是最後的轉換類方法了。所謂轉換類方法也就是將集合轉換為另一種表達形式的方法。 首先討論兩種不同的 toArray(); 從方法簽名中就能很清楚的看出倆toArray()僅返回的型別不同罷了,要返回正確型別的陣列就得傳入正確型別的引數才行,比如:ArrayList<Sring>,如果該方法直接呼叫toArray()方法,很明顯會返回Object型別陣列。該方法在設計上是因為Java是用型別擦除來實現泛型的,基本型別Java能用隱式強制型別轉換來達成泛型目的,而陣列則無法直接進行強制型別轉換了,同時集合介面並不知道集合類中儲存的是何種型別,型別是否一致,如果使用原型集合類來儲存元素就能儲存一切型別元素了。而該集合也只能由Object陣列來儲存。所以設計了該方法。 後面的toArray方法很明顯將型別處理成了一致型別,引數的作用是:如果傳入的陣列引數長度足夠則將集合內容複製到該陣列中,多餘的長度賦值為空,最後返回該陣列的引用。如果陣列引數長度不夠則只參考傳入引數的型別來構造長度與集合長度一致的陣列複製並返回其引用。 上面的實現步驟是參考自ArrayList的實現,不過介面的行為應該是一致的,所以該實現所造成的行為可以推廣到其餘實現中。
後面的三種方法都是來自於後面Java對併發程式的支援,首先 stream方法,該方法在後續路線至ArrayList類中都沒有重新實現,所以它們所用的版本都是Collection中的預設實現:
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
該方法進入後又呼叫了StreamSupport類中的方法,該方法使用集合的splierator()方法產生的可分割迭代器 和 是否並行(false為序列流,true為並行流) 作為引數返回一個流。繼續深入該部分則涉及到了Java對於流以及並行的設計,所以該方法的介紹就此打住了。
parallelStream方法就如同stream方法的實現,僅將false改為了true,具體原始碼如下:
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), true);
}
spliterator方法返回一個可分割迭代器,是產生並行流所需用到的引數。這裡也不過多討論可分割迭代器的實現和用法,只簡要介紹一下概念:
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
從原始碼跟蹤開始分析,該方法使用最終類Spliterators中的spliterator傳入了this和0,跟蹤進入該程式碼即可得知:
public static <T> Spliterator<T> spliterator(Collection<? extends T> c,
int characteristics) {
return new IteratorSpliterator<>(Objects.requireNonNull(c),
characteristics);
}
該方法會返回一個IteratorSpliterator物件,該物件以Spliterator介面作為控制代碼,返回給方法呼叫者,要弄清楚該迭代器的作用也就得從該介面和具體的實現類上手了,這裡暫時不打算細講。 IteratorSpliterator是該介面的內部類,建構函式的任務是初始化集合的引用以及集合的特徵(用int表示以位區分)。可分割迭代器相對於普通迭代器在概念上主要僅多了個可分割的功能,也就是能使用trySplit方法對可分割迭代器根據特徵進行分割,使原迭代器和產生的新的可分割迭代器各自處理分割後的一塊內容。
List介面學習
//該介面繼承於Collection<E>並且有如下新增方法:
public interface List<E> extends Collection<E> {
------查詢類--------------------------
E get(int index); //返回該列表索引處的元素
int indexOf(Object o); //返回該列表中出現該物件時的第一個索引
int lastIndexOf(Object o); //返回該列表中出現該物件時的最後一個索引
\--------------------------------------
------修改類--------------------------
void add(int index, E element); //在列表索引index處增加元素element
E remove(int index); //刪除列表index處的元素
E set(int index, E element); //設定列表index處的元素為element
default void sort(Comparator<? super E> c) //根據傳入的比較器引數對列表元素進行排序
default void replaceAll(UnaryOperator<E> operator) //對列表的每一個元素根據舊值生成新值
\--------------------------------------
------轉換類--------------------------
ListIterator<E> listIterator(); //返回該列表的列表迭代器
List<E> subList(int fromIndex, int toIndex); //返回該列表[fromIndex, toIndex)的子列表
\--------------------------------------
小結:該介面繼承於Collection,所以父介面的部分我都省略掉了,在新增的具體化的方法中需要重點注意只有subList方法,該方法返回的列表並不是新的列表,而是對原列表的某部分的引用,所以我們將之稱之為"檢視",在子列表檢視中操作元素將會把更改反映到原列表,需要特別注意的是如果操作原列表改變了其列表結構(比如增刪元素),子列表檢視將會作廢。 子列表作廢的檢測是一種叫做 快速失敗迭代器 的操作完成的,關鍵變數為modCount,代表使用迭代器修改該列表的次數,需要快速失敗機制的列表實現每次操作列表時都會檢查該變數,如果該變數的值意外更改則會丟擲異常。 舉例: A為具體列表,B為A的子列表檢視,如果A呼叫了修改列表結構的方法則會更新A的modCount但不會更新B的modCount,這時B呼叫操作列表的方法就會因為丟擲異常而失敗。 而如果B呼叫修改列表結構的方法,由於B修改列表結構實際上是在檢查範圍等值的基礎上呼叫的A的方法和做一些善後處理,所以A和B的modCount都會改變,這樣一來A和B兩個列表都不會出現問題。
最後可能需要說一次的replaceAll方法,該方法和Collection介面中的removeIf方法類似,傳入的都是函式式介面引數,由方法簽名很容易知道其用途,實現也僅僅是以前匿名內部類的改進,由於以前想傳遞方法引數就得傳遞一個只包含一個方法的物件,這樣寫程式碼十分繁瑣,所以才出現了函式式介面。該replaceAll方法的作用通過觀察其原始碼很容易發現是通過函式式引數根據列表舊值改變所有元素的方法。
Set介面學習
//該介面繼承於Collection<E>並且有如下新增方法:
public interface Set<E> extends Collection<E>{
// 該介面方法與Collection方法簽名完全一樣,但描述不同,方法的具體行為有更加嚴謹的定義
}
小結:該介面所擁有的方法簽名是完全等同於Collection的,但方法的行為有更加嚴謹的定義,比如:
- add 方法不允許新增重複元素
- 要適當定義 equals 方法
- 只要兩個集包含同樣的元素就認為是相等的而不要求其順序
- hashCode方法的定義要保證包含相同元素的兩個集會得到相同的雜湊碼。
- Set迭代器元素的返回不要求保證其順序性
再討論,為什麼方法一樣還要定義一個相同的介面呢?明顯的是Set是Collection的子概念,擁有更完整和嚴謹的定義,就如同編譯原理中的文法等級一樣,層次依次遞增,定義越加嚴謹的。從Collection的子介面中可以看出,要將連結串列,佇列,集的共性抽象出一個父介面,根據木桶原理也只能從最簡潔的集開始下手。將Set與Collection分離以後就能方便的編寫適合於所有Collection型別的或者Set型別的方法了,否則需要編寫只適合Set而不適合Collection的方法將很困難。
Queue介面學習
//該介面繼承於Collection<E>並且有如下新增方法:
public interface Queue<E> extends Collection<E>{
------查詢類--------------------------
E element(); //檢索佇列頭部,如果佇列為空則丟擲異常
E peek(); //檢索佇列頭部,如果佇列為空則返回null
\--------------------------------------
------修改類--------------------------
boolean offer(E e); //新增元素到佇列,和add的區別是add因為容量不夠會丟擲異常,該方法只返回false
E remove(); //檢查並刪除返回佇列頭部,如果佇列為空則丟擲異常
E poll(); //檢查並刪除返回佇列頭部,如果佇列為空則返回null
\--------------------------------------
}
小結:作為Collection的三大子介面該介面實現的佇列是比較容易理解的,該介面對比Collection新增了不同的查詢/新增和刪除元素的方法,根據其需求在佇列為空時有返回null和丟擲異常兩種操作方式。 可能會有這樣的疑問,為什麼同樣功能的方法僅僅因為一丁點兒特殊情況就需要兩種不同的方法簽名呢?我個人認為這樣是為了使佇列能適用於各種不同的情況(可能有點廢話),要弄清楚這一點可以先從 “丟擲異常” 和 返回null 的區別開始分析,因為該類方法僅在佇列為空時的返回動作有所不同。 丟擲異常,代表的是正常執行時不該出現的情形,通常異常都會有其處理程式來使系統在區域性出錯的情況下仍然能恢復健康。 而返回null值,在正常情況下通常都作為程式跳轉的一個條件,在不為null時使用A方法處理,在為null時使用B方法處理。 然而,實際上,兩種情況都是可以相互替代的,都是起到一個在佇列為空的情況下做出特別處理的一個作用。 在JDK標準文件中描述的是:佇列的方法通常會有兩種形式,在操作失敗時丟擲異常和返回特殊值。通常後一種形式的插入專用於操作容量有限的佇列實現;
還需要注意的是因為 null 是介面方法規定的特殊返回值,所以佇列最好不要存入null作為資料項。
Deque介面學習
//該介面繼承於Queue<E>並且有如下新增方法:
public interface Deque<E> extends Queue<E> {
------查詢類--------------------------
E getFirst();
E getLast();
E peekFirst();
E peekLast();
/*如果佇列非空,則返回佇列頭部或尾部元素,如果佇列為空,則前倆方法丟擲異常,後倆方法返回null*/
\--------------------------------------
------修改類--------------------------
void addFirst(E e);
void addLast(E e);
boolean offerFirst(E e);
boolean offerLast(E e);
/*將給定物件新增到雙端佇列的頭部或尾部,如果佇列滿了,前兩個方法丟擲異常,後兩個方法返回false*/
E removeFirst();
E removeLast();
E pollFirst();
E pollLast();
/*如果佇列不空,刪除並返回佇列頭部或尾部元素。如果佇列為空,前倆方法丟擲異常,後倆方法返回null*/
boolean removeFirstOccurrence(Object o);
//從雙端佇列中刪除第一次出現的指定元素,如果雙端佇列不包含該元素,則不會更改
//如果此雙端佇列包含指定元素,則返回true
void push(E e); //將元素入棧,此方法等同於addFirst
E pop(); //元素出棧,刪除並返回此雙端佇列的第一個元素。
\--------------------------------------
------轉換類--------------------------
Iterator<E> descendingIterator(); //返回一個反向迭代器
\--------------------------------------
}
小結:該介面繼承於Queue,並且有反向迭代器方法,所以對比原佇列介面可以從正反兩個方向遍歷該佇列,也就是雙端佇列,因為棧是雙端佇列的一個特例,所以該介面中包含入棧出棧的方法,可以當做棧用。 該雙端佇列除了要實現原有於Queue的方法之外,還增加了操作兩端的方法,當然,也是同時對佇列為空時提供了兩種方法。 這裡可能會有人有疑問,原來的檢視/增加/刪除方法不正好對應雙端佇列一端的情況嗎?為什麼還要新增這麼多方法呢?關於這點,可能是設計該介面人員的方法簽名強迫症,介面繼承繼承了原先不嚴謹,不清楚的名字,所以需要為它更名。比如offerLast方法,就對應於原Queue介面中的offer方法,它們是完全等效的,僅名稱不同。所以offerLast方法的實現一般都是這樣的:
public boolean offer(E e) {
return offerLast(e);
}
這種方法我們稱之為橋方法,僅使這個方法簽名和原有的方法起到一個橋接的作用。名稱風格一致的方法簽名更易於使用,程式碼可讀性也越高。所以這樣做是有價值的。
注意:老版本Java中使用棧所用的Stack類是繼承於Vector的,而Vertor是遺留的執行緒安全類,效率不高,所以現代程式要使用棧時一般使用Deque。需要執行緒安全也可以使用相關的包將現存的非執行緒安全集合轉換為執行緒安全的。
SortedSet介面學習
//該介面繼承於Set<E>並且有如下新增方法:
public interface SortedSet<E> extends Set<E>{
------查詢類--------------------------
Comparator<? super E> comparator(); //返回一個比價器,如果使用自然排序則返回null
E first(); //返回此集合中第一個元素。
E last(); //返回此集合中最後一個元素。
\--------------------------------------
------修改類--------------------------
// 使用Set/Collection介面中的方法修改
\--------------------------------------
------轉換類--------------------