Java基礎教程(24)--集合

一.Java集合框架

  集合,有時也稱爲容器,是一個用來存儲和管理多個元素的對象。Java中的集合框架定義了一套規範,用來表示和操作集合,使具體操作與實現細節解耦。集合框架都包含下列內容:

  • 接口:這些是表示集合的抽象數據類型,它們定義了集合中常見的操作。
  • 實現:爲各種集合提供了具體的實現。
  • 算法:這些是對實現集合接口的對象執行有用計算(如搜索和排序)的方法。算法被認爲是多態的:也就是說,相同的方法可以用於集合接口的不同實現。

  集合框架有以下優點:

  • 減少編程工作:通過提供有用的數據結構和算法,集合可以讓我們專注於程序的重要部分,而不是考慮如何實現集合。
  • 提高程序速度和質量:集合框架提供了各種集合的高性能、高質量實現,使得我們在編寫代碼時不必過多地考慮程序的速度和性能。
  • 促進代碼重用:如果我們要實現自己的集合,只需要實現集合框架中的標準接口,就可以應用集合框架提供的算法,而無需重複地“造輪子”。

二.接口

  下圖是集合框架中的核心接口:

  Java中的集合框架分爲Collection和Map兩類。下面是這兩個接口的聲明:

public interface Collection<E> extends Iterable<E> {...}
public interface Map<K,V> {...}

  可以看到,Java中的集合都是泛型的。在聲明集合時,應該指定集合中包含的對象類型。這樣編譯器可以幫我們驗證放入集合中的對象類型是否正確,從而減少運行時的錯誤。
  現在對上圖中的接口進行介紹:

  • Collection:集合層次結構的根接口。集合表示一組被稱爲元素的對象,不同的集合有不同的特點。例如,有些集合允許存在重複的元素,有些則不允許;有些集合的元素是有序的,有些則是無序的。這個接口定義了所有集合都應該具有的行爲,至於這些特性行爲則交給子接口去定義。Java平臺不提供這個接口的直接實現,而是提供它的子接口的實現。
  • Set:不能包含重複元素的集合。這個接口是對數學概念中的集合的抽象。
  • List:List表示有序集合。注意,這個有序的意思是位置有序,而不是內容有序。它與Set的區別在於,Set沒有位置的概念,並且List允許有重複的元素。
  • Queue:Queue是對數據結構中的隊列的抽象。隊列按照(但不一定)FIFO(first in first out,先進先出)方式對元素進行存放,它的插入和刪除操作是在不同的兩端進行的。
  • Deque:Deque表示雙端隊列,它與Queue的不同之處在於它的兩端都支持插入和刪除。
  • Map:映射層次結構的根接口。它可以用來存儲多個鍵值對,但是Map中不能包含重複鍵。

  最後的兩個接口僅僅是Set和Map接口的排序版本:

  • SortedSet:按升序維護Set中的元素。它提供了幾個額外的操作來利用Set中的順序。
  • SortedMap:按鍵的升序來維護Map中的鍵值對。它也提供了幾個額外的操作來利用Map中的順序。

  下面分別對這些接口進行介紹。

1.Collection接口

  集合表示一組被稱爲元素的對象,它與數組的最大區別就是數組的長度是固定的,而集合的長度是可以動態變化的。當我們無法事先預估元素的個數時,就可以選擇使用集合。此外,使用集合還可以利用集合爲我們提供的大量實用的方法。
  Collection接口用在需要最大通用性地傳遞集合的地方。例如,按照慣例,每種集合的實現都有一個接受Collection參數的構造方法。這個方法被稱爲轉換構造器,它使用指定集合中的所有元素來初始化新的集合,無論給定的集合是什麼類型。換句話說,使用這個構造方法可以轉換集合的類型。
  例如,現在有一個Collection類型的集合c,它可能是一個List,Set,或者其他類型的集合。下面的代碼使用c中的所有元素來初始化一個新的ArrayList(List接口的一種實現):

List<String> list = new ArrayList<>(c);

  Collection接口中包含了一些基本操作:

  Collection接口中還包含了一些操作整個集合的方法:

  還有兩個將集合轉爲數組的方法:

  這兩個方法都可以將集合轉換爲數組。不過,toArray()方法只能將集合轉換爲Object數組,而Object數組又無法直接強制轉換爲其他類型的數組,因此這個方法的實用性就要差一些。而toArray(T[] a)方法則可以直接將集合轉換爲T類型的數組,如果參數a的長度小於集合中元素的個數,該函數會返回一個包含集合中所有元素的新的數組;如果參數a的長度大於等於集合中元素的個數,該函數就會使用數組a來返回,並且在a[size]放置一個null,size表示集合中元素的個數,這樣toArray(T[] t)方法的調用者就可以知道null後面已經沒有集合元素了。不過,一般我們傳遞的數組只是爲了傳遞數組的類型,因此我們會傳遞一個空的數組進去。例如,假設現在有一個存放字符串的集合,要將它轉換爲字符串數組,可以像下面這樣寫:

String[] y = x.toArray(new String[0]);

  從Java8之後,Collection接口中增加了兩個與流(Stream)相關的方法,Stream stream()和Stream parallelStream(),它們分別用於獲取集合的順序流和並行流。有關流與聚合操作的內容會在稍後進行介紹。

遍歷Collection

  有三種遍歷Collection的操作:(1)增強型for循環;(2)迭代器;(3)聚合操作。
(1)增強型for循環
  我們早在流程控制一文中已經提到了增強型for循環。增強型for循環可以用來遍歷數組或實現了Iterable接口的類。從上面的Collection接口的聲明可以看到,Collection接口繼承了Iterable接口,這意味着所有的集合都可以使用增強型for循環來遍歷。例如:

public void traverseWithFor(Collection<String> collection) {
    for(String str : collection) {
        System.out.println(str);
    }
}

