直接进入正题,HashSet是通过组合模式,使用HashMap的key是不重复的来实现HashSet中的元素是不重复的,遍历时为该map对象的keySet()即map的key集合。而HashMap通过hash函数和key实现,具体实现见HashMap,篇幅有限这里就不展开说明了。
以下为HashSet的源码,HashSet有一个全局唯一的PRESENT指向的Object对象,add的时候使用其作为map的value。
现在有疑问的地方就是,这里为什么不用null来代理PRESENT比直接使用new Object()还能节约空间?
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
其中奥秘就在以下使用PRESENT的两个方法的返回值上
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
两个值都是boolean类型,map.put和remove方法的处理逻辑均是,key有关联的值则返回对应的值,否则返回null。
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V remove(Object key)....
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value)...
直接上代码验证,以下代码为HashSet的源码拷贝版,只保留了主要方法。
public class MyHashSet<E>
extends AbstractSet<E>
implements Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
// private static final Object PRESENT = new Object();
private static final Object PRESENT = null;
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public MyHashSet() {
map = new HashMap<>();
}
/**
* Returns an iterator over the elements in this set. The elements
* are returned in no particular order.
*
* @return an Iterator over the elements in this set
* @see ConcurrentModificationException
*/
public Iterator<E> iterator() {
return map.keySet().iterator();
}
/**
* Returns the number of elements in this set (its cardinality).
*
* @return the number of elements in this set (its cardinality)
*/
public int size() {
return map.size();
}
/**
* Returns <tt>true</tt> if this set contains no elements.
*
* @return <tt>true</tt> if this set contains no elements
*/
public boolean isEmpty() {
return map.isEmpty();
}
/**
* Returns <tt>true</tt> if this set contains the specified element.
* More formally, returns <tt>true</tt> if and only if this set
* contains an element <tt>e</tt> such that
* <tt>(o==null ? e==null : o.equals(e))</tt>.
*
* @param o element whose presence in this set is to be tested
* @return <tt>true</tt> if this set contains the specified element
*/
public boolean contains(Object o) {
return map.containsKey(o);
}
/**
* Adds the specified element to this set if it is not already present.
* More formally, adds the specified element <tt>e</tt> to this set if
* this set contains no element <tt>e2</tt> such that
* <tt>(e==null ? e2==null : e.equals(e2))</tt>.
* If this set already contains the element, the call leaves the set
* unchanged and returns <tt>false</tt>.
*
* @param e element to be added to this set
* @return <tt>true</tt> if this set did not already contain the specified
* element
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
/**
* Removes the specified element from this set if it is present.
* More formally, removes an element <tt>e</tt> such that
* <tt>(o==null ? e==null : o.equals(e))</tt>,
* if this set contains such an element. Returns <tt>true</tt> if
* this set contained the element (or equivalently, if this set
* changed as a result of the call). (This set will not contain the
* element once the call returns.)
*
* @param o object to be removed from this set, if present
* @return <tt>true</tt> if the set contained the specified element
*/
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
/**
* Removes all of the elements from this set.
* The set will be empty after this call returns.
*/
public void clear() {
map.clear();
}
/**
* Returns a shallow copy of this <tt>HashSet</tt> instance: the elements
* themselves are not cloned.
*
* @return a shallow copy of this set
*/
@SuppressWarnings("unchecked")
public Object clone() {
try {
MyHashSet<E> newSet = (MyHashSet<E>) super.clone();
newSet.map = (HashMap<E, Object>) map.clone();
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
}
针对以上代码执行测试案例:
Set myHashSet = new MyHashSet();
System.out.println(myHashSet.add("zhangsan"));
System.out.println(myHashSet.add("zhangsan"));
System.out.println(myHashSet.add("zhangsan"));
System.out.println(myHashSet.remove("zhangsan"));
System.out.println(myHashSet.remove("zhangsan"));
System.out.println(myHashSet.remove("zhangsan"));
修改PRESENT为null之后,输出结果为
true
true
true
true
true
true
结果显然不符合要求,无法区分key在HashSet是否存在,使用PRESENT的理论是,使用一个对象,remove和add,key存在时操作返回true,key不存在时返回false,可以对key是否存在作出区分。
当PRESENT为null时,是无法区分的。使用一些空间可以换取操作的有效信息,操作上可以更加灵活,因此是有价值的,到此就回答了最初的问题。
最后抛出一个问题,MyHashSet没有像HashSet一样实现Set接口,因为AbstractSet已经implements Set接口,从刚才的使用上来看MyHashSet和HashSet并无区别,那为什么HashSet还有再实现Set接口,欢迎大家说出自己的看法。