首先,hashcode与equals并不是一定要一起重写的。
先说结论:在用到哈希相关的集合时,作为key的类一定要重写hashcode与equals方法,因为这些集合在计算下标时,使用到了key的hashcode方法,并且在判断key是否已经存在时,使用到了equals方法。如果不重写会允许多个相同的key插入,因此需要同时重写。
但是在其他场景中,equals方法的使用更加频繁,并不必须使用hashcode,允许只重写单个方法。但是还是强烈建议同时重写,官方在equals方法上也有相关的说明:It is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.
以下是一个简单的案例分析,分析为什么Hash集合需要同时重写hashcode 与 equals
1.先说没有重写的情况:
hashcode方法:对于两个对象a b,无论这两个对象的属性是否相同,两个对象通过hashcode方法得到的哈希值是不同的(排除哈希碰撞的情况)。
equals方法:a.equals(b) 即为判断a b是否为同一个对象,底层就是return a==b。
设置一个类Student
public void HashHash(){
Student a= new Student("小明", 123);
Student b= new Student("小明", 123);
System.out.println(a.hashCode());//输出1866161430
System.out.println(b.hashCode());//输出2024918163
}
static class Student{ String name; int id; Student(){}; Student(String name,int id){ this.name=name; this.id=id; } }
计算得到a的hashcode为1866161430,b的为2024918163。可见即使属性一致,只要对象不同,hashcode就不同
2.为什么要一起重写?
一起重写的主要的应用场景在哈希相关的集合上。
以HashMap为例子,他的底层是通过一个Node数组加上链表与红黑树实现的。
1 static class Node<K,V> implements Map.Entry<K,V> { 2 final int hash; 3 final K key; 4 V value; 5 Node<K,V> next; 6 7 Node(int hash, K key, V value, Node<K,V> next) { 8 this.hash = hash; 9 this.key = key; 10 this.value = value; 11 this.next = next; 12 } 13 14 public final K getKey() { return key; } 15 public final V getValue() { return value; } 16 public final String toString() { return key + "=" + value; } 17 18 public final int hashCode() { 19 return Objects.hashCode(key) ^ Objects.hashCode(value); 20 } 21 22 public final V setValue(V newValue) { 23 V oldValue = value; 24 value = newValue; 25 return oldValue; 26 } 27 28 public final boolean equals(Object o) { 29 if (o == this) 30 return true; 31 32 return o instanceof Map.Entry<?, ?> e 33 && Objects.equals(key, e.getKey()) 34 && Objects.equals(value, e.getValue()); 35 } 36 }
四个属性为:int hash即哈希值,K key传入的key值,V value传入的value值,Node<K,V> next指向下一个节点(链表or红黑树使用)
当传入一个新的对象时,HashMap通过该对象的hash值计算该对象在数组中对应的位置。
//计算key的哈希值,需要调用key自己的hashcode方法
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
//得到hash后,该对象在Node数组中的下标为i = (n - 1) & hash 其中n是Node数组的长度,是2的整数次方,该计算下标实际上就是取模,取hash的后log2 n位
由于HashMap<key , value>要求集合中的key具有唯一性,集合中不能存在两个key相同的对象。这里的“相同”指的是数值上的相同,并非一定要是同一个对象,只要属性相同就行。而如果key没有重写hashcode方法,会导致即使是两个属性完全相同的对象,计算得到的两个hash也不同。这就导致插入这两个对象。还以Student为例。
@Test public void HashHash(){ Student a= new Student("小明", 123); Student b= new Student("小明", 123); HashMap<Student,String> map=new HashMap<>(); map.put(a,"学生a"); map.put(b,"学生b"); System.out.println(map); }
得到结果{com.nys.LeetcodeTest$Student@78b1cc93=学生b, com.nys.LeetcodeTest$Student@6f3b5d16=学生a}
可以看到,没有重写hashcode方法时,即使a b两个对象属性相同,还是全部存入了hashmap中。这就违背了hashmap的原则。
所以,如果使用哈希相关的集合,必须重写key的hashcode方法。
那么我们就先把hashcode给重写了
@Test public void HashHash(){ Student a= new Student("小明", 123); Student b= new Student("小明", 123); HashMap<Student,String> map=new HashMap<>(); map.put(a,"学生a"); map.put(b,"学生b"); System.out.println(map); } static class Student{ String name; int id; Student(){}; Student(String name,int id){ this.name=name; this.id=id; } @Override public int hashCode() { int result = 1; result = 31 * result + (name == null ? 0 : name.hashCode()); result = 31 * result + (Integer.valueOf(id) == null ? 0 : Integer.valueOf(id).hashCode()); return result; } }
重写key的hashcode过后,输出,发现map里还是有两个元素!{com.nys.LeetcodeTest$Student@165f43d=学生a, com.nys.LeetcodeTest$Student@165f43d=学生b}
这是为啥呢?不都重写了吗?怎么还这样?
且慢。不要忘了,HashMap的底层不仅仅是一个Node数组,还有链表与红黑树,hashmap对于key是否相同的判断不仅仅使用到了hashcode,同时用到了equals!重写hashcode后,虽然解决了属性相同的对象哈希值不一致的问题,但是在表中,还是会发生碰撞。当map中已经有了a,在加入b时,就会使得a.next=b,形成一个链表。因此重写hashcode并不能完全满足要求。
看看HashMap是如何添加新元素的吧
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; //Node数组,HashMap的底层存储
Node<K,V> p;
int n, i;
//判断是否为空,为空则resize()扩容到16 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
//计算得到key对应的下标i
//若tab[i]为空则可以直接插入到这 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else {
//tab[i]不为空 Node<K,V> e; K k;
//判断key与Hashmap中的元素的key是否一致 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);
//与tab[i]不一致,并且不是红黑树,因此只能是链表
//遍历链表,寻找key相同的节点
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; }
//找到了就是e if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } }
//e为空,则代表没有找到key值,可以直接插入
//e非空,代表找到相应的节点。进入下面的if,直接修改value即可
if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount;
//插入后判断是否需要扩容(size达到0.75*table.length) if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
可以顺便复习一下hashmap底层,版本是jdk17。可以看到,判断的核心代码是
p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
对于key相同的判断,不仅仅需要判断hash的一致性(通过hashcode移位以及异或运算得到),key相同还需要满足这俩引用的是同一个对象或者这俩equals为true
而由于equal没有重写,因此在刚刚的测试中,上面这一段判断等价于
p.hash == hash && ((k = p.key) == key || (key != null && key == k))
也就是判断了两次是否为同一对象,由于a b本来就是不同的,因此都插入到map中了。
通过模拟HashMap计算下标的方法,可以得到a和b的下标都是8(仅重写hashcode)
@Test public void HashMapIndex(){ Student a= new Student("小明", 123); Student b= new Student("小明", 123); int initialCap=16; int hashA=hash(a); int hashB=hash(b); int indexA=(initialCap - 1) & hashA; int indexB=(initialCap - 1) & hashB; System.out.println("HashMap初始化的长度"+ initialCap);//HashMap初始化的长度16 System.out.println("a的hashcode:"+a.hashCode()+" a的hash:"+hashA+" a的index:"+indexA);//a的hashcode:23458877 a的hash:23459160 a的index:8 System.out.println("b的hashcode:"+b.hashCode()+" b的hash:"+hashB+" b的index:"+indexB);//b的hashcode:23458877 b的hash:23459160 b的index:8
} static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
当hashcode与equals都没重写时
HashMap初始化的长度16 a的hashcode:2024918163 a的hash:2024911906 a的index:2 b的hashcode:107241811 b的hash:107243319 b的index:7
现在我们补上hashcode与equals方法
再次执行,得到结果如下
{com.nys.LeetcodeTest$Student@165f43d=学生b}
终于
标签:hash,key,int,equals,hashcode,Student,Java From: https://www.cnblogs.com/nys1999/p/17984833