HashSet 具有不重复的特性,来专门学习一下。
实体类:StudyDTO
public class StudyDTO {
public StudyDTO() {
}
public StudyDTO(int id, String name) {
this.id = id;
this.name = name;
}
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
测试代码:
public static void testHashSet(){
//看对象是否重写了equals和hashCode,未重写的时候,比较的就是this == obj
HashSet hashSet = new HashSet();
hashSet.add(new StudyDTO(1,"a"));
hashSet.add(new StudyDTO(1,"a"));
hashSet.add(new StudyDTO(1,"a"));
hashSet.add(new StudyDTO(2,"a"));
hashSet.add(new StudyDTO(3,"b"));
System.out.println(hashSet.size());
}
首先,来看一下new HashSet()部分的源码:
public HashSet() {
map = new HashMap<>();
}
其它方式的构造仅仅是参数跟随变化,无大的区别。
仅从这一句new HashMap,就知道原因了,HashMap中的key是不允许重复的,而HashSet中存放的对象,就是HashMap中的key。
来看具体的分析:
hashSet.add(new StudyDTO(1,"a")); ①
public boolean add(E e) { ②
//private static final Object PRESENT = new Object(); 这就是一个空对象
return **map.put(e, PRESENT)**==null;
}
public V put(K key, V value) { ③
return putVal(hash(key), key, value, false, true);
}
调用顺序:
测试代码中的①->HashSet中的②->HashMap中的③
而putVal,即为HashMap中也是非常核心的存储方法,换言之,所谓HashSet的put调用,就是HashMap中有K同V的put方法。(HashMap中的putVal方法非常牛X,大家有空了可以去学学)。
再来看一下putVal的处理方式(我在上一篇博客中也有写这一部分,如果感兴趣,可以去看看):
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//初次进入的时候,进行正常的扩容操作
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//进行哈希判定,看传入对象的hashCode是否与当前哈希桶中存在,不存在,就直接存储,存在,则走else
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//hashCode和equals相同,就进行替换
//这里需要重点看一下,直接使用 测试操作 hashSet.add(new StudyDTO(1,"a")) 存储的时候,每次都是一个新的值,不会进行冲突判定
//但是当重写了hashCode和equals以后,就会发现,用hashSet.add(new StudyDTO(1,"a")) 存储时,不论多少次,都是一个,并进入下面的替换。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//被冲突的数据是红黑树中的数据,进行替换
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//被冲突的数据为链表时,进行替换
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//当链表长度到8位时,转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
hashSet调用进行了说明,其它地方同样也会触碰,不过就不再详细说明了。
通过上面的代码可以看到,处理的全是hash方法处理过了的key,value的作用就是被存储,并未对其进行什么具体的操作。
而在HashSet的put方法中,对象均是以key的形势存储进行,value=new Object(),所以它完美的调用了HashMap中的操作。
而putVal中的操作,又 保证了K的不重复性。
所以HashSet中可以存放一个null以及对象不可以重复,就有了解释。
通过以上说明,就可以知道,当执行测试代码时,可以得到结果为:5
因为每个new StudyDTO都是一个新对象,它的hashCode每次都是一个新值,在哈希桶中不重复。
再对StudyDTO进行添加hashCode和equals方法:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StudyDTO studyDTO = (StudyDTO) o;
return id == studyDTO.id &&
Objects.equals(name, studyDTO.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
这时,可以得到结果为:3
其原因就是上面所说明的,调用putVal方法时的处理,hashCode与equal均相等,
前面的new StudyDTO(1,“a”)会被后面的覆盖,结果自然为3.
新人学习源码,总有一种雾里看花的感觉,不够透彻,如有地方写的不对,请各位大神指出。