定义语句结构:HashSet<数据类型> 定义变量=HashSet<>();
注意一:一般默认返回的是集合的toString()结果。二:hashCode()是把地址只要字符串相同,字符串的hashCode()是一样的如上是直接赋值,但俩次的对象地址是一样的,add方法不允许,是怎么执行的呢?(到最好再给分析代码,先列出来可能遇到的情况)
##重写hashCode,equals法之前:
俩次的hashCode值输出不一样,是因为String类重写了hashCode方法,比较的是对象中存储的字符串,而上面自定义的Stu中没有重写hashCode方法,比较的是对象,因为创建了俩个对象tom和jim,自然结果不同。 思考:当比较的对象类型不同时结果怎样,如下试验:
@ hashCode的作用:
Java中的集合有两类,一类是List,集合中元素是有序的,元素可以重复。另一个是Set,集合中元素无序,但元素不允许重复。equals方法可以避免重复,但如果集合重元素很多,每增加一个元素就要遍历,效率很低,于是有了哈希表原理:
哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。这样一来,当集合要添加新的元素时,调用这个元素的HashCode方法,就一下子能定位到它应该放置的物理位置上。
(1)如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;
(2)如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了;
(3)不相同的话,也就是发生了Hash key相同导致冲突的情况,那么就在这个Hash key的地方产生一个链表,将所有产生相同HashCode的对象放到这个单链表上去,串在一起。这样一来实际调用equals方法的次数就大大降低了。
所以hashCode在上面扮演的角色为寻域(寻找某个对象在集合中区域位置)。hashCode可以将集合分成若干个区域,每个对象都可以计算出他们的hash码,可以将hash码分组,每个分组对应着某个存储区域,根据一个对象的hash码就可以确定该对象所存储区域,这样就大大减少查询匹配元素的数量,提高了查询效率。
基于以上定义要实现上面要的结果,要重写hashCode方法与equals方法:
(1),当只重写了hashCode方法时会有什么结果呢?如下:
明显,set集合中元素还是重复,下面再来看看也重写equals方法能不能实现:
很明显,必须要重写俩个方法才能实现定义的结果。 【注意】上面在自定义类Stu,dog中在重写equals方法时先做了类型判断,如果从传入参数类型属于要比较的对象的类型或其子类类型要下转型为相同类型,再返回进行比较后的结果,否则类型不同,无法比较,返回false。
现在进行add的底层代码详细分析:
一:没有重写任何方法,首先ctrl+点击HashSet进入:
解释:在Test类中 创建HashSet对象,调用了该类中无参构造方法——>执行了该构造方法中map = new HashMap<>();——>map为HashSet全局变量。
ctrl+点击第一个add方法会进入下面的页面:
再次点击return的方法,出现:
返回值又调用了hash(key)方法,点击:
没有重写hashCode方法时,hash(key)方法的返回值根据key的不同是不同的,
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;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
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);
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;
}
第一次传入tom时,方法中的hash是有tom.hashCode()编写又进行相关加工的十进制数值,进入if语句,table是全局变量。初始值是null,会执行语句n=(tab=resize()).length。又调用了下面方法:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
点击常量蓝色字体进入下面:可知 是16个结点的 长度大小。
注意到把他赋值给了newCap,后面又 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;可知是将数组table经由newTable(局部变量),变成了一个空间长度为16的数组,此时二者是相等的。再经由p = tab[i = (n - 1) & hash]
会把tab数组中下标为i的结点赋值给p,第一次是null进入下面的语句把tom的对象传进去。掠过else语句直接来到return null,add方法的返回值为true,添加成功。进行第二次add时,同样上面过程,但hash(key)方法返回值不同,因为没有重写hashCode方法,此时的key是jim,相应对应另一个值,再来分析putval方法,table(全局变量)在上一次执行的时候与tab指向同一个首地址,在上一次tab[i]中已经存有了tom的对象,所以这次的table中不为null,进入下一个if语句,由于p = tab[i = (n - 1) & hash]这次的hash值是根据jim编写的,在tab数组中这次的i值与上次不同,在对应的空间是null,下面语句把jim对象传进来,掠过else语句直接来到return null,同样添加成功。
二:只是重写了hashCode方法:
在putval方法中,由于重写了hashCode方法使得只要对象中内容相同即返回值相同,则俩次的hash(key)方法的返回值一样,hash一样,在进行第二次传值的时候p = tab[i = (n - 1) & hash]结点与上一次的是同一个空间不为null,进入else语句,会进行判断:
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
tom不等于jim,来到||后,现在没重写equals方法,不通,进入下一个else,类型不同,进入下一个else,在if语句中e值为空,但把jim对象传给了p.next,break;
if (e != null) {
不满足,此时e是空,直接返回null,添加成功。
三:重写了hashCode方法和equals方法:
在第二次添加时到下面代码时:
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
是成立的 ,因为第二次运行到下面时
if ((p = tab[i = (n - 1) & hash]) == null)
已经把上一次tom的节点给p了,判断不为null,在下面又把p 的地址给了e,此时;
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
是成立的,直接返回有值,自然add方法添加失败。