(2)迭代器
  迭代器(也稱作遊標)是一種可以訪問容器中的元素的對象。由於各種容器的內部結構不同,它們的遍歷方式也就不盡相同。爲了使對容器內元素的遍歷方式更加簡單和統一,Java引入了迭代器模式。迭代器模式提供了一種方法順序訪問一個聚合對象中的各種元素,但又不暴露該對象的內部表示。Java中定義的迭代器接口是Iterator,每個迭代器都必須實現這個接口。這個接口中最主要的三個方法是:

boolean hasNext();
E next();
void remove();
}

  hasNext方法用於判斷迭代器所在的位置後面是否存在元素,而next方法則會返回迭代器所在位置後面的元素並移動迭代器,如果沒有下一個元素,這個方法會拋出NoSuchElementException。remove方法會移除上一個由next返回的元素,但是這個方法只能在每次調用next後調用一次。
  如何獲取迭代器呢?或者說什麼樣的對象才能獲取迭代器呢?實際上,如果要從一個對象上獲取迭代器,那麼這個對象必須是可迭代的,也就是說這個類必須實現Iterable接口,通過Iterable的iterator方法可以獲取到迭代器。Collection接口繼承了Iterable接口,也就是說所有的集合都是可迭代的,因此可以從所有的集合對象上獲取迭代器,並使用迭代器對集合進行遍歷。例如:

public void traverseWithIterator(Collection<String> collection) {
    Iterator <String> it = collection.iterator();
    while(it.hasNext()) {
        System.out.println(it.next());
    }
}

  如果要在遍歷的過程中刪除元素,那麼使用迭代器的remove方法是最安全的做法,調用Collection中定義的remove方法可能會引起錯誤。例如:

public void removeSomething(Collection<String> collection, String something) {
    for(int i = 0;i < collection.size(); ++i){
        if(collection.get(i).equals(something)) {
            collection.remove(i);
        }
    }
}

  這種方式的問題在於,刪除某個元素後,集合內元素的索引發生變化,而我們的索引也在變化,所以會導致你在遍歷的時候漏掉某些元素。而且,如果在刪除元素時引起了集合的收縮(當集合中的元素個數小於集合的容量且達到一定程度後,集合會自動進行縮容操作),那麼我們的最後幾次循環可能會造成索引越界。所以,更安全的做法是:

public void removeSomething(Collection<String> collection, String something) {
    Iterator <String> it = collection.iterator();
    while(it.hasNext()) {
        if(it.next().equals(something)) {
            it.remove();
        }
    }
}

(3)聚合操作
  在Java8及更高版本中,迭代集合的首選方法是獲取流並對其執行聚合操作。聚合操作通常與lambda表達式結合使用,以使用較少的代碼使程序更具表現力。以下代碼按順序遍歷一個形狀集合並打印出紅色的形狀:

myShapesCollection.stream()
.filter(e -> e.getColor() == Color.RED)
.forEach(e -> System.out.println(e.getName()));

  同樣,您可以輕鬆地請求並行流,如果集合足夠大並且您的CPU具有足夠的核心,這可能是有意義的:

myShapesCollection.parallelStream()
.filter(e -> e.getColor() == Color.RED)
.forEach(e -> System.out.println(e.getName()));

  使用此API操作集合元素的方法有很多種。例如,您可能希望將一個Collection的元素轉換爲String對象,然後將它們連接起來,用逗號分隔:

String joined = elements.stream()
    .map(Object::toString)
    .collect(Collectors.joining(", "));

  或者對所有員工的工資進行求和:

int total = employees.stream()
    .collect(Collectors.summingInt(Employee::getSalary)));

  這裏只是簡單的對集合的聚合操作進行一個簡單的展示,稍後我們將會詳細地介紹聚合操作。

批量操作

  批量操作對於整個集合進行操作。下面是Collection中的批量操作:

  • containsAll(Collection<?> c):如果集合中包含指定集合的所有元素,則返回true。
  • addAll(Collection<? extends E> c):將指定集合的所有元素添加到當前集合。
  • removeAll(Collection<?> c):從集合中刪除指定集合中的所有元素。
  • retainAll(Collection<?> c):從當前集合中刪除指定集合中不存在的元素,相當於求交集操作。
  • clear():從集合中刪除所有元素。

  如果集合發生了變化,那麼addAll,removeAll和retainAll將會返回true。
  爲了展示批量操作的效率,下面的例子從集合c中刪除指定元素e:

c.removeAll(Collections.singleton(e));

  或者說,要從集合中刪除所有null元素:

c.removeAll(Collections.singleton(null));

  Collections.singleton是一個靜態工廠方法,它返回一個只包含一個元素的Set。Collections類提供了大量操作集合的方法,我們會在下面逐一進行介紹。

2.Set接口

  Set是Collection接口的子接口,它表示不能含有重複元素的集合,這一點和數學中的集合相同。Set接口包含了繼承自Collection接口中的方法並添加了禁止重複元素的限制。
  下面是一個簡單但實用的使用Set的例子。假設現在有一個集合,我們希望創建另一個包含相同元素但刪除了所有重複元素的集合:

Collection<Type> noDups = new HashSet<>(c);

  Set最大的特點就是不包含重複項,因此在構造過程中會自動去除重複的元素,而無需我們關心。上面使用的HashSet是Set接口的一個實現,我們會在稍後介紹各接口的常用實現。
  由於Set接口繼承自Collection接口,因此大部分方法都和Collection接口中的方法相同,這裏不再贅述。此外,從Java 9之後,Set接口還提供了of和copyOf兩個用於快速創建集合的靜態方法,不過需要注意的是,它們創建的都是不可變的Set,也就是說集合一經創建就不能再添加或刪除元素。其中of方法接受0個或多個參數,並返回包含這些元素的不可變集合,而copyOf則根據指定的集合來創建包含該集合中元素的不可變集合。下面是使用這兩個方法的例子:

Set<Integer> set1 = Set.of(1, 2);
Set<Integer> set2 = Set.copyOf(set1);

  此外,對於Set來說,只要兩個集合的元素都是相等的,那麼我們就認爲這兩個集合是相等的。這與Object類默認的equals方法不同,因此在實現Set接口時,我們還需要重新定義equals和hashcode方法。

