HashSet:
HashSet背后主要是一个HashMap在支持。HashSet的元素都作为HashMap每一对key-value的Key来存储,每个Key的Value都等于PRESENT。
以下是HashSet的部分源代码:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, 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();
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
public Iterator<E> iterator() {
return map.keySet().iterator();
}
public int size() {
return map.size();
}
public boolean isEmpty() {
return map.isEmpty();
}
public boolean contains(Object o) {
return map.containsKey(o);
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
public void clear() {
map.clear();
}
}
有上面的源码可以看出HashSet的相关操作都是HashMap的操作,元素不重复主要是通过HashMap来实现。因为HashMap的Key是不允许重复的,所以就保证了HashSet的元素不重复。那么这里对重复的判断是怎样实现的?那就得看看HashMap的实现。
HashMap:
内部主要有一个Entry<K,V>类型的数组table,即
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
transient int size;
int threshold;
final float loadFactor;
transient int modCount;
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
实际上java容器都是自动增长类型的数据结构,他们实现自动增长的方式都类似,都是通过capacity和loadFactor来动态调整容器大小。当达到loadfactor的数据比例时候,容器申请2倍更大的空间,然后将原来的数据拷贝到新空间中。但是当数据量变小,并不会自动缩小。因此对于提前预知数据量很大的时候,可以直接先设置capacity的初始值大一点,以防止自动增长时候内存拷贝的开销。如果提前预知数据量很小,那么就不需要设置很大的capacity以免浪费内存。
table数组的类型是Entry<K,V>我们来看看这是什么数据结构。
以下是Entry的部分源代码:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
void recordAccess(HashMap<K,V> m) {
}
void recordRemoval(HashMap<K,V> m) {
}
}
Entry是由final Kkey;V value;Entry<K,V>next;int hash;组成的。其实就是一个链表的结点,数据域有hash,key,value,指针域是next(当然java中是引用)。也就是HashMap是一个数组链表法解决hash冲突实现的hash结构。这里的key是final类型,是不可以改变的;也就是说一旦你put一个新key,那么他就不能再改变指向了。
Entry的equals方法已经被重写了,当且仅当两个对象都是Entry对象且key和value同时相等时才相等。
hashCode方法也被重写成key和value的hashCode异或值。这是为什么呢?似乎HashMap并没有对Entry的比较,HashMap比较的都是Entry.key和Entry.hash。可能有的代码通过HashMap.entrySet()方法得到Entry集合,需要比较里面的Entry。HashMap.Entry是接口Map.Entry的实现类,需要重写这两个方法以便确保两个Entry的正确比较。
知道了table的结构,就来看看主要操作put和remove的实现,这里只介绍put部分源代码:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> 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;
}
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
private V putForNullKey(V value) {
/*这里之所以是循环,是因为可能还有其他非空key也会映射到0地址处*/
for (Entry<K,V> 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;
}
private void putForCreate(K key, V value) {
int hash = null == key ? 0 : hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}
createEntry(hash, key, value, i);
}
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
/*头插法,先保存头table[bucketIndex],在将新Entry的next域指向为table[bucketIndex],最后将头指向新Entry*/
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
如果table还是空的,如果第一次调用put,这时候table首先会生成一个16个元素大小的hash表。也就是调用时才申请空间copy on write。
然后分以下情况:
a.key不是空且key为null,则放入null。这里说明HashMap支持nullkey。并且所有的nullkey都放在table中0地址中。这也说明HashSet可以存放一个null元素。HashMap也只能存放一个nullkey的map,再次存入肯定value会被覆盖。
b.以上情况都不是,那么计算key的hash值。每个对象的hashCode方法默认都是本地方法。其实本地方法hashCode返回的就是对象的地址值。hashMap里面的hash函数,实际上是分两种情况处理, 1.String对象,那么直接sun.misc.Hashing.stringHash32((String)k);2.其他对象,先算出hashCode,然后再映射到table数组下标。然后搜索是否存在e使得e.hash==hash和e.key==key同时成立。这里可以看出,即使是不同的key,也有可能最终得到相同的index。如果存在,那么修改value即可,不存在则生成一个新的entry加入。因此如果想保证hashSet或者HashMap只放入内容不重复的元素,必须同时重写hashCode方法和equals方法。
通过返回关注内容的hashCode和比较关注内容(这里关注内容可以是对象的某些属性)重新定义hashCode和equals方法。
关于头插法的示意图:
下面是一个小测试代码:
package test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
class Person {
String Id;
String name;
Person(String id, String name){
this.Id = id;
this.name = name;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return Id + ":" + name;
}
@Override
public int hashCode() {
// TODO Auto-generated method stub
return this.name.hashCode()^this.Id.hashCode();
}
@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
if (obj instanceof Person ) {
Person p = (Person)obj;
if (this.Id == p.Id || (this.Id != null && this.Id.equals(p.Id)))
return true;
else
return false;
}
return false;
}
}
public class JavaFscan {
public static void main(String args[]){
Map<Person,String> m = new HashMap<Person,String>();
Set<Person> s = new HashSet<Person>();
Person p1 = new Person("1", "ki");
Person p2 = new Person("2", "ki");
Person p3 = p1;
Person p4 = new Person("4", "qi");
s.add(p1);
s.add(p2);
s.add(p3);
s.add(p4);
//s.add(null);
//m.put(null,null);
//m.put(null, "si");
//m.put(null, "ti");
m.put(p1, "1");
m.put(p2, "2");
m.put(p3, "3");
Iterator<Person> its = s.iterator();
Set<Map.Entry<Person, String>> sets = m.entrySet();
Iterator<Map.Entry<Person, String>> itm = sets.iterator();
while(its.hasNext()){
Person p = its.next();
p.Id= "1";
}
while(itm.hasNext()){
Map.Entry<Person, String> e = itm.next();
Person pp = e.getKey();
pp.Id = "1";
}
System.out.println(m.get(p1));
System.out.println(s);
System.out.println(m);
}
}
总结:
1.HashSet可以支持null元素,但最多放一个,HashSet不支持重复元素,因为元素是内部HashMap的Key;
2.HashSet由HashMap支持,HashMap支持null,但最多只能有一个NULL Key map;
3.判断HashSet或者HashMap元素重复,可以重写元素的hashCode和equals方法,比较需要关心的内容;
4.不要试图修改HashSet里面对象元素的内容,这样可能会导致修改后和其中一个已经存在的元素相等的情况,从而造成下次查询HashMap不知道返回哪一个;