前言
为什么会有HashSet,其特点是什么?解决了什么问题?接下来我们就学习HashSet,以及其源码。
上帝视角
因为HashSet的实现方式是使用HashMap,其最大特点就是无序不可重复。在下面源码中可以看到,HashSet操作的都是new出的"HashMap"类型的成员变量map。在HashSet存值得过程中是允许为null的,和HashMap一样是非同步的
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
源码分析
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
//序列版本号
static final long serialVersionUID = -5024744406713321676L;
//不需要被序列化的map变量
private transient HashMap<E,Object> map;
// 与支持映射中的对象关联的虚拟值
private static final Object PRESENT = new Object();
//创建一个map实例,Map初始大小为16,负载因子为0.75
public HashSet() {
map = new HashMap<>();
}
//创建一个HashMap,初始容量为Math.max((int) (c.size()/.75f) + 1, 16)
//为什么是(int) (c.size()/.75f) + 1和16中取一个;这里要知道Hashmap扩容机制,Hashmap中的容量达到阈值就扩容为原来的两倍,阈值=宿数组容量*0.75;
//(c.size()/.75f) + 1,反过来就是容纳这个数据的总长度
//使用16是因为初始容量必须是2的指数倍数,当int) (c.size()/.75f) + 1得到的不是一个2的指数倍数,hashMap会重新计算一个符合条件的初始长度,这里设置16算是一个优化
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
//创建一个指定长度和负载因子的HashMap
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
boolean add(E e)
public boolean add(E e) {
//因为hashMap的put()方法成功的情况下返回null,如果hashMap返回null那么HashSet的add方法就成功了
return map.put(e, PRESENT)==null;
}
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
...
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1 判断当前table是否为空,第一次会为空在resize()中就会创建一tablle,下次就不为空
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2 (n - 1) & hash 计算的到一个数组下标i,取出tab[]数组中i 的值p,如果为空,就直接在tab[i]上进行赋值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//3 如果当前下标i上已经有值,就可能产生哈希冲突或者是相同元素,
Node<K,V> e; K k;
//4 如果hash相等,并且key相等或者key.eques相等,则说明是重复元素,直接覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//5 如果不是同一元素,产生冲突前判断p节点是否是树结构
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//6 这里就是说不是树,但是产生了hash冲突,开始要循环遍历树了
for (int binCount = 0; ; ++binCount) {
//7 首先判断p的next节点是否有数据
if ((e = p.next) == null) {
//8 没有数据直接将新数据添加到p的next节点上
p.next = newNode(hash, key, value, null);
//9 当链表长度大于8就转成红黑树处理
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//10 如果可key存在就直接覆盖
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
//11 一直循环到p.next 为空,或者有key存在就跳出循环
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);//这里的方法使用使用多态,如果是LinkedHashMap就会调用其相关代码
return oldValue;
}
}
++modCount;
//12 如果超过最大容量threshold就扩容,threshold在初始化的时候是没有赋值的,在第1步resize()扩容的时候进行了赋值
if (++size > threshold)
resize();
afterNodeInsertion(evict);//这里的方法使用使用多态,如果是LinkedHashMap就会调用其相关代码
return null;
}
...
}
boolean remove(Object o)
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
...
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
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;
//如果添加过数据table就是有值的
//首先判断table不为空并且tab长度>0,并且tab[index]下标的值不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//tab[index]的p.hash相等,key相等,或者key不为空且equals相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//获得了对应的值,可以删除了
node = p;
else if ((e = p.next) != null) {//比如上面hash相同key不同,就是冲突了。冲突后,下面判断是否已经是红黑树
if (p instanceof TreeNode)
//是树,取出值
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
//不是,还是根据hash和key同时相等,或者key不为空,key的equals相等,取出值
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
//循环到e.next不为空,找到后跳出
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//如果是树
if (node instanceof TreeNode)
//走删除树逻辑
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)//不是树,判是否为空
tab[index] = node.next;//设置空
else
p.next = node.next;//将关联进行交换
++modCount;
--size;//hashMap的长度减少
afterNodeRemoval(node);
return node;
}
}
return null;
}
...
}
iterator()
HashSet的迭代器只使用HashMap的key,在 KeySet
类的iterator()方法中调用KeyIterator
的next()
方法返回nextNode().key
public Iterator<E> iterator() {
return map.keySet().iterator();
}
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
...
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
//具体返回调用KeyIterator的方法,其next返回的是key
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (Node<K,V> e : tab) {
for (; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
...
}
重写equal()方法
这里提到重写equal()方法,是基于一个场景,当我们将两个对象存储到HashSet中时,对于数据的唯一性,同一个类生成的对象,如果具有相同的属性,那么我们希望HashSet只存储一个,但结果可能和我们想的不一样,看下面代码
MyBean m = new MyBean(1, 1, "2");
MyBean m2 = new MyBean(1, 1, "2");
HashSet h = new HashSet();
h.add(m);
h.add(m2);
System.out.println(h.size());
对于对象m和m2,我们认为是同一数据,但是HashSet并不能区分会保存两个,这里要看一下hashMap判读重复数据的方法(e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
,这里要先说明下==号对比和equals对比;
- ==:等号比较的是两个对象的内存地址是否相等,也就是比较的是否是一个对象
- equals:比较的是两个对象是否相等,但对于equals方法实现方式分为两种情况
1、
没有重写equals()方法,使用的是父类Object的方法,比较对象使用==对比
2、
如String类重写equals()方法,会比较两个对象相关属性值;
Object类中的equals方法
public boolean equals(Object obj) {
return (this == obj);
}
回到HashMap的方法,或方法两边一边使用((k = e.key) == key
判断对象相等,一边使用key.equals(k)
比较,显然在 m和m2对象不相等,MyBean类没有实现重写equals情况下key.equals(k)
也是不相等的。所以HashSet会添加两个数据;
要想保证只能HashSet能够区分,我们需要在MyBean模型中重写equals()方法,判断对象的值是否相等;在此我们需要了解HashCode的协定
1.equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠。
2.hashCode()相等的两个对象他们的equal()不一定相等,就是hashCode()不是绝对可靠。
这说明在重新MyBean的equal()方法的时候需要重写hashCode()方法;
重写后的MyBean()如下:
public class MyBean {
@MyAnnotation(age = 10, value = 20)
private int value;
@MyAnnotation(age = 40, value = 30)
private int age;
@MyAnnotations(name = "张珊")
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public MyBean(int value, int age, String name) {
super();
this.value = value;
this.age = age;
this.name = name;
}
public MyBean() {
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
// 如果是同一个对象返回true,反之返回false
if (this == obj) {
return true;
}
// 判断是否类型相同
if (this.getClass() != obj.getClass()) {
return false;
}
MyBean person = (MyBean) obj;
return name.equals(person.name) && age == person.age && value == person.value;
}
@Override
public int hashCode() {
int nameHash = name.toUpperCase().hashCode();
return nameHash ^ age;
}
}
对于重写hashCode()是是一个技术活,重写的好坏会影响在Hash表中的性能,可以参考String类的hashCode()方法