3.List接口

  List也是Collection接口的子接口,它表示有序集合,並且它可以包含重複元素,有時也將其稱之爲列表。除了繼承自Collection的方法外,它還支持以下操作:

  • 位置訪問:根據在列表中的位置來操作元素,例如get、set、add、addAll和remove等方法。
  • 查找:查找指定的對象並返回其在列表中的位置,查找方法包括indexOf和lastIndexOf。
  • 迭代:除了繼承自Collection接口的iterator方法,List接口還提供了listIterator方法來獲取利用列表的順序性對列表進行遍歷的迭代器。
  • 視圖操作:subList方法獲取一個子列表的視圖,對它進行操作實際上就是對原列表進行操作。
  • 排序:按照指定的比較規則對列表中的元素進行排序。
  • 替換:按照指定的規則對列表中的元素進行替換。

  由於List接口繼承自Collection接口,因此Collection中的方法不再介紹。此外,List接口中也包含of和copyOf靜態方法,除了它返回的是不可變的List之外,其他都和上面Set接口中的這兩個方法一致。下面對List接口中特有的方法進行介紹。

位置訪問

  下面是List接口中與位置有關的操作:

  • add(int index, E element):將指定元素插入到指定位置上。
  • addAll(int index, Collection<? extends E> c):將指定集合中的元素插入到指定位置上。
  • get​(int index):獲取指定位置上的元素。
  • remove​(int index):移除指定位置上的元素。
  • set​(int index, E element):使用指定元素替換指定位置上的元素。

  這些方法的使用都比較簡單,這裏不再一一舉例。

搜索

  List接口中提供了indexOf和lastIndexOf兩個方法來對指定元素進行查找,它們與contains最大的區別就是如果找到元素,就可以返回元素在列表中的位置。如果沒有找到,這兩個方法將會返回-1。由於列表允許重複的元素,因此indexOf會返回從前向後查找時該元素第一次出現的位置,而lastIndexOf會返回從後向前查找時該元素第一次出現的位置。例如:

List<Integer> intList = List.of(0, 1, 2, 1, 0);
System.out.println(intList.indexOf(1));
System.out.println(intList.lastIndexOf(1));

  上面的例子將會輸出:

1
3

迭代器

  除了繼承自Collection接口的iterator方法,List接口還提供了listIterator方法,使用這個方法可以獲取一個ListIterator類型的迭代器。使用這種迭代器可以以任一方向對列表進行遍歷,在遍歷期間修改列表(不只是刪除,還可以插入和替換),或者是獲取迭代器的當前位置。
  ListIterator接口是Iterator接口的子接口,除了繼承自Iterator接口的三個方法外(hasNext、next和remove),它還新增了六個方法。下面是這九個方法的簡介:

  • hasNext():判斷迭代器所在的位置後面是否有元素。
  • next():返回迭代器所在位置後面的元素並向後移動迭代器。
  • hasPrevious():判斷迭代器所在的位置前面是否有元素。
  • previous():返回迭代器所在位置前面的元素並向前移動迭代器。
  • previousIndex():返回迭代器所在位置的前一個元素的索引。
  • nextIndex():返回迭代器所在位置的後一個元素的索引。
  • add(E e):將指定元素插入到迭代器所在的位置前面。
  • set(E e):使用指定元素替換上一次next或previous操作返回的元素。
  • remove():移除上一次next或previous操作返回的元素。

  這裏有必要解釋一下ListIterator的位置的概念。對於一個ListIterator來說,它的遊標並不指向某個元素,而是位於兩個元素之間。如果一個列表有n個元素,那麼ListIterator的遊標的位置就有n+1個,例如:

  上圖中的列表共有4個元素,那麼遊標可能出現的位置就有5個,分別是0,1,2,3,4。
  List接口的listIterator方法有兩種形式。listIterator()返回一個遊標在0處的迭代器,listIterator(int index)返回一個指定位置的迭代器,例如上圖中,list.listIterator(4)就會返回最後一個位置的迭代器。通過這兩個方法和ListIterator,就可以從任意位置,任意方向去遍歷列表。
  需要注意的是,remove和set方法並沒有涉及到迭代器的位置,它們操作的是上一次next或previous操作返回的元素。除此之外,在調用next或previous之後,調用remove之前,這中間不能調用add方法;而在調用next或previous之後,調用set之前,這中間不能調用add或remove方法。這是由於如果在set或remove之前進行了其他操作,列表會發生變化,此時再調用這兩個方法有可能產生歧義,因此爲了避免這種情況,這兩個方法將會拋出IllegalArgumentException。

視圖操作

  subList(int fromIndex, int toIndex)是一個視圖操作,它返回一個子列表,這個子列表包含了原列表中索引在fromIndex到toIndex(不包括toIndex)之間的元素。之所以說subList是一個視圖操作,是因爲它返回的子列表的元素都是原列表中的元素,而不是它們的拷貝。對子列表的操作也會影響到原列表。
  例如,下面的例子從列表中移除子列表視圖中的元素:

list.subList(fromIndex, toIndex).clear();

  這比我們使用迭代器去刪除這些元素簡潔不少。下面的例子在指定的範圍中查找元素:

int i = list.subList(fromIndex, toIndex).indexOf(o);
int j = list.subList(fromIndex, toIndex).lastIndexOf(o);

  注意,上面的例子中返回的是元素在子列表中的索引,而不是在原列表中的索引。
  儘管subList操作非常強大,但在使用時必須小心。在修改了原列表後,不要再使用之前的subList,否則可能會出現異常,例如:

List<String> list = new ArrayList<>();
list.add("0");
list.add("1");
list.add("2");
list.add("3");
List<String> subList = list.subList(0,2);
list.remove(1);
System.out.println(subList);

  上面的程序中,subList是索引爲0和1的兩個元素的視圖,從原集合中刪除索引爲1的元素,視圖也會隨着改變。但是對於我們來說,如果這個操作在其他的方法中,那麼我們無法感知視圖的變化,程序也有可能會拋出異常。爲了避免這種情況,建議在修改原始列表後,不要再使用之前的視圖,而是應該重新獲取。

