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

  • 作者
  • 戈寒
  • 2020-06-11 12:30:18
  • 阅读  20

引言

师哥程序员交流群里,上次那位刚刚入门的兄弟,在看完我为他写的《你真的了解,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方法

  • 2020-06-11 12:30:18
  • 阅读  20

引言

师哥程序员交流群里,上次那位刚刚入门的兄弟,在看完我为他写的《你真的了解,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;
}	


诗与远方

  • 请你用慈悲心和温和的态度
    把你的不满与委屈说出来
    别人就容易接受得多地多

诗与远方

  • 诚实的面对你内心的矛盾和缺点
    不要欺骗你自己

诗与远方

  • 成功要爬上梯子才能到达
    双手插在口袋里的人是爬不上去的

诗与远方

  • 良心是每一个人最公正的审判官
    你骗得了别人
    却永远骗不了你自己的良心

诗与远方

  • 不要因为小小的争执
    疏远了你的至亲好友
    也不要因为小小的怨恨
    忘记了别人的恩情

诗与远方

  • 凡是能多站在别人的角度着想
    就能做到,理解,体谅

诗与远方

  • 当幻想和现实面对时
    总是很痛苦的
    要么你被痛苦击倒
    要么你把痛苦踩在脚下

诗与远方

  • 梯子的梯阶从来不是用来搁脚的
    它只是让人们的脚踏上去
    以便让另一只脚能够再往上攀登

诗与远方

  • 毁灭一个人只要一句话
    培植一个人却要千句话
    所以请你多多口下留情

诗与远方

  • 财以不炫为富,官以不显为贵
    名以不彰为誉,施以不报为惠

诗与远方

  • 世界原本就不是属于你
    因此你用不着抛弃
    要抛弃的是一切的执着
    万物皆为我所用,但非我所属

诗与远方

  • 只要自觉心安,东西南北都好
    如有一人未度,切莫自觉逃了

诗与远方

  • 崇高的理想就像生长在高山上的鲜花
    如果要摘下它
    勤奋才是攀登的途径

诗与远方

  • 人之谤我也
    与其能辩,不如能容
    人之侮我也
    与其能防,不如能化

诗与远方

  • 不要在你的智慧中夹杂着傲慢
    不要使你的谦虚缺乏智慧

诗与远方

  • 看透大事者超脱,看不透者执着
    看透小事者豁达,看不透者计较

诗与远方

  • 坚韧是成功的一大要素
    只要在成功之门上敲得够久够大声
    终会把成功唤醒

诗与远方

  • 根本不必回头去看咒骂你的人是谁
    如果有一条疯狗咬了你一口
    难道你也要趴下去反咬它一口吗

诗与远方

  • 交有道之人,莫结无义之友
    饮清净之茶,莫贪花色之酒
    开方便之门,闲是非之口

诗与远方

  • 谦虚但不自卑
    自信但不自大
    自由但不放纵
    人一生很难做到这三点

诗与远方

  • 活着一天,就是有福气,就该珍惜
    当我哭泣没有鞋子穿的时候
    我发现有人没有脚……

诗与远方

  • 不要让追求之舟停泊在幻想的港湾
    而应扬起奋斗的风帆
    驶向现实生活的大海

诗与远方

  • 不要刻意去猜测他人的想法
    如果你没有智慧与经验的正确判断
    通常都会有偏差的

诗与远方

  • 心中装满自己的看法与想法的人
    是听不见别人的声音的

诗与远方

  • 要了解一个人
    只需要看他的出发点与目的地是否相同
    就可以知道他是否真心

诗与远方

  • 一个人如果不能从内心去原谅别人
    那他就放不下怨恨,得不到快乐的生活

诗与远方

  • 你不要一直不满人家
    你应该一直检讨自己才是
    不满人家,是苦了你自己

诗与远方

  • 你硬要把单纯的事情看得很复杂
    那你会很痛苦

诗与远方

  • 当你劝告别人时
    若不顾及别人的自尊心
    那么再好的言语都是没有用的

诗与远方

  • 一份耕耘,一份收获,付出就会有回报
    不曾遭遇过失败,因为一直往成功方向发展
    所碰到的都是暂时的挫折

诗与远方

  • 同样的瓶子
    你为什么要装毒药呢
    同样的心理
    你为什么要充满着烦恼呢

诗与远方

  • 把气氛的心境转化为柔和
    把柔和的心境转化为爱
    如此,这个世间将更加完美

诗与远方

  • 说话不要有攻击性
    不要有杀伤力
    不夸已能,勿扬人恶,自然能化敌为友

诗与远方

  • 如果你不给自己烦恼
    别人也永远不可能给你烦恼
    因为你不会放在自己的心上

诗与远方

  • 懦弱的人只会裹足不前
    莽撞的人只能引火烧身
    只有真正勇敢的人才能所向披靡

诗与远方

  • 多一分心力去注意别人
    就少一分心力反省自己

诗与远方

  • 有时候我们要冷静问问自己
    我们再追求什么
    我们活着为了什么

诗与远方

  • 彩云飘在空中,自然得意洋洋
    但最多智能换取几声赞美
    唯有化作雨并倾注于土壤之中
    才能给世界创造芳菲

诗与远方

  • 当你快乐时你要想,这快乐不是永恒的
    当你痛苦时你要想,这痛苦也不是永恒的

诗与远方

  • 快乐是一份自然
    做自己想做的事
    做好自己选择的事
    自然地做人,自然地笑,自然地生活

诗与远方

  • 狂妄的人有救
    自卑的人没有救
    认识自己,相信自己,改变自己
    才能改变别人对你的态度

诗与远方

  • 只要永不放弃,持之以恒
    每次挫折,都是你进步的阶梯
    如果你逃避退缩,那就等于自毁前途

诗与远方

  • 用伤害别人的手段来掩饰自己缺点的人是可耻的

诗与远方

  • 玩像玩的,干像干的
    人生苦短,能享受时就享受,能轻松时就轻松
    不要跟自己过不去,要保持一种良好的心境

诗与远方

  • 责人要含蓄,忌太尽
    劝人要委婉,忌太直
    警人要疑似,忌太真

诗与远方

  • 你一定要宽恕众生
    不论他有多坏,甚至伤害过你
    你只有放下了,才能得到真正的快乐

诗与远方

  • 要是面前有一堵墙
    不要轻易退缩逃避
    要想办法绕过去,超越过去
    即使有困难也不要轻易放弃

诗与远方

  • 势不可使尽,聪明不可用尽
    福不可享尽,便宜不可占尽

诗与远方

  • 当你对自己诚实的时候
    世界上没有人能够欺骗得了你

诗与远方

  • 心是最大的骗子
    别人能骗你一时
    而它却会骗你一辈子

诗与远方

  • 大多数的人一辈子只做了三件事
    自欺,欺人,被人欺

诗与远方

  • 一个人如果没有感受过苦难
    就不会体会到他人的苦难
    你要学救苦救难的精神,就得先受苦受难

诗与远方

  • 每一个人都拥有生命
    但并非每个人都懂得生命,珍惜生命
    不了解生命的人,体会不到生命的价值

诗与远方

  • 生活可以是甜的,也可以是苦的
    但不能是没味的
    你可以胜利,也可以失败
    但你不能屈服

随意打赏