上節介紹了HashMap,提到了Set接口,Map接口的兩個方法keySet和entrySet返回的都是Set,本節,我們來看Set接口的一個重要實現類HashSet。
與HashMap類似,字面上看,HashSet由兩個單詞組成,Hash和Set,Set表示接口,實現Set接口也有多種方式,各有特點,HashSet實現的方式利用了Hash。
下面,我們先來看HashSet的用法,然後看實現原理,最後我們總結分析下HashSet的特點。
用法
Set接口
Set表示的是沒有重復元素、且不保證順序的容器接口,它擴展了Collection,但沒有定義任何新的方法,不過,對於其中的一些方法,它有自己的規范。
Set接口的完整定義為:
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。
批量添加
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內部是用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實現的,這決定了它有如下特點:
如果需求正好符合這些特點,那HashSet就是一個理想的選擇。
小結
本節介紹了HashSet的用法和實現原理,它實現了Set接口,不含重復元素,內部實現利用了HashMap,可以方便高效地實現如去重、集合運算等功能。
同HashMap一樣,HashSet沒有順序,如果要保持添加的順序,可以使用HashSet的一個子類LinkedHashSet。Set還有一個重要的實現類,TreeSet,它可以排序。這兩個類,我們留待後續章節介紹。
HashMap和HashSet的共同實現機制是哈希表,Map和Set還有一個重要的共同實現機制,樹,實現類分別是TreeMap和TreeSet,讓我們在接下來的兩節中探討。
----------------
未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心原創,保留所有版權。