排序

  List接口提供了一個默認方法sort​(Comparator<? super E> c),它接受一個Comparator類型的對象作爲參數,然後根據該對象中定義的比較規則對列表進行排序。Comparator定義了某種類型的比較規則,它只有一個抽象方法compare(T o1, T o2),因此它是一個函數式接口,也就是說可以向sort方法傳遞lambda表達式。當o1大於o2時,compare方法返回正數;當o1小於o2時,compare方法返回負數;當o1等於o2時,compare方法返回0。
  現在假設我們有一個Apple類,這個類有weight以及其他的屬性。當一個Apple的weight大於另一個Apple的weight時,我們就認爲這個Apple是“大於”另一個Apple的。根據這個定義,我們來編寫用於排序Apple列表的Comparator:

public class AppleComparator implements Comparator<Apple> {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight() - a2.getWeight();
    }
}

  現在就可以對Apple列表appleList進行排序了:

appliList.sort(new AppleComparator());

  當然,可以使用更加簡潔的lambda表達式,這樣就無需編寫AppleComparator類了,只需要像下面這樣:

appleList.sort((a1, a2) -> a1.getWeight() - a2.getWeight());

  sort方法首先將列表轉換爲數組,然後使用歸併排序對數組進行排序,最後再將數組中的元素放回列表中。該sort方法提供了快速、穩定的排序(排序算法的穩定性是排序前後相等元素的相對順序不發生改變)。

替換

  List接口提供了一個默認方法replaceAll​(UnaryOperator operator),用於替換列表中的元素。這個方法接受一個UnaryOperator對象作爲參數,這是Java中定義的一個函數式接口,因此我們一般直接向replaceAll方法傳遞lambda表達式。UnaryOperator接口的抽象方法apply接受一個參數,對它執行某些操作然後返回操作後的結果。replaceAll方法會對所有的元素執行apply方法,然後用返回的結果替換原來的元素。
  利用這個方法可以對滿足條件的元素執行替換操作。例如,現在有一個存放字符的集合charList,我們需要找到其中的小寫字母並將其轉換爲大寫字母:

charList.replaceAll(c -> {
    if (Character.isLowerCase(c)) {
        return Character.toUpperCase(c);
    }
    return c;
});

列表算法

  除了List接口中聲明的這些方法以外,Collections類還提供了許多操作List的方法。下面對這些方法進行簡要地介紹:

  • sort——對指定的列表進行排序,本質上只是調用了List自身的sort方法。
  • shuffle——隨機打亂列表中元素的順序。
  • reverse——反轉列表。
  • rotate——將列表按照指定的距離進行旋轉。
  • swap——交換兩個指定索引處的元素。
  • replaceAll——使用指定的新元素替換指定的舊元素。
  • fill——使用指定元素替換列表中的所有元素。
  • copy——將源List中的元素拷貝到目標List。
  • binarySearch——使用二分查找算法在列表中查找元素,前提是列表必須是排好序的(升序)。
  • indexOfSubList——從前向後查找子列表在指定列表中出現的位置,若未找到則返回-1。
  • lastIndexOfSubList——從後向前查找子列表在指定列表中出現的位置,若未找到則返回-1。

4.Queue接口

  隊列是在處理之前保存元素的集合。除了基本的集合操作外,Queue接口還新增了以下方法:

E element();
boolean offer(E e);
E peek();
E poll();
E remove();

  Queue的每種方法都存在兩種形式:一種是在操作失敗的時候拋出異常,另一種在操作失敗的時候返回特定值(null或false,取決於操作類型)。下表列出了這些方法:

  隊列通常(但不一定)是按照先進先出(FIFO,first in first out)的方式來保存元素。優先級隊列除外,它們根據元素的值對元素進行排序。無論使用什麼順序,隊列頭部的元素都是通過調用remove或poll方法將會被刪除的那個元素。在FIFO隊列中,所有的新元素都被插入隊列的尾部。其他類型的隊列可能使用不同的排列規則。每個Queue接口的實現都必須指定其排序特性。
  有些類型的隊列會限制元素的個數,這樣的隊列被認爲是有界的。java.util.concurrent包中的某些Queue的實現是有界的,而java.util包中的Queue的實現則不是。
  Queue接口中的add方法繼承自Collection接口,它向隊列中插入一個元素。如果元素的個數超出容量限制,這個方法會拋出一個IllegalStateException異常。另一個向隊列中插入元素的方法是offer方法,當插入失敗時,這個方法將會返回false。當使用有界隊列時,更推薦使用這個方法。
  remove和poll方法都返回並移除隊列頭部的元素。當隊列爲空時,remove拋出NoSuchElementException異常,而poll則返回null。
  element和peek方法返回但不移除隊列頭部的元素。當隊列爲空時,element拋出NoSuchElementException異常,而peek則返回null。
  Queue的實現類通常來說不允許插入null元素。但LinkedList類是個例外,它也是Queue的實現類,但是它允許插入null。在使用LinkedList時應該避免插入null元素,因爲null被peek和poll方法作爲特殊的返回值。
  下面的例子將隊列用作一個倒計時器:

public void countdown(int seconds) throws InterruptedException {
    int time = Integer.parseInt(seconds);
    Queue<Integer> queue = new LinkedList<Integer>();
    for (int i = time; i >= 0; i--)
        queue.add(i);
    while (!queue.isEmpty()) {
        System.out.println(queue.remove());
        Thread.sleep(1000);
    }
}

  該程序將會每秒輸出一個數字,這個數字代表倒計時所剩的秒數。由於我們入隊時是按照從大到小的順序,而出隊時也是從大到小的順序,這正好證明了隊列的先進先出的特性。

