上来先了解一下HashSet这东西是个什么来头
public class HashSet
extends AbstractSet
implements Set, Cloneable, java.io.Serializable{...}
继承自AbstractSet抽象类,实现了Set接口。该集合内无重复元素且遍历是无序的。基本操作就是add跟remove那些方法,基本不变。
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
说实话,当我点开的时候我心里有点平静,甚至还差点笑出声,你确定就是这样?
我一再劝说自己应该是看少了些什么,但是命运就是如此,当我点开构造方法的时候心里也就释然了
public HashSet() {
map = new HashMap<>();
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(Collection extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
HashSet的底层由一个HashMap来实现,默认大小为0,而参数为集合的时候,默认大小会在集合数量与0.75的倍数加1,同16之间取最大值;且其增删查改都是基于内部维护的HashMap来做对应的操作。因为其底层基于HashMap,可以确定HashSet的遍历也是无序的。但是这样子我们就由疑问了,HashMap允许空键(当然只允许存在一个),也允许不同键内多个空值,但是HashSet只允许非重复元素呀。如果是这样的话,衍生了4个问题:那如果说HashSet的底层是HashMap的话,究竟我们add方法执行的时候,存的元素是在key-value的哪一个?怎样存?怎样避免重复值保存?还有那HashSet允许空值么?我们带着这个问题来看add方法
我们点开add方法一脸的思密达,于是我们再看了看remove。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
此时我颤抖的嘴角强行微笑,因为,他真的是用map来操作,足见第一二个问题的答案,HashSet调用add方法其实就是将元素作为key,将HashSet内部维护的一个标示对象存入map中,但是这里的比较是否等于null是几个意思?就这样来实现去重判断?
这里我们需要回顾一下HashMap的put方法,因为HashSet本身就是对HashMap的又一层封装,如果不懂的需要回头去看一下我在《java HashMap 底层实现和源码分析》里面分享的内容。这里贴出HashMap的put方法。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);
for (HashMapEntry e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
private V putForNullKey(V value) {
for (HashMapEntry e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry e = table[bucketIndex];
table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
size++;
}
到这里,我们可以看到,HashMap在做put操作的时候,其实是已经做对应结果返回了。
如果是未存在的key,那么就创建Entry单向链表保存value值(只是在HashSet的环境下我们只关注key就可以,value的值是什么无意义),创建完成返回null;如果是已经存在的key,那么就修改该key对应的hash值在哈希table中的下标Entry,最后返回旧值。
第三个问题的答案便是:HashSet只需要根据HashMap返回的结果,就可以知道现在add所传的对象是否已经存在,并将结果返回。所以,即使HashSet调用add方法返回了false,其实内部交由HashMap去执行时候也是执行过一次put操作了,只是插入的值没有变而已
最后一个问题我想大家也都猜到了,HashSet允许用add插入null值。
感觉HashSet寥寥数言,这里顺便介绍下LinkedHashSet吧
不错,唯一成员就是序列化的id
private static final long serialVersionUID = -2851667679971038690L;
剩下的就是构造函数
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(Collection extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
可以确定的是,对内部维护的HashMap的加载因子是使用默认的0.75,且默认的Entry数组大小根据不同的情况确定,无参则是16,有参则是在集合数量的2倍,同11之间取最大值