《OnJava8》精讀(五) 集合

在這裏插入圖片描述

介紹


《On Java 8》是什麼?

它是《Thinking In Java》的作者Bruce Eckel基於Java8寫的新書。裏面包含了對Java深入的理解及思想維度的理念。可以比作Java界的“武學祕籍”。任何Java語言的使用者,甚至是非Java使用者但是對面向對象思想有興趣的程序員都該一讀的經典書籍。目前豆瓣評分9.5,是公認的編程經典。

爲什麼要寫這個系列的精讀博文?

由於書籍讀起來時間久,過程漫長,因此產生了寫本精讀系列的最初想法。除此之外,由於中文版是譯版,讀起來還是有較大的生硬感(這種差異並非譯者的翻譯問題,類似英文無法譯出唐詩的原因),這導致我們理解作者意圖需要一點推敲。再加上原書的內容很長,只第一章就多達一萬多字(不含代碼),讀起來就需要大量時間。

所以,如果現在有一個人能替我們先仔細讀一遍,篩選出其中的精華,讓我們可以在地鐵上或者路上不用花太多時間就可以瞭解這邊經典書籍的思想那就最好不過了。於是這個系列誕生了。

一些建議

推薦讀本書的英文版原著。此外,也可以參考本書的中文譯版。我在寫這個系列的時候,會盡量的保證以“陳述”的方式表達原著的內容,也會寫出自己的部分觀點,但是這種觀點會保持理性並儘量少而精。本系列中對於原著的內容會以引用的方式體現。
最重要的一點,大家可以通過博客平臺的評論功能多加交流,這也是學習的一個重要環節。

第十二章 集合


本章總字數:19000

關鍵詞:

  • 集合的概念
  • List
  • Set
  • Map
  • 隊列
  • 集合與迭代器

如果你讀過本系列的前幾篇博文,你會發現本篇(第五篇)是唯一一個專注講一個知識點的。前幾篇都是以概括一系列內容爲主,而這一篇只講集合。原因很簡單——因爲Java中的集合是如此的重要。

由於原著的本章節內容講解太過偏向理論且知識點比較分散。所以本章將通過我自己的總結來闡述。但是知識是相通的。如果需要可以參讀原著內容。

集合的由來

在OOP的相關內容中曾經講過,我們可以使用 new關鍵詞來創建任意一個對象。有些時候,我們可能需要同一種類型的一系列對象,比如多個 String對象。用以前的知識,我們可以使用數組。

但是數組也有自己的侷限性——它的數量是固定的。這樣就引出了另一個問題,我們在不清楚一個對象的個數時,如何來創建?

這是一個很常見的問題,比如:學校今天要接待一些學生,如果你知道學生的數量是10位可以這麼做:

class Student{}
...
Student[] students=new Student[10];

但是如果你不知道具體數量,只知道會來一些學生,這麼做就不合理。這時候就需要一個新概念,一個非固定長度的一系列對象——集合。

集合的概念

在說集合概念前需要先提一下另外一個概念——泛型。泛型指代某一種固定類型,在與集合搭配後可以限制傳入集合的對象類型。在之後的章節有專門的泛型相關講解。

通過使用泛型,就可以在編譯期防止將錯誤類型的對象放置到集合中。

Java中集合的概念分爲兩種——集合與映射。

  • 集合(Collection) :一個獨立元素的序列,這些元素都服從一條或多條規則。List 必須以插入的順序保存元素, Set 不能包含重複元素, Queue 按照排隊規則來確定對象產生的順序(通常與它們被插入的順序相同)。
  • 映射(Map) : 一組成對的“鍵值對”對象,允許使用鍵來查找值。 ArrayList 使用數字來查找對象,因此在某種意義上講,它是將數字和對象關聯在一起。 map 允許我們使用一個對象來查找另一個對象,它也被稱作關聯數組(associative array),因爲它將對象和其它對象關聯在一起;或者稱作字典(dictionary),因爲可以使用一個鍵對象來查找值對象,就像在字典中使用單詞查找定義一樣。 Map 是強大的編程工具。