5.Deque接口

  Deque是Queue的子接口,它表示雙端隊列。雙端隊列是支持在兩端進行插入和刪除操作的隊列。正是由於雙端隊列支持在兩端進行操作,它既可以被用作後進先出的棧,也可以被用作先進先出的隊列。
  由於Deque支持在兩端進行插入、刪除和查看操作,再加上每種操作都有拋出異常和返回特殊值兩種形式,因此Deque接口中共定義了以下12個訪問元素的方法:

  以上這些方法與除了操作的位置不同外,其餘與Queue中的方法完全相同,這裏不再贅述。除此之外,Deque還提供了removeFirstOccurence和removeLastOccurence兩個方法,這兩個方法分別刪除指定元素在Deque中第一次和最後一次出現的位置。

6.Map接口

  Map接口表示映射結構,它是對數學概念中的映射的抽象,可以將它理解爲存儲鍵值對的集合。但實際上它並不是集合,它是Java集合框架中映射結構的頂級接口。通過給定的鍵可以很快地從Map中找到對應的值。Map中不能包含重複的鍵,如果向Map中插入鍵相同的鍵值對,那麼Map中這個鍵對應的值將會被新的值覆蓋。
  下面是Map接口中定義的基本方法:

  需要注意的是,某些Map實現支持null值,而有些則不支持。因此,當使用get方法獲取value時,若返回null,則需要根據對應的實現類來進行判斷是key不存在還是key對應的value是null。當然,爲了保險起見,可以先調用containsKey來判斷指定的key是否存在。
  除了操作單個元素的基本操作外,Map中還定義了兩個批量操作映射的方法:

  Map中定義了一個內部接口Entry,它用來表示Map中的一個鍵值對。Entry接口提供了getKey和getValue方法,可以分別獲得鍵值對的鍵和值。Map接口還提供了一個靜態方法entry(K key,V value),這個方法將會根據指定的key和value生成一個不可變的Entry類型的對象。
  Map接口提供了針對鍵、值和鍵值對的集合視圖:

  • keySet——返回包含Map中所有鍵的Set。
  • values——返回包含Map中所有值的Collection。這個Collection不會是Set,因爲多個鍵可能對應相同的值。
  • entrySet——返回包含Map中所有鍵值對的Set。

  Map接口中還提供了幾個可以直接返回Map實例的方法:

  copyOf相當於是對原Map的拷貝,of方法根據指定的若干對鍵和值來創建Map,ofEntries接受的是可變參數,它根據指定的若干個鍵值對來創建Map。需要注意的是,這些方法返回的都是不可變的Map,對其調用任何可能會改變映射內容的方法都會拋出異常。
  此外,Map接口中還提供了一些實用的默認方法,下面對這些方法做一個簡單的介紹,具體的使用方法可以參考官方的API:

7.SortedSet接口

  SortedSet是Set接口的子接口,它實際上是Set的排序版本。它對內部的元素按照自然順序或者在構建SortedSet的實例時指定的Comparator來對元素進行排序。我們上面提到的TreeSet就實現了SortedSet接口。除了繼承自Set接口的方法外,SortedSet還提供了以下操作:

  • 範圍視圖:獲取任意範圍內的元素的視圖。
  • 訪問雙端元素:返回第一個或最後一個元素。
  • 獲取比較器:返回排序使用的比較器(如果有)。

  SortedSet中定義了以下方法:

public interface SortedSet<E> extends Set<E> {
    // Range-view
    SortedSet<E> subSet(E fromElement, E toElement);
    SortedSet<E> headSet(E toElement);
    SortedSet<E> tailSet(E fromElement);

    // Endpoints
    E first();
    E last();

    // Comparator access
    Comparator<? super E> comparator();
}

  SortedSet的視圖操作共有三個方法:

  • subSet(E fromElement, E toElement):返回包含範圍在fromElement(包含)到toElement(不包含)之間的元素的集合。
  • headSet(E toElement):返回小於toElement的元素的集合。
  • tailSet(E fromElement):返回大於fromElement的元素的集合。

  與List的視圖不同,即使修改了原集合,SortedSet的視圖仍然有效。List的視圖是原集合中的特定元素,當原集合發生結構性的變化後,視圖無法繼續保持原來的特定元素;而SortedSet的視圖則是原集合中特定範圍內的元素,只與範圍有關,因此在修改了原集合之後,視圖仍然有效。
  訪問雙端元素的方法是first()和last(),它們分別返回SortedSet中的第一個元素和最後一個元素。
  通過comparator()方法可以獲取SortedSet在排序時使用的比較器。如果集合內元素是按照自然順序排序的,這個方法將會返回null。

8.SortedMap接口

  SortedMap是Map接口的子接口,它實際上是Map的排序版本。它按照自然順序或者在構建SortedMap的實例時指定的Comparator來對鍵進行排序。與SortedSet類似,它也提供了以下操作:

  • 範圍視圖:獲取鍵在指定範圍內的子映射的視圖。
  • 訪問雙端鍵:返回第一個或最後一個鍵。
  • 獲取比較器:返回排序使用的比較器(如果有)。

  SortedMap中定義了以下方法:

public interface SortedMap<K, V> extends Map<K, V>{
    // Range-view
    SortedMap<K, V> subMap(K fromKey, K toKey);
    SortedMap<K, V> headMap(K toKey);
    SortedMap<K, V> tailMap(K fromKey);

    // Endpoints
    K firstKey();
    K lastKey();

    // Comparator access
    Comparator<? super K> comparator();
}

  這些方法與SortedSet中定義的方法基本類似,可以類比參照上一小節中對這些方法的介紹,這裏不再進行過多描述。

三.實現

  在講完了Java集合框架中的基本接口後,現在我們來學習這些接口的實現。本文描述了以下幾種實現:

  • 通用實現——最常用的實現,專爲日常使用而設計。
  • 專用實現——專門爲一些特殊場景設計,性能、限制或行爲可能與通用實現不同。
  • 併發實現——旨在支持高併發性。
  • 包裝實現——包裝其他類型的實現(通常是通用實現),以增加或限制功能。
  • 便捷實現——通常是通過靜態工廠方法提供的迷你實現,爲特殊集合(例如單例集)的實現提供方便、有效的替代方案。
  • 抽象實現——可以理解爲骨架實現,有助於方便、快速地構建自定義實現。

  下面將依次對第二節中提到的接口的實現進行介紹。在介紹實現時,由於之前已經對各接口中定義的方法做了說明,因此不再重複闡述,只是描述各實現類的特點和注意事項。

