重写equals之后,师哥一行一行教你重写hashcode方法

  • 作者
  • KING

引言

师哥程序员交流群里,上次那位刚刚入门的兄弟,在看完我为他写的《你真的了解,JAVA中equals方法背后的细节吗?》之后。立马就来告诉我喜讯,他按着师哥的步骤,成功地写出漂亮,正确,完美的equals方法。但俗话说,乐极生悲是有道理的,前话刚落三秒,就有了下面的对话。

兄弟:师哥,我又遇到问题了。。。
师哥:what!!!你怎么这么多问题,你是不是,就是喜欢看师哥,肝技术文章时帅气的模样? 兄弟:不是的,师哥,是真有问题。我把一个元素放进HashMap之后,但取不出来。
师哥:♩ ♪ ♫ ♬ ♭ ♮ ♯
师哥:上次说漏了equals的生死相依的兄弟hashcode。我现在去给你写文章,好了给你说。。。

分析

问题出现了,为了明白发生错误的根本原因。师哥就从事故现场开始,一步一步为大家剖析出那个唯一的真相。

事故现场

我们来看看,这兄弟发给我的这份,有错误的代码。

package wejias.com;

import java.util.HashMap;
import java.util.Map;

public class Student {
	public int id; 			// 学号
	public String name; 	// 学生姓名

	@Override
	public boolean equals(Object obj) {
		if (this == obj) { return true; }
		if (obj == null) { return false; }
		if (getClass() != obj.getClass()) { return false; }
		Student other = (Student) obj;
		if(id != other.id) {
		 return false;
		}
		return true;
	}

	public static void main(String... args) {
		Map<Student,Integer> studentScoreMap = new HashMap<Student,Integer>(2);
		studentScoreMap.put(new Student(1, "师哥"), 10);
		studentScoreMap.put(new Student(2, "程序员伪架师"), 20);
		
		System.out.println("hashMap中查找的结果:   "+studentScoreMap.get(new Student(1, "师哥")));
	}
}

师哥,一看这代码很漂亮呀,和我风格很近呀。。。(尴尬脸),但确实运行起来,就是找不到已经put进Map里面,那个“1,师哥”。

原因分析

  1. 难道是没有put成功???But,一测试,put是成功的。“1,师哥”还在的。 这是什么情况呢,放进去了,为什么取又不取出来呢?

    重写equals之后,师哥一行一行教你重写hashcode方法

2.源码大法。既然找不到原因,我们就去看这个Map的get方法,底层源码到底是如何实现的。

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

获取结点时,是将hash(key)和key两个参数,传入getNode方法。看来,我们还要去看getNode方法。

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

这里我们,注意2个关键的地方。

if (first.hash == hash &&((k = first.key) == key || (key != null && key.equals(k))))
................
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))

这两个地方,我们可以发现,都是先比较hash,如果hash不同,则不进行equals方法判断。那么上面的hash(key),又是怎么样的呢?

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

哎,此处key.hashCode(),好像我们Student类,没有这个hashcode方法。。。

总结

因为,hashmap根据键提取元素时,首先比较 hashcode,然后是 equlas。上面Student类没有实现自己的hashcode方法。而父类Object类的hashcode是根据存储地址计算出来的。那么相同元素的,hashcode有可能不相等,从而导致了上面的问题。

解决

解决方案就是,为Student类在重写equals方法之后,再将hashcode进行重写即可。师哥之前说过,Object类中,所有可以重写方法,都有一定的约束,hashcode也不例外,它约束有以下面3个。

  • 重写equals,必须重写hashcode。
  • 如果equals为true的两个对象,hashcode也必须相等。
  • 如果equals为false的两个对象,hashcode可以相等,也可以不相等。

针对最后一点,作为一个优秀程序员的师哥。需要告诉你的是,一个优秀的hashcode算法,必须尽量让不同元素的hashcode也不相同,以此降低hash冲突,提升效率。

重写hashcode的Tips

师哥为大家总结了以下几点,重写hashcode方法的Tips:

1.参与hashcode计算的字段,应该是equals中用到的字段;

2.equals方法没有用到字段,不需要参与hashcode的计算,减少计算量,提升效率;

3.针对不可变类(final class),可以将hash存为局部变量,从而只计算一次,提升效率。

示例

百说不如一练,下面师哥为上面的Student重写hashcode方法。你看完了,也赶快回去找找,你代码中有没有重写equals方法之后,没有重写hashcode的BUG吧。

public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + id;
  return result;
}	