集合的關係(圖片來自百度)

集合的關係(圖片來自百度)

如圖,Collection接口下的都是集合,Map接口下的都是映射。而這兩個接口下又分別有不同的接口繼承。下面就來逐一詳細介紹。

列表List

List是一系列有序的對象集合。 List分爲兩種: ArrayList LinkedList

  • 基本的 ArrayList ,擅長隨機訪問元素,但在 List 中間插入和刪除元素時速度較慢。
  • LinkedList ,它通過代價較低的在 List 中間進行的插入和刪除操作,提供了優化的順序訪問。LinkedList 對於隨機訪問來說相對較慢,但它具有比 ArrayList 更大的特徵集。

接下來我們來看一下的源碼:

public interface List<E> extends Collection<E> {
....

List是一個繼承自Collection的接口, List在後者的基礎上加入了不少拓展方法,比如:

void add(int index, E element);

這些拓展方法豐富了原有接口的內容。

ArrayList是一個繼承自抽象類AbstractList並實現了 List、RandomAccess、Cloneable等接口的類。這裏需要着重提一下Serializable接口,一個類只有實現了Serializable才能被序列化。Serializable接口的內部是空的,沒有任何方法,它的作用更多的是給類作出序列化的“標記”。

在C#中,這種方式被優化爲了[Serializable]的類標記,而不需要專門繼承。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...

LinkedList 中,也繼承了List、Serializable,但是對比 ArrayList你會發現一些區別。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
...

比如在接口的繼承中, ArrayList繼承了RandomAccess接口,而 LinkedList 繼承了Deque接口。參照官方說明文檔,RandomAccess接口和Serializable接口一樣是一個標記接口(它的內部也同樣沒有任何方法),它用來標記隨機快速訪問實現。也就是說它的作用是讓 ArrayList的訪問速度得以提升,但是犧牲了插入數據與刪除數據的速度。

相反的,由於 LinkedList 繼承了Deque(Deque繼承自Queue接口),使得其數據的查詢速度變慢但是增刪的速度變快。

所以總結之後我們發現:
繼承自 List的兩個集合:ArrayList、LinkedList,他們都是有序的。但是 ArrayList更擅長查詢,而 LinkedList 增刪速度更快

瞭解了這些有助於我們在項目開發時選擇更合適的集合。

再回過頭看學生的例子,我們就可以這麼做:

List<Student> list = new ArrayList<Student>();
list.add(new Student());
        
List<Student> list2 = new LinkedList<Student>();
list2.add(new Student());

集合Set

首先,與 List最大的不同是, Set不允許存儲重複的值

首先看源碼,與 List一致, Set也繼承自Collection接口:

public interface Set<E> extends Collection<E> {
...

再來看看HashSet 的實現:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable{
    ...

HashSet 與上文ArrayList及LinkedList繼承關係類似,但是少了關於RandomAccess和Deque的繼承。

至於 TreeSet,它的區別就更大了,少了Set接口的直接繼承,而是繼承了NavigableSet接口:

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable

不過NavigableSet本身是繼承自SortedSet,而SortedSet繼承自Set接口。

public interface NavigableSet<E> extends SortedSet<E> {
...

SortedSet的存在,這也就導致了TreeSet與HashSet最大的不同——TreeSet 可以排序的。

可以排序不等於一定有序,只是意味着你可以爲你的自定義類型實現排序的功能。只有你實現了排序規則,它才能按照規則排序。Comparator (比較器)排序相關內容會在後期的章節詳細介紹。

再來說說 LinkedHashSet,很明顯它是HashSet的派生類:

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
    ...

雖然是派生類,但是兩者的數據存儲方式有一些不同。

HashSet 使用了散列。由 HashSet 維護的順序與 TreeSet 或 LinkedHashSet 不同,因爲它們的實現具有不同的元素存儲方式。
TreeSet 將元素存儲在紅-黑樹數據結構中,而 HashSet 使用散列函數。 LinkedHashSet 因爲查詢速度的原因也使用了散列,但是看起來使用了鏈表來維護元素的插入順序。

上文提到了 Set是不允許存儲重複數據的,但是如果我強制插入呢(大霧)?

        Set<Integer> set2=new HashSet<Integer>();
        set2.add(100);
        set2.add(100);

        Set<Integer> set3=new TreeSet<Integer>();
        set3.add(100);
        set3.add(100);

結果:
在這裏插入圖片描述
不會報錯,只是結果不會出現重複的數據。

映射Map

Map接口下主要有HashMap TreeMapLinkedHashMap 。與 List Set不同的是, Map由一系列<key,value>鍵值對構成,且key不能重複。

我們來統一看一遍源碼中的繼承關係:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    ...
public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
    ...
public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>{
    ...

有了之前的經驗,我們從源碼能看得出 TreeMap似乎與 TreeSet更相似, HashMap HashSet更相似(從他們命名上你也能看出一些規律)。比如, TreeMap繼承了NavigableMap接口,而 TreeSet繼承了NavigableSet接口。這兩個接口都與排序相關(所以很明顯的,TreeMap同樣支持排序)。

有了鍵值對的概念,我們再次回頭看學生的例子,比如我們可以爲學生進行編號(學號是唯一的):

Map<String,Student> map=new HashMap<String,Student>();
map.put("001",new Student());
map.put("002",new Student());

在很多情況下,集合與映射可以搭配使用。比如,我們現在要爲學生分配班級:

Set<Student> students=new HashSet<Student>();
students.add(new Student());
students.add(new Student());
students.add(new Student());

Set<Student> students2=new HashSet<Student>();
students2.add(new Student());
students2.add(new Student());
students2.add(new Student());

Map<String, Set<Student>> map = new HashMap<String, Set<Student>>();
map.put("一班", students);
map.put("二班", students2);

集合的其他知識

在原著中,作者用不少篇幅解釋了隊列、迭代器的知識。

隊列是一個典型的“先進先出”(FIFO)集合。 即從集合的一端放入事物,再從另一端去獲取它們,事物放入集合的順序和被取出的順序是相同的。LinkedList 實現了 Queue 接口,並且提供了一些方法以支持隊列行爲,因此 LinkedList 可以用作 Queue 的一種實現。

出於對隊列“先進先出”的理解,我們可以做出嘗試:

        Set<Integer> set2 = new HashSet<Integer>();
        set2.add(1);
        set2.add(3);
        set2.add(2);

        Set<Integer> set3 = new TreeSet<Integer>();
        set3.add(1);
        set3.add(3);
        set3.add(2);

        Set<Integer> set4 = new LinkedHashSet<Integer>();
        set4.add(1);
        set4.add(3);
        set4.add(2);

結果:
在這裏插入圖片描述
結果與預期相同,LinkedHashSet的結果是按照插入數據的方式展現的。而HashSet和TreeSet則不一樣。

迭代器是一個對象,它在一個序列中移動並選擇該序列中的每個對象,而客戶端程序員不知道或不關心該序列的底層結構。另外,迭代器通常被稱爲輕量級對象(lightweight object):創建它的代價小。因此,經常可以看到一些對迭代器有些奇怪的約束。例如,Java 的 Iterator 只能單向移動。這個 Iterator 只能用來:

  • 使用 iterator() 方法要求集合返回一個 Iterator。 Iterator 將準備好返回序列中的第一個元素。
  • 使用 next() 方法獲得序列中的下一個元素。
  • 使用 hasNext() 方法檢查序列中是否還有元素。
  • 使用 remove() 方法將迭代器最近返回的那個元素刪除。

總結

本篇是本系列很重要的一部分內容。對實際編程影響最大,使用也是最多的。幾乎在任何時候我們都會遇到集合的使用。深入瞭解每個集合的特點,我們才能清楚在不同場景下如何選擇合適的集合類型。

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