1.Set實現

通用實現

  Java平臺中提供的Set接口的通用實現是HashSet、TreeSet和LinkedHashSet。HashSet將元素存在一個哈希表中,是性能最佳的實現,但是它不能保證迭代的順序。TreeSet將元素存儲在一個紅黑樹中,根據元素的值來進行排序,它比HashSet慢得多。LinkedHashSet同時具備了鏈表和哈希表的特點,使用鏈表來保存插入順序,使用哈希表來快速定位元素,也就是說它的遍歷順序和插入順序是一致的,但同時它的成本也是最高的。在實際使用過程中,可以靈活選擇這幾種Set。如果對遍歷的順序沒有要求或者不需要遍歷,可以選擇HashSet;如果在遍歷時需要按照元素的值進行排序,可以選擇TreeSet;如果需要按照插入順序遍歷,可以選擇LinkedHashSet。
  實際上,有關這些實現類還有很多實現細節沒有介紹。由於本系列是基礎教程,因此不再過多深入。後續會推出閱讀Java源碼的系列,屆時將會對Java中部分常用的類的源碼進行詳細介紹,敬請期待。

專用實現

  Java提供了兩個Set接口的專用實現——EnumSet和CopyOnWriteArraySet。
  EnumSet是爲枚舉類型提供的高性能實現。EnumSet的所有成員必須具有相同的枚舉類型。下面是EnumSet類中提供的構造EnumSet的工廠方法:

  EnumSet內部維護了一個存儲該枚舉類型所有成員的數組,還有一個表示每個枚舉成員是否存在於集合內的“位向量”,這個“位向量”可能是long也可能是long數組。因爲每個枚舉對象都有一個序號,因此位向量使用序號對應的位上的0或1來表示該對象是否存在於集合中。例如,現在我們有一個關於星期的枚舉類型:

public enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}

  我們使用of方法來創建一個EnumSet實例:

Set<Day> days = EnumSet.of(Day.TUESDAY, Day.FRIDAY);

  那麼對於這個EnumSet來說,它的位向量是下面這樣的:

  實際上,EnumSet是一個抽象類。java.util包中提供了它的兩個子類,分別是RegularEnumSet和JumboEnumSet,RegularEnumSet使用一個long變量來表示位向量,而JumboEnumSet則使用long數組來表示位向量。在使用EnumSet的工廠方法構建實例時,它會自己選擇使用哪個子類,而無需我們關心。
  現在來看另外一個專用實現——CopyOnWriteArraySet。在介紹這個類之前,首先我們要了解一下CopyOnWrite的概念。這個術語的字面意思是在寫的時候複製,實際上也是這麼做的。在修改某個數據時,我們先拷貝一份副本,在副本上進行寫操作,然後再替換之前的數據。這樣可以避免在寫數據時,因爲加鎖而造成其他讀線程等待的情況。CopyOnWriteArraySet內部使用數組來保存元素,當對集合進行修改操作時(例如add、set或remove等),它會先將數組拷貝出一個副本,然後對這個副本進行操作,最後再將對原數組的引用指向新的數組。實際上,CopyOnWriteArraySet內部使用了下文中List接口的實現類CopyOnWriteArrayList來保存元素,而CopyOnWriteArrayList纔是真正使用數組來實現的。
  正是由於這個機制,CopyOnWriteArraySet具有以下特點:

  1. 它適用於讀操作遠遠多於寫操作的場景,因爲寫操作代價較高,它通常意味着複製整個底層數組;
  2. 它是線程安全的;
  3. 迭代器不支持remove操作;
  4. 讀進程有可能讀到的是過時的數據;
  5. 讀寫進程之間沒有競爭,但是寫進程之間還是需要加鎖。

2.List實現

通用實現

  Java集合框架中提供了兩個List集合的通用實現——ArrayList和LinkedList。ArrayList內部使用數組實現,因此它的訪問速度非常快。但正是由於它內部使用數組,因此在指定位置處添加或刪除元素時需要移動後面所有的元素。需要注意的是,它並不是線程安全的。不過,可以使用Collections類的synchronizedList方法來將一個ArrayList轉換爲線程安全的對象。
  如果需要頻繁地在List的開頭添加或刪除元素並且元素的個數非常多時,則應考慮使用LinkedList。它是使用鏈表來實現的,因此它的添加和刪除元素操作非常快。但正是由於鏈式結構,因此它的訪問速度則需要花費線性時間。此外,LinkedList還實現了Deque,因此它支持Deque接口中定義的操作。
  在使用List時,要充分考量程序的性能,來選擇使用ArrayList還是LinkedList。一般來說,ArrayList更快。

專用實現

  Java提供了兩個List接口的專用實現——Vector和CopyOnWriteArrayList。
  Vector是線程安全的List,並且它比經過Collections.synchronizedList處理過的ArrayList還要快一點。但是Vector中含有大量的遺留代碼,畢竟它從Java1.0就開始存在了,當時還沒有集合框架。從Java1.2開始,這個類被改進以實現List接口,使其成爲Java集合框架的一員。因此,在使用Vector時,應該儘量使用List接口去操作。
  CopyOnWriteArrayList的原理在上文中對CopyOnWriteArraySet的介紹中已經做了說明,這裏不再贅述。

3.Map實現

通用實現

  Map接口的三個通用實現分別是HashMap、TreeMap和LinkedHashMap。如果你想要最快的速度而不關心迭代順序,請使用HashMap。如果需要SortMap操作或按鍵排序的迭代順序,請使用TreeMap。如果需要接近HashMap的速度和按插入順序迭代,請使用LinkedHashMap。這三個實現和Set接口中的三個通用實現非常類似。
  雖然LinkedHashMap默認是按照插入順序來排序,但是可以在構造LinkedHashMap實例時指定另外一種排序規則。這種規則按照最近對每個鍵值對的訪問次數來排序,通過這種Map可以很方便地構建LRU緩存(Least Recently Used,一種緩存策略)。LinkedHashMap還提供了removeEldestEntry方法,通過覆蓋該方法,可以定義何時刪除舊緩存。例如,假如我們的緩存最多隻允許100個元素,在緩存中的元素個數達到100個之後,每次添加新元素時都要清除舊元素,從而保持100個元素的穩定狀態,可以像下面這樣:

