一、前言
這一篇里,我將對HashSet、LinkedHashSet、TreeSet進行匯總分析,並不打算一一進行詳細介紹,因為JDK對Set的實現進行了取巧。我們都知道Set不允許出現相同的對象,而Map也同樣不允許有兩個相同的Key(出現相同的時候,就執行更新操作)。所以Set里的實現實際上是調用了對應的Map,將Set的存放的對象作為Map的Key。
二、源碼分析
這里筆者就以最常用的HashSet為例進行分析,其余的TreeSet、LinkedHashSet類似,就不贅述了。
2.1 結構概覽
關系也很簡單,實現了Set的接口,繼承了AbstractSet抽象類,抽象類里面定義了集合的常見操作,與我們之前分析過的ArrayList之類的相似。
2.2 成員變量
// HashMap就是HashSet里實現具體操作的對象
private transient HashMap<E,Object> map;
// 將對象作為Value存進去
private static final Object PRESENT = new Object();
由於使用Map進行操作,把E作為Key,要定義一個PRESENT對象作為Value,每個存入的對象都使用它來作為Value。
2.3 構造方法
2.3.1 HashSet()
public HashSet() {
map = new HashMap<>();
}
看了這個,相信園友們應該就知道它是怎么實現的了,我們平時構建HashSet的時候,其實它是在里面new了一個HashMap。
2.3.2 HashSet(Collection<? extends E> c)
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中。
2.3.3 HashSet(int initialCapacity, float loadFactor, boolean dummy)
最后值得一提的是這個構造方法:
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
注意它是包訪問權限的,而不是public,因為這個構造方法是提供給LinkedHashSet使用的,所以里面初始化的也是LinkedHashMap,有興趣的園友們也可以去LinkedHashSet里看一下它的構造方法。
三、Set的使用注意事項
筆者前端時間恰好碰到了因為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操作(雖然兩個是同一個引用)。
四、總結
Set的各種底層實現都是對應的Map,熟悉了Map里的各種方法,相信對於Set的了解也會更加深入。最后謝謝各位園友觀看,如果有描述不對的地方歡迎指正,與大家共同進步!