重写equals之后,师哥一行一行教你重写hashcode方法

引言

师哥程序员交流群里,上次那位刚刚入门的兄弟,在看完我为他写的《你真的了解,JAVA中equals方法背后的细节吗?》之后。立马就来告诉我喜讯,他按着师哥的步骤,成功地写出漂亮,正确,完美的equals方法。但俗话说,乐极生悲是有道理的,前话刚落三秒,就有了下面的对话。

兄弟:师哥,我又遇到问题了。。。
师哥:what!!!你怎么这么多问题,你是不是,就是喜欢看师哥,肝技术文章时帅气的模样? 兄弟:不是的,师哥,是真有问题。我把一个元素放进HashMap之后,但取不出来。
师哥:♩ ♪ ♫ ♬ ♭ ♮ ♯
师哥:上次说漏了equals的生死相依的兄弟hashcode。我现在去给你写文章,好了给你说。。。

分析

问题出现了,为了明白发生错误的根本原因。师哥就从事故现场开始,一步一步为大家剖析出那个唯一的真相。

事故现场

我们来看看,这兄弟发给我的这份,有错误的代码。

package wejias.com;

import java.util.HashMap;
import java.util.Map;

public class Student {
	public int id; 			// 学号
	public String name; 	// 学生姓名

	@Override
	public boolean equals(Object obj) {
		if (this == obj) { return true; }
		if (obj == null) { return false; }
		if (getClass() != obj.getClass()) { return false; }
		Student other = (Student) obj;
		if(id != other.id) {
		 return false;
		}
		return true;
	}

	public static void main(String... args) {
		Map<Student,Integer> studentScoreMap = new HashMap<Student,Integer>(2);
		studentScoreMap.put(new Student(1, "师哥"), 10);
		studentScoreMap.put(new Student(2, "程序员伪架师"), 20);
		
		System.out.println("hashMap中查找的结果:   "+studentScoreMap.get(new Student(1, "师哥")));
	}
}

师哥,一看这代码很漂亮呀,和我风格很近呀。。。(尴尬脸),但确实运行起来,就是找不到已经put进Map里面,那个“1,师哥”。

原因分析

  1. 难道是没有put成功???But,一测试,put是成功的。“1,师哥”还在的。 这是什么情况呢,放进去了,为什么取又不取出来呢?

    重写equals之后,师哥一行一行教你重写hashcode方法

2.源码大法。既然找不到原因,我们就去看这个Map的get方法,底层源码到底是如何实现的。

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

获取结点时,是将hash(key)和key两个参数,传入getNode方法。看来,我们还要去看getNode方法。

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

这里我们,注意2个关键的地方。

if (first.hash == hash &&((k = first.key) == key || (key != null && key.equals(k))))
................
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))

这两个地方,我们可以发现,都是先比较hash,如果hash不同,则不进行equals方法判断。那么上面的hash(key),又是怎么样的呢?

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

哎,此处key.hashCode(),好像我们Student类,没有这个hashcode方法。。。

总结

因为,hashmap根据键提取元素时,首先比较 hashcode,然后是 equlas。上面Student类没有实现自己的hashcode方法。而父类Object类的hashcode是根据存储地址计算出来的。那么相同元素的,hashcode有可能不相等,从而导致了上面的问题。

解决

解决方案就是,为Student类在重写equals方法之后,再将hashcode进行重写即可。师哥之前说过,Object类中,所有可以重写方法,都有一定的约束,hashcode也不例外,它约束有以下面3个。

  • 重写equals,必须重写hashcode。
  • 如果equals为true的两个对象,hashcode也必须相等。
  • 如果equals为false的两个对象,hashcode可以相等,也可以不相等。

针对最后一点,作为一个优秀程序员的师哥。需要告诉你的是,一个优秀的hashcode算法,必须尽量让不同元素的hashcode也不相同,以此降低hash冲突,提升效率。

重写hashcode的Tips

师哥为大家总结了以下几点,重写hashcode方法的Tips:

1.参与hashcode计算的字段,应该是equals中用到的字段;

2.equals方法没有用到字段,不需要参与hashcode的计算,减少计算量,提升效率;

3.针对不可变类(final class),可以将hash存为局部变量,从而只计算一次,提升效率。

示例

百说不如一练,下面师哥为上面的Student重写hashcode方法。你看完了,也赶快回去找找,你代码中有没有重写equals方法之后,没有重写hashcode的BUG吧。

public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + id;
  return result;
}