private static final int MAX_ENTRIES = 100;

protected boolean removeEldestEntry(Map.Entry eldest) {
    return size() > MAX_ENTRIES;
}

  實際上,還有一個Map接口的通用實現——Hashtable(注意小寫)。它也是從Java1.0開始就存在的一個“元老”。從Java1.2開始,它被改進以實現Map接口。它是線程安全的,但是由於效率較低,單線程時可以使用HashMap來代替,多線程時又可以使用下文中的ConcurrentHashMap來代替,因此這個類現在已經不再推薦使用。

專用實現

  Java提供了三個Map接口的專用實現——EnumMap,WeakHashMap和IdentityHashMap。
  EnumMap用在那些需要將枚舉元素作爲鍵的映射中,它的底層是使用數組來實現的。EnumMap是一個Map與枚舉鍵一起使用的高性能實現。該實現將Map接口的豐富性和安全性與數組的速度相結合。如果要將枚舉映射到值,則應始優先考慮EnumMap。
  WeakHashMap只存儲對其鍵的弱引用。JVM提供了四種類型的引用,分別是強引用、弱引用、軟引用和虛引用,可以讓程序員來決定某些對象的生命週期,有利於JVM進行垃圾回收。在進行垃圾回收時,若某個對象只具有弱引用,它一定會被回收。因此,當WeakHashMap中的鍵不再被外部引用時,JVM將會對它進行回收,該鍵值對也會消失。
  IdentityHashMap在比較鍵時使用引用相等性代替對象相等性。在正常的Map實現中,當key1.equals(key2)返回true時,我們就認爲這兩個key是相等的;而在IdentityHashMap中,只有當key1==key2時,才認爲這兩個key是相等的。

併發實現

  ConcurrentHashMap時Java提供的一個高併發、高性能的Map實現,它位於java.util.concurrent包中。這個實現在執行讀操作時不需要加鎖。因爲這個類旨在作爲Hashtable的替代品,因此,除了實現ConcurrentMap接口外(爲線程安全Map定義的接口),它還保留了大量Hashtable中的遺留代碼。因此,在使用ConcurrentHashMap時,應該儘量使用ConcurrentMap或Map接口去操作。

4.Queue實現

通用實現

  Queue接口的通用實現包括LinkedList和PriorityQueue。
  我們已經在List實現中介紹了LinkedList,爲什麼還要在Queue實現中再次提到它呢?這是因爲LinkedList同時實現了List接口和Deque接口,而Deque接口又是Queue的子接口。因此,LinkedList可以算是List、Queue和Deque的實現。當我們使用不同的接口去引用LinkedList實例時,它就會表現出不同的行爲。
  PriorityQueue用來表示基於堆結構的優先級隊列。它使用指定的排序規則來對元素進行排序,而不是按照隊列默認的先進先出的順序。在對PriorityQueue進行遍歷時,迭代器不保證以任何特定順序遍歷優先級隊列的元素。如果需要有序遍歷,可以使用Arrays.sort(pq.toArray())。

併發實現

  java.util.concurrent包中提供了一組Queue實現類,這些類都是線程安全的,因此可以在多線程中使用。這些類都實現了BlockingQueue接口,這個接口繼承了Queue接口並且增加了一些額外的操作,支持當獲取隊列元素但是隊列爲空時,會阻塞等待隊列中有元素再返回或超時返回;也支持添加元素時,如果隊列已滿,那麼等到隊列可以放入新元素時再放入或超時返回。
  下表是BlockingQueue接口中操作元素的方法,除了繼承自Queue接口的拋出異常和返回特定值的形式外,又增加了阻塞和超時兩種形式:

  下面是BlockingQueue接口的實現類:

  • LinkedBlockingQueue——由鏈式節點實現的有界FIFO阻塞隊列。
  • ArrayBlockingQueue——由數組實現的有界FIFO阻塞隊列。
  • PriorityBlockingQueue——由堆實現的無界阻塞優先級隊列。
  • DelayQueue——支持延時獲取元素的無界阻塞隊列。
  • SynchronousQueue——同步阻塞隊列,無容量,它的每個插入操作都要等待其他線程相應的移除操作,反之亦然。

  此外,java.util.concurrent包中還定義了TransferQueue接口,它是BlockingQueue的子接口。在添加元素時,除了BlockingQueue中定義的方法,TransferQueue還定義了transfer和tryTransfer方法。transfer方法在傳遞元素時,若存在消費者,則立即將元素傳遞給消費者,否則將元素插入到隊列尾部;tryTransfer方法在傳遞元素時,若存在消費者,則立即將元素傳遞給消費者,否則將元素插入到隊列尾部,若在指定的時間內元素沒有被消費者獲取,則將該元素從隊列中移除並返回false。TransferQueue接口有一個基於鏈式節點的無界實現——LinkedTransferQueue。

5.Deque實現

通用實現

  Deque接口的通用實現包括LinkedList和ArrayDeque。ArrayDeque使用可調整大小的數組實現,而 LinkedList則是鏈表實現。
  LinkedList允許null元素,而ArrayDeque則不允許。在效率方面,ArrayDeque比LinkedList兩端的添加和刪除操作更高效。LinkedList的最佳使用方式是在迭代期間刪除元素。不過LinkedList並不是迭代的理想結構,並且它比ArrayDeque佔用更多的內存。此外,無論是LinkedList還是ArrayDeque均不支持多個線程的併發訪問。

併發實現

  LinkedBlockingDeque類是Deque接口的併發實現。和BlockingQueue相同,它在獲取雙端隊列元素但是隊列爲空時,會阻塞等待隊列中有元素再返回或超時返回;也支持在雙端添加元素時,如果隊列已滿,那麼等到隊列可以放入新元素時再放入或超時返回。

