這一篇裏,我將對HashSet、LinkedHashSet、TreeSet進行彙總分析,並不打算一一進行詳細介紹,由於JDK對Set的實現進行了取巧。咱們都知道Set不容許出現相同的對象,而Map也一樣不容許有兩個相同的Key(出現相同的時候,就執行更新操做)。因此Set裏的實現其實是調用了對應的Map,將Set的存放的對象做爲Map的Key。前端
<br/>java
這裏筆者就以最經常使用的HashSet爲例進行分析,其他的TreeSet、LinkedHashSet相似,就不贅述了。ide
關係也很簡單,實現了Set的接口,繼承了AbstractSet抽象類,抽象類裏面定義了集合的常見操做,與咱們以前分析過的ArrayList之類的類似。源碼分析
<br/>this
// HashMap就是HashSet裏實現具體操做的對象 private transient HashMap<E,Object> map; // 將對象做爲Value存進去 private static final Object PRESENT = new Object();
因爲使用Map進行操做,把E做爲Key,要定義一個PRESENT對象做爲Value,每一個存入的對象都使用它來做爲Value。spa
<br/>debug
public HashSet() { map = new HashMap<>(); }
看了這個,相信園友們應該就知道它是怎麼實現的了,咱們平時構建HashSet的時候,其實它是在裏面new了一個HashMap。code
<br/>對象
public HashSet(Collection<? extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); }
調用傳集合的構造方法則是使用了HashMap裏指定初始化容量的構造方法,而後再調用addAll()。繼承
public boolean addAll(Collection<? extends E> c) { boolean modified = false; for (E e : c) if (add(e)) modified = true; return modified; }
addAll方法很簡單,其實就是遍歷集合c,而後調用add方法,實現插入HashMap。
public boolean add(E e) { return map.put(e, PRESENT)==null; }
add方法則是調用了map的put()方法,將對象做爲Key,以前域裏的PRESENT做爲Value,插入到HashMap中。
<br/>
最後值得一提的是這個構造方法:
HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
注意它是包訪問權限的,而不是public,由於這個構造方法是提供給LinkedHashSet使用的,因此裏面初始化的也是LinkedHashMap,有興趣的園友們也能夠去LinkedHashSet裏看一下它的構造方法。
<br/>
筆者前端時間剛好碰到了由於HashSet的底層事項致使的一個bug,在此跟你們分享一下:
/** * @author joemsu 2018-02-04 上午10:33 */ public class UserInfo { private Long id; private String name; private Integer age; public UserInfo(Long id, String name, Integer age) { this.id = id; this.name = name; this.age = age; } public void setName(String name) { this.name = name; } @Override public String toString() { return "UserInfo{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserInfo info = (UserInfo) o; return Objects.equals(id, info.id) && Objects.equals(name, info.name) && Objects.equals(age, info.age); } @Override public int hashCode() { return Objects.hash(id, name, age); } }
UserInfo爲當時筆者要處理的一個pojo對象,由第三方提供,重寫了equals和hashCode方法(當時沒有發現)。而筆者當時要獲取全部的UserInfo對象,放入集合當中進行復雜的邏輯處理,出於可能出現重複對象的考慮(使用遞歸遍歷不一樣部門下的人員信息,可能存在一我的在多個部門),選擇使用HashSet。而後在遍歷HashSet集合a的時候,會將每一個遍歷到的對象加入另外一個集合b做記錄,過後會將a,b作一個差集,取出其中沒有訪問到的元素。這個過程當中可能會涉及到更新某個對象,具體過程簡化以下:
public static void main(String[] args) { // 將對象加入set UserInfo info1 = new UserInfo(1L, "zhangsan", 22); UserInfo info2 = new UserInfo(2L, "lisi", 23); UserInfo info3 = new UserInfo(3L, "wangwu", 24); Set<UserInfo> userInfoSet = new HashSet<>(); userInfoSet.add(info1); userInfoSet.add(info2); userInfoSet.add(info3); // 對訪問到的元素加入集合 List<UserInfo> visited = new ArrayList<>(); visited.add(info1); visited.add(info2); visited.add(info3); // 假設對其中一個對象進行修改 info1.setName("liliu"); // 去掉訪問過的元素 userInfoSet.removeAll(visited); userInfoSet.stream().forEach(System.out::println); }
最後的輸出結果:
UserInfo{id=1, name='liliu', age=22}
是的,全部修改過的元素都沒法移除。當筆者經過debug發現這一現象後馬上就意識到,可能就是hashCode致使被修改過的元素沒法訪問到,爲何呢,咱們能夠回顧一下HashMap的remove操做:
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { // 省略 } return null; }
在HashMap中,是經過Key的hash值來定位桶的位置,當筆者修改了對象的name屬性以後,因爲重寫的hashCode方法裏包括了name這一字段,因此,hash值也發生了改變,致使在對應的桶裏找不到該對象,也就沒法實現remove操做(雖然兩個是同一個引用)。
<br/>
Set的各類底層實現都是對應的Map,熟悉了Map裏的各類方法,相信對於Set的瞭解也會更加深刻。最後謝謝各位園友觀看,若是有描述不對的地方歡迎指正,與你們共同進步!
<br/> <br/>