HashSet 是 AbstractSet的子类
这里使用泛型编程
HashSet有一个成员是private transient HashMap<E,Object> map;
(不被序列化)
HashSet的构造函数都是使用HashMap的各种构造方式对map变量进行初始化。
HashSet的size、isempty等函数都是直接调用HashMap里面的
从源码上分析:
为什么Set 接口实例存储的是无序的,不重复的数据。
而List 接口实例存储的是有序的,可以重复的元素。
以HashMap为例,key是不能重复的,因为key相当于是map的索引,但是value是可以重复的,如果要put的key,已经有了,key不变,value会更新。
set就是使用HashMap的构造函数进行构造,使用HashMap的putVal函数进行put。
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);//AbstractCollection类的一个方法
}
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))//重点就在于add函数
modified = true;
return modified;
}
HashSet重写了add函数
public boolean add(E e) {
return map.put(e, PRESENT)==null;//调用HashMap的put函数
}
HashMap的put函数,需要传入key和value两个参数
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
传入的value是一个Object对象PRESENT,putVal函数中只是在判断key是否相同。所以set的元素,如果是相同的,则会存储到同一个位置。
putVal函数的解析参考这个链接:
https://blog.csdn.net/weixin_44893585/article/details/103638927
putVal函数判断key是否相同依赖于equals和hashcode方法
要实现自动去重,需要重写equals和hashcode,因为在putVal里面判断元素是否相等就是靠equals和hashcode进行比较。
就是利用下面这一句进行判断,
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
原来的Node是p,要放入的Node是e,如果p的hash值和要e的hash相同并且p的key==或者equals e的key,则说明Node已经存在了。
是先用hash找到索引,然后在索引那个桶里,用equals逐项比较。
这里的hash值可以看下面函数,是hash(key)的返回值
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
hash函数的返回值是key的hashcode和hashcode带符号右移16位异或的结果。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hashCode()函数的返回值取决于key所属的类是怎样实现hashcode的。
常见的类对hashcode的实现见链接:
https://blog.csdn.net/weixin_44893585/article/details/103638755
所以,如果要put的key是自己定义的类,那么要在自己的类里重写hashcode函数,否则会调用默认的hashcode,是c实现的本地方法,如下。
public native int hashCode();
同样的,如果自己写的类不能实现equals函数,那么会调用Object类的equals函数,也就是下面
public boolean equals(Object obj) {
return (this == obj);
}
而 == 号对于八种基本数据会比较值的大小,对于其他类的对象,会比较存储地址。同一个类的两个相同的实例对象,会储存在不同的存储空间,如果仅用 == 号来比较,会返回false,所以需要重新写equals方法,是一种判断值相同的办法。
这能才能真正的实现自动去重。
比如下面例子
String s1=new String("hello");
String s2=new String("hello");
System.out.println(s1.equals(s2));//true
System.out.println(s1 == s2);//false
这是因为String类重写了equals方法,如下
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}//首先判断内存地址相同
if (anObject instanceof String) {
String aString = (String)anObject;
if (!COMPACT_STRINGS || this.coder == aString.coder) {
return StringLatin1.equals(value, aString.value);
}
}//COMPACT_STRINGS一直为true
//所以必须要判断string的编码方式是否相同
//coder是UTF16或者是LATIN1
//编码方式相同则调用StringLatin1.equals函数
return false;
}
StringLatin1.equals函数是对两个String的value进行比较。JDK9之后,String在构造的时候会把值保存在byte[]里面,为了节省内存。
StringLatin1.equals实现如下:
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}
所以byte[]的每一个元素都相等,才能判断为value相等。