6.包裝實現

  位於java.utils包中的Collections也是Java集合框架的一員,但它不是任意一種集合,而是一個包裝類。它包含各種有關集合操作的靜態方法,這個類不能實例化。它就像一個工具類,服務於集合框架。
  這個類提供了很多的靜態工廠方法,通過這些方法,可以提供具有某些特性的集合(例如空集合,同步集合,不可變集合等),或者包裝指定的集合,使之具有某些特性。下面對這個類中的一些方法進行介紹。

同步包裝器

  同步包裝器將自動同步(線程安全)特性添加到任意集合上。Collection,Set,List,Map,SortedSet和 SortedMap接口都有一個靜態工廠方法。

public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> Set<T> synchronizedSet(Set<T> s);
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);

  每一個方法都會返回指定集合的包裝對象,這個集合是線程安全的。所有對於集合的操作都應該通過這個集合來完成,保證這一點最簡單的方法就是不再保留對原集合的引用。
  面對併發訪問,用戶必須在迭代時手動同步返回的集合。這是因爲迭代是通過對集合的多次訪問來完成的,而返回的集合雖然是同步的,但是它只能保證每一次對集合的操作都是線程安全的,而不能保證對於幾個的多次操作也是安全的,因此這些操作必須組成一個單獨的原子操作。以下是迭代通過包裝器返回的同步集合的習慣用法:

Collection<Type> c = Collections.synchronizedCollection(myCollection);
synchronized(c) {
    for (Type e : c)
        foo(e);
}

  有關synchronized關鍵字和多線程的內容會在之後的文章中進行介紹。

不可變包裝器

  不可變包裝器可以使集合變爲不可變集合,從而具有隻讀屬性,任何會對集合進行改變的操作都會拋出一個UnsupportedOperationException。與同步包裝器一樣,六個核心接口中的每一個都有一個靜態工廠方法:

public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c);
public static <T> Set<T> unmodifiableSet(Set<? extends T> s);
public static <T> List<T> unmodifiableList(List<? extends T> list);
public static <K,V> Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m);
public static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<? extends T> s);
public static <K,V> SortedMap<K, V> unmodifiableSortedMap(SortedMap<K, ? extends V> m);

  爲了保證集合的絕對不變性,應該在獲取集合的不可變包裝後,不再保留對原集合的引用。

7.便捷實現

  本節描述了幾種便捷實現,當您不需要集合的全部功能時,它們比通用實現更方便,更高效。本節中的所有實現都是通過靜態工廠方法提供的。

數組的列表視圖

  Arrays.asList方法可以接受一個數組並返回該數組的列表視圖。對於集合的改變會應用在數組上,反之亦然。集合的大小就是數組的大小且不能更改,如果再集合視圖上調用可能會修改集合大小的方法將會拋出一個UnsupportedOperationException。
  這個實現的一般用途是作爲數組和集合之間的橋樑。它允許我們將數組傳遞給需要Collection或List的方法。這種實現還有一個用途,如果我們需要固定大小的List,它比任何通用實現都要高效:

List<String> list = Arrays.asList(new String[size]);

指定元素的集合

  下面的方法可以根據指定的元素來創建集合:

  • Arrays.asList(T... a)——返回包含指定的若干個元素的不可變List。
  • Collections.nCopies(int n, T o)——返回包含n個指定元素o的不可變List(這些元素都是o的引用)。
  • Collections.singleton(T o)——返回只包含該對象的不可變Set。

空集合或空映射

  Collections類提供了返回空Set、List和Map的方法,它們分別是emptySet()、emptyList()和emptyMap()。

四.算法

  本節中所有的算法都來自Collections類,這些算法絕大多數都以List實例作爲參數,也有少部分是以Collection實例作爲參數的。下面是這些算法:

  • sort(List list)——按照自然順序對List進行排序。
  • sort​(List list, Comparator<? super T> c)——根據指定的比較器對List進行排序。
  • shuffle​(List<?> list)——使用默認隨機源打亂List元素順序。
  • shuffle​(List<?> list, Random rnd)——使用指定隨機源打亂List元素順序。
  • reverse(List<?> list)——反轉List中的元素順序。
  • fill​(List<? super T> list, T obj)——使用指定元素替換List中的所有元素。
  • copy​(List<? super T> dest, List<? extends T> src)——將src中的元素拷貝到dest中。dest的size要大於等於src的size。
  • swap​(List<?> list, int i, int j)——交換指定位置的元素。
  • addAll​(Collection<? super T> c, T... elements)——將若干元素添加到指定的Collection中。
  • binarySearch​(List<? extends Comparable<? super T>> list, T key)——使用二分搜索算法在List中查找指定元素。該List必須是按照升序排列,且使用自然順序排序。
  • binarySearch​(List<? extends T> list, T key, Comparator<? super T> c)——使用二分搜索算法在List中查找指定元素。該List必須是按照升序排列。調用者需要提供比較器以用於在查找過程中進行比較。
  • frequency​(Collection<?> c, Object o)——返回指定元素在Collection中出現的次數。
  • disjoint​(Collection<?> c1, Collection<?> c2)——如果兩個集合沒有交集,返回true,否則返回false。
  • min​(Collection<? extends T> coll)或max​(Collection<? extends T> coll)——返回根據自然順序排列的最小值或最大值。
  • min​(Collection<? extends T> coll, Comparator<? super T> comp)或max​(Collection<? extends T> coll, Comparator<? super T> comp)——返回根據指定比較器排列的最小值或最大值。

五.總結

  到這裏爲止,關於Java集合框架的介紹就告一段落了。我們從接口、實現和算法三個層面瞭解了Java爲我們提供的優秀的集合框架。考慮到本系列是基礎教程,因此對於部分實現類的數據結構和實現細節沒有進行過多地闡述。感興趣的讀者可以查閱其他資料或者嘗試閱讀源碼,我之後也會在其他的系列中對部分重要的類的源碼進行講解,敬請期待。

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