計算機程序的思惟邏輯 (41) - 剖析HashSet

本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html

上節介紹了HashMap,提到了Set接口,Map接口的兩個方法keySet和entrySet返回的都是Set,本節,咱們來看Set接口的一個重要實現類HashSet。java

與HashMap相似,字面上看,HashSet由兩個單詞組成,Hash和Set,Set表示接口,實現Set接口也有多種方式,各有特色,HashSet實現的方式利用了Hash。編程

下面,咱們先來看HashSet的用法,而後看實現原理,最後咱們總結分析下HashSet的特色。bash

用法

Set接口

Set表示的是沒有重複元素、且不保證順序的容器接口,它擴展了Collection,但沒有定義任何新的方法,不過,對於其中的一些方法,它有本身的規範。微信

Set接口的完整定義爲:ide

public interface Set<E> extends Collection<E> {
    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);
    boolean removeAll(Collection<?> c);
    void clear();
    boolean equals(Object o);
    int hashCode();
}
複製代碼

與Collection接口中定義的方法是同樣的,不過,一些方法有一些不一樣的規範要求。工具

添加元素開發工具

boolean add(E e);
複製代碼

若是集合中已經存在相同元素了,則不會改變集合,直接返回false,只有不存在時,纔會添加,並返回true。this

批量添加spa

boolean addAll(Collection<? extends E> c);
複製代碼

重複的元素不添加,不重複的添加,若是集合有變化,返回true,沒變化返回false。

迭代器

Iterator<E> iterator();
複製代碼

迭代遍歷時,不要求元素之間有特別的順序。HashSet的實現就是沒有順序,但有的Set實現可能會有特定的順序,好比TreeSet,咱們後續章節介紹。

HashSet

與HashMap相似,HashSet的構造方法有:

public HashSet() public HashSet(int initialCapacity) public HashSet(int initialCapacity, float loadFactor) public HashSet(Collection<? extends E> c) 複製代碼

initialCapacity和loadFactor的含義與HashMap中的是同樣的,待會咱們再細看。

HashSet的使用也很簡單,好比:

Set<String> set = new HashSet<String>();
set.add("hello");
set.add("world");
set.addAll(Arrays.asList(new String[]{"hello","老馬"}));

for(String s : set){
    System.out.print(s+" ");
}
複製代碼

輸出爲:

hello 老馬 world 
複製代碼

"hello"被添加了兩次,但只會保存一份,輸出也沒有什麼特別的順序。

hashCode與equals

與HashMap相似,HashSet要求元素重寫hashCode和equals方法,且對兩個對象,equals相同,則hashCode也必須相同,若是元素是自定義的類,須要注意這一點。

好比說,有一個表示規格的類Spec,有大小和顏色兩個屬性:

class Spec {
    String size;
    String color;
    
    public Spec(String size, String color) {
        this.size = size;
        this.color = color;
    }

    @Override
    public String toString() {
        return "[size=" + size + ", color=" + color + "]";
    }
}
複製代碼

看一個Spec的Set:

Set<Spec> set = new HashSet<Spec>();
set.add(new Spec("M","red"));
set.add(new Spec("M","red"));

System.out.println(set);
複製代碼

輸出爲:

[[size=M, color=red], [size=M, color=red]]
複製代碼

同一個規格輸出了兩次,爲避免這一點,須要爲Spec重寫hashCode和equals方法,利用IDE開發工具每每能夠自動生成這兩個方法,好比Eclipse中,能夠經過"Source"->"Generate hashCode() and equals() ...",咱們就不贅述了。

應用場景

HashSet有不少應用場景,好比說:

  • 排重,若是對排重後的元素沒有順序要求,則HashSet能夠方便的用於排重。
  • 保存特殊值,Set能夠用於保存各類特殊值,程序處理用戶請求或數據記錄時,根據是否爲特殊值,進行特殊處理,好比保存IP地址的黑名單或白名單。
  • 集合運算,使用Set能夠方便的進行數學集合中的運算,如交集、並集等運算,這些運算有一些很現實的意義。好比用戶標籤計算,每一個用戶都有一些標籤,兩個用戶的標籤交集就表示他們的共同特徵,交集大小除以並集大小能夠表示他們的類似長度。

實現原理

內部組成

HashSet內部是用HashMap實現的,它內部有一個HashMap實例變量,以下所示:

private transient HashMap<E,Object> map;
複製代碼

咱們知道,Map有鍵和值,HashSet至關於只有鍵,值都是相同的固定值,這個值的定義爲:

private static final Object PRESENT = new Object();
複製代碼

理解了這個內部組成,它的實現方法也就比較容易理解了,咱們來看下代碼。

構造方法

HashSet的構造方法,主要就是調用了對應的HashMap的構造方法,好比:

public HashSet(int initialCapacity, float loadFactor) {
    map = new HashMap<>(initialCapacity, loadFactor);
}

public HashSet(int initialCapacity) {
    map = new HashMap<>(initialCapacity);
}

public HashSet() {
    map = new HashMap<>();
}
複製代碼

接受Collection參數的構造方法稍微不同,代碼爲:

public HashSet(Collection<? extends E> c) {
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}
複製代碼

也很容易理解,c.size()/.75f用於計算initialCapacity,0.75f是loadFactor的默認值。

添加元素

咱們看add方法的代碼:

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
複製代碼

就是調用map的put方法,元素e用於鍵,值就是那個固定值PRESENT,put返回null表示原來沒有對應的鍵,添加成功了。HashMap中一個鍵只會保存一份,因此重複添加HashMap不會變化。

檢查是否包含元素

代碼爲:

public boolean contains(Object o) {
    return map.containsKey(o);
}
複製代碼

就是檢查map中是否包含對應的鍵。

刪除元素

代碼爲:

public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
}
複製代碼

就是調用map的remove方法,返回值爲PRESENT表示原來有對應的鍵且刪除成功了。

迭代器

代碼爲:

public Iterator<E> iterator() {
    return map.keySet().iterator();
}
複製代碼

就是返回map的keySet的迭代器。

HashSet特色分析

HashSet實現了Set接口,內部是經過HashMap實現的,這決定了它有以下特色:

  • 沒有重複元素
  • 能夠高效的添加、刪除元素、判斷元素是否存在,效率都爲O(1)。
  • 沒有順序

若是需求正好符合這些特色,那HashSet就是一個理想的選擇。

小結

本節介紹了HashSet的用法和實現原理,它實現了Set接口,不含重複元素,內部實現利用了HashMap,能夠方便高效地實現如去重、集合運算等功能。

同HashMap同樣,HashSet沒有順序,若是要保持添加的順序,可使用HashSet的一個子類LinkedHashSet。Set還有一個重要的實現類,TreeSet,它能夠排序。這兩個類,咱們留待後續章節介紹。

HashMap和HashSet的共同實現機制是哈希表,Map和Set還有一個重要的共同實現機制,樹,實現類分別是TreeMap和TreeSet,讓咱們在接下來的兩節中探討。


未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。

相關文章
相關標籤/搜索