「譯」Java集合框架系列教程四:Set介面

迷渡發表於2013-01-22

一個Set是一個不能包含重複元素的集合。它對映了數學意義上的集合抽象。Set介面只是在繼承自Collecton介面的方法基礎之上加上不允許元素重複的限制。Set也對equals和hashCode的行為規約施加了更強的限制,使得Set例項允許進行有意義的比較,即使他們的具體實現不同。兩個集合例項相等(equal)如果它們包含相同的元素。

一個Set是一個不能包含重複元素的集合。它對映了數學意義上的集合抽象。Set介面只是在繼承自Collecton介面的方法基礎之上加上不允許元素重複的限制。Set也對equals和hashCode的行為規約施加了更強的限制,使得Set例項允許進行有意義的比較,即使他們的具體實現不同。兩個集合例項相等(equal)如果它們包含相同的元素。

下面是Set介面的API實現:

public interface Set<E> extends Collection<E> {
    // Basic operations
    int size();
    boolean isEmpty();
    boolean contains(Object element);
    // optional
    boolean add(E element);
    // optional
    boolean remove(Object element);
    Iterator<E> iterator();

    // Bulk operations
    boolean containsAll(Collection<?> c);
    // optional
    boolean addAll(Collection<? extends E> c);
    // optional
    boolean removeAll(Collection<?> c);
    // optional
    boolean retainAll(Collection<?> c);
    // optional
    void clear();

    // Array Operations
    Object[] toArray();
    <T> T[] toArray(T[] a);
}

Java平臺提供了通用的Set實現:HashSet、TreeSet和LinkedHashSet。HashSet將元素儲存在雜湊表(hash table)中,它是最高效的Set實現,但是它無法確定迭代順序。TreeSet將元素儲存在紅黑樹中,元素按照值進行排序,它比HashSet稍慢。LinkedHashSet是HashSet的連結串列實現,它保持元素插入的順序,但是訪問效能比HashSet和TreeSet差。

這裡有一個簡單但是有用的Set使用場景。假設你有一個Collection c,你想建立另外一個Collection,但必須去除重複元素(只保留一個)。下面一行程式碼就實現了你的要求:

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

這裡有另外一個變種實現,能保證原始集合中元素的順序:

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

下面是一個泛化的封裝了上面這行程式碼的方法:

public static <E> Set<E> removeDups(Collection<E> c) {
   return new LinkedHashSet<E>(c);
}


Set介面的基本操作

size方法返回Set中元素的數目。isEmpty方法判斷Set是否為空。add方法往Set中新增指定元素,如果該元素不存在於集合中,並且返回一個布林值標識元素是否成功新增。類似地,remove方法從Set中移除指定元素,如果該元素存在於集合中,並且返回一個布林值標識是否成功移除。iterator方法返回Set的迭代器。

下面的程式接受一組單詞作為引數args,列印出任何重複的單詞、所有不重複單詞的數目以及不重複單詞列表。

import java.util.*;

public class FindDups {
    public static void main(String[] args) {
        Set<String> s = new HashSet<String>();
        for (String a : args)
            if (!s.add(a))
                System.out.println("Duplicate detected: " + a);

        System.out.println(s.size() + " distinct words: " + s);
    }
}

現在用下面的命令執行程式:

java FindDups i came i saw i left

程式輸出如下:

Duplicate detected: i
Duplicate detected: i
4 distinct words: [i, left, saw, came]

注意上面的程式碼始終通過介面型別(Set)來引用對應的集合,而不是引用具體實現型別(HashSet)。這是強烈推薦使用的程式設計實踐,因為這使得我們能更靈活地切換集合具體實現,只需要改變構造器函式。如果用來儲存集合的變數或者用來傳遞給其他方法的引數被宣告為集合具體實現型別而不是集合介面型別,那麼這些變數或引數也必須隨著具體實現的改變而改變。

前面例子中Set的具體實現型別是HashSet,它不能保證Set中元素的順序。如果你想讓程式按照字母順序列印出單詞,只需要把Set的具體實現型別從HashSet修改為TreeSet。僅僅修改前面一行程式碼:

Set<String> s = new TreeSet<String>();

將產生下面的輸出:

java FindDups i came i saw i left
Duplicate detected: i
Duplicate detected: i
4 distinct words: [came, i, left, saw]


Set介面批量操作(bulk operation)

批量操作尤其適用於Set。當執行批量批量操作相當於執行集合代數意義上的運算。假設s1和s2都是Set。下面是各種批量操作:

  • s1.containsAll(s2) — 如果s2是s1的子集,返回true,否則返回false
  • s1.addAll(s2) — 得到的是s1和s2的並集
  • s1.retainAll(s2) — 得到的是s1和s2的交集
  • s1.removeAll(s2) — 得到的是s1和s2的差集(s1-s2,即所有s1中有但是s2中沒有的元素的集合)

為了計算兩個集合的並、交、差集而不修改這兩個集合,呼叫者必須先拷貝一份,然後再呼叫bulk opertaion,比如下面:

Set<Type> union = new HashSet<Type>(s1);
union.addAll(s2);

Set<Type> intersection = new HashSet<Type>(s1);
intersection.retainAll(s2);

Set<Type> difference = new HashSet<Type>(s1);
difference.removeAll(s2);

上面程式碼的結果集型別是HashSet。

讓我們再次回顧之前的FindDups程式。假設你想知道哪些單詞只出現一次,哪些單詞出現不止一次,但是又不想重複列印單詞。那麼這種效果可以用兩個Set來實現,一個Set包含引數列表中的所有單詞,另外一個Set只包含重複出現的單詞。那麼只出現一次的單詞就是這兩個Set的差集。下面是程式碼實現:

import java.util.*;

public class FindDups2 {
    public static void main(String[] args) {
        Set<String> uniques = new HashSet<String>();
        Set<String> dups    = new HashSet<String>();

        for (String a : args)
            if (!uniques.add(a))
                dups.add(a);

        // Destructive set-difference
        uniques.removeAll(dups);

        System.out.println("Unique words:    " + uniques);
        System.out.println("Duplicate words: " + dups);
    }
}

程式碼執行結果如下:

Unique words:    [left, saw, came]
Duplicate words: [i]

還有一個不那麼常見的集合代數操作,那就是對稱集合差集 —— 由兩個集合中的元素組成,但元素不能包含於兩個集合的交集中。下面的程式碼實現了這種效果:

Set<Type> symmetricDiff = new HashSet<Type>(s1);
symmetricDiff.addAll(s2); // 並集
Set<Type> tmp = new HashSet<Type>(s1);
tmp.retainAll(s2)); // tmp成了交集
symmetricDiff.removeAll(tmp);


Set介面的陣列操作

Set介面的陣列操作與前面的Collection介面的陣列操作沒有任何不同。

原文連結:「譯」Java集合框架系列教程四:Set介面

相關文章