Java中的equals()和hashCode()

概述

在我们使用类集框架(比如使用hashMap、hashSet)的时候,经常会涉及到重写equals()和hashCode()这两个方法。

这两个方法的联系是:
1. 如果两个对象不同,那么他们的hashCode肯定不相等;
2. 如果两个对象的hashCode相同,那么他们也未必相等。

所以说,如果想在hashMap里面让两个不相等的对象对应同一个值,首先需要让他们的hashCode相同,其次还要让他们的equals()方法返回true,因此为了达到这个目的,我们就只能重写hashCode()和equals()这两个方法了。

引用一篇文章的解说:The idea behind a Map is to be able to find an object faster than a linear search. Using hashed keys to locate objects is a two-step process. Internally the Map stores objects as an array of arrays. The index for the first array is the hashcode() value of the key. This locates the second array which is searched linearly by using equals() to determine if the object is found.

大致意思就是:使用Map比线性搜索要快。Map存储对象是使用数组的数组(可以理解为二维数组,这并不准确,不过可以按照这个理解。之前hashMap用的是数组,每个数组节点对应一个链表,现在Java 8已经把链表改成了treeMap,相当于一个二叉树,这样检索的时候比链表更快,尤其是在最坏情况下,由原来链表的O(n)变成了二叉树的O(logn),详见https://dzone.com/articles/hashmap-performance),所以搜索大致是分为两步的,第一步是根据hashCode寻找第一维数组的下标,然后根据equals的返回值判断对象是第二维数组中的哪一个。

例证

举个栗子——

import java.util.HashMap;

public class Apple {
    private String color;

    public Apple(String color) {
        this.color = color;
    }

    public static void main(String[] args) {
        Apple a1 = new Apple("green");
        Apple a2 = new Apple("red");

        //hashMap stores apple type and its quantity
        HashMap<Apple, Integer> m = new HashMap<Apple, Integer>();
        m.put(a1, 10);
        m.put(a2, 20);
        System.out.println(m.get(new Apple("green")));
    }
}

程序的运行结果为:null
此时,我们已经向hashMap里面存储两个对象了,且a1就是green Apple,那么为什么我们通过”green”去查找却返回null呢?
显然,后来我们新new出来一个对象,这和之前加入的a1绿苹果那个对象绝对不是同一个对象,根据终极父类Object中的hashCode()的计算结果,其返回值绝对是不一样的。

所以——

第一步:重写hashCode()

我们需要先让“凡是color属性相同的对象,其hashCode都一样”,所以我们可以这样重写hashCode():

public int hashCode(){
    return this.color.length();
}

这里我们使用color属性的内容的长度作为hashCode的大小,那么凡是green的苹果,hashCode肯定都是5(green字符串长度为5),这样一来,属性相同的对象的hashCode肯定都相同了。这只是保证了一维数组的下标找到了(姑且这样理解),还需要找第二维的下标呢,这个需要在第二步中解决。在解决第二步之前,你可能会有问题——这样一来的话,如果有black苹果(假设有black),那么它的hashCode也变成了5了啊,和green一样了。这个同样靠第二步解决。

第二步:重写equals()

我们已经知道,如果想让两个对象一样,除了让他们的hashCode值一样外,还要让他们的equals()函数返回true,两个都符合才算一样。所以第二步我们要重写equals()函数,使“只要color一样,两个苹果就是相同的”:

public boolean equals(Object obj) {
    if (!(obj instanceof Apple))    //首先类型要相同才有可能返回true
        return false;   
    if (obj == this)    //如果是自己跟自己比,显然是同一个对象,返回true
        return true;
    return this.color.equals(((Apple) obj).color);    //如果颜色相同,也返回true
}

这样一来,根据最后一句话,凡是颜色相同的苹果,第二维也映射到同一个位置了(姑且这么理解)。这样一来,就可以根据颜色在hashMap里寻找苹果了。
把我们重写过的hashCode()和equals()加入到之前的代码中,便会输出结果:10,即键a1所对应的值。

结语

感觉这篇文章涉及的内容还是相当基础和重要的。文章到此也差不多可以结束了,另附上以前学习时记的笔记,感觉还是挺有用的,我自己的笔记自己看起来自然是毫无障碍的,不过实在不想整理了,就直接贴上来吧,大家将就将就看看吧,可以作为上面内容的唠叨和补充。

附录1

HashSet在存储的时候(比如存的是字符串),则存进去之后按照哈希值排序(也就意味着遍历的时候得到的顺序不是我们添加的顺序,即乱序),如果第二个对象和第一个Hash值一样但是对象不一样,则第二个会链在第一个后面。在添加对象的时候,add()返回boolean型,如果添加的对象相同(比如两个相同的字符串),则返回false,添加失败。

HashSet如何保证元素的唯一性?
通过元素的方法——hashCode()和equals()来实现。
如果两个元素的hashCode不同,直接就存了;
如果两个元素的hashCode相同,则调用equals判断是否为true,true则证明是同一个对象,就不存了,false的话证明是不同的对象,存之。

一旦自定义了对象,想要存进HashSet,则一定要覆写hashCode()和equals()方法——
比如我们定义Person类,仅含有name,age两个参数,规定:只要姓名和年龄相同,就断定为“同一个人”,则不能存入HashSet,否则的话可以。
对于这种情况,如果我们new出来几个人,其中存在名字和年龄相同的,则均会存入HashSet,原因就是这些对象是不同的对象,所占内存不一样,则通过hashCode()返回的哈希值也都不一样,所以理所当然的存入了HashSet。为了避免把“同一个人”存进HashSet,我们首先需要让hashCode()针对“同一个人”返回相同的哈希值,即覆写hashCode()方法!

public int hashCode(){
    return 110;
}

这样自然也可以,不过没有必要让所有的对象返回的哈希值都一样,只要“同一个人”的哈希值一样就行了,所以写成这样更好:

public int hashCode(){
    return name.hashCode() + age;
}

这样的话“同一个人”返回的哈希值就是相同的。不过这样还是不够完美,因为覆写的这个hashCode()虽然会让“相同的人”返回相同的哈希值,但也可能会让“不同的人”返回相同的哈希值,比如两个人name不同,age不同,但name的哈希值加上age恰恰相同,这样的话就坑爹了。为了避免这种现象,让这种哈希值恰巧撞上的概率进一步减小,我们写成这样会更好:

public int hashCode(){
    return name.hashCode() + age * 19;
}

最好是乘上一个素数之类的,可以大大降低“不同的人”的哈希值撞上的概率。
通过以上hashCode()函数的覆写,我们让“相同的人”的哈希值相同了,那么接下来就要覆写equals()函数!因为Java碰到哈希值相同的情况之后,接下来要根据equals()函数判断两个对象是否相同,相同则不再存入HashSet,不同的话就存进去,且是链在和它哈希值相同的对象上的(链成一串儿)。

附录2:以下是TreeSet内容,和上面关系不大了

TreeSet可以对里面的元素进行排序,比如如果对象是字符串,则按照字典序排序。

如果要存自定义对象,需要让自定义的对象具有比较性,这样的话TreeSet才能将其按照一定的顺序去排序。否则会报出异常。为了让自定义对象能够具有比较性,对象需要实现Comparable接口。
比如,有一个Person类,我们要new出来一些人放到TreeSet里面,为了使对象具有可比性从而能够存入TreeSet,我们规定按照对象的年龄排序。
首先,让Person类实现Comparable接口,覆写接口的compareTo()方法,其返回值和c语言的strcmp()相同:

这样的话,如果某两个对象的年龄相同,则后来者将不会被存入TreeSet,因为后来者被认为和之前的那个是同一个对象。
所以我们对主关键字排序之后一定要对次关键字进行排序,只有所有的关键字都比较完毕还是返回0,我们才能认为两个对象相同。
所以覆写的compareTo()应该是这样的:

public int compareTo(Object obj){   //传进来的需要是Object类型,这一点要注意
    if(!(obj instanceof Person)){   //传进来的对象不对,直接抛出异常
        throw new RuntimeException("Not The Same Kind Of Object!");
    }

    Person p = (Person)obj;         //将对象向下转型

    if(this.age < p.age)
        return -1;
    if(this.age > p.age)
        return 1;
    if(this.age == p.age)
    {
        return this.name.compareTo(p.name);     //String类中覆写过Comparable接口的空方法compareTo(),按照字典序对字符串进行排序
    }
}

因此,如果你想让TreeSet按照输入顺序存数据,而不是自动排序,可以这样覆写compareTo()方法:

public int compareTo(Object obj){
    return 1;
}

很简单,不过缺陷就是“相同的人”也会被存进去。这个函数是完全按照输入内容的顺序不加以任何删改原模原样原顺序存进TreeSet的。
同理,如果return -1就是输入顺序的逆序;如果return 0则只能存入第一个输入的对象。

以上是TreeSet排序的第一种方法——让元素自身具有比较性(让类实现Comparable接口,覆写compareTo()方法)。
不过这种方法有缺陷,比如我突然不想按照年龄排,想按照姓名排序,这就需要重新修改代码了。

所以TreeSet排序的第二种方法——让集合自身具有比较性(将自定义的比较器传入Treeset的构造函数)。
比如我们现在要按照人名字典序排序:
首先建造一个比较器,实现Comparator接口,覆写compare()方法(返回值也和strcmp()一样):

public PersonCompareMethod implements Comparator{
    public int compare(Object obj1, Object obj2){       //传入两个Object对象
        Person p1 = (Person)obj1;                       //向下转型
        Person p2 = (Person)obj2;

        int num = p1.getName().compareTo(p2.getName()); //直接比较两个字符串的字典序,因为String类已经覆写过Comparable接口的空方法compareTo(),按照字典序对字符串进行排序
        if(num == 0){
            return new Integer(p1.getAge()).compareTo(new Integer(p2.getAge()));
                    //这一句话比较巧妙!!!本来我们是可以直接按照数字大小比较年龄这个次要关键字的,不过在比较的时候,我们换了一种比较高端的方法:新建了两个Integer对象,因为Integer类里面也有compareTo()方法,将数字按照字典序进行比较。当然是实现了Comparable接口并覆写compareTo()方法才具有这样的功能
        }
        return num;
    }
}

之后,将我们的构造器传入TreeSet新建对象时调用的构造函数即可:
TreeSet ts = new TreeSet(new MyCompareMethod());

使用第二种方式的情况:
对象不具有比较性,或者是比较性并不是我们所需要的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在 Java equals() 和 hashCode() 是两个重要的方法,用于判断对象是否相等和哈希值的计算。 equals() 方法用于比较两个对象是否相等。默认情况下,它使用 == 运算符比较对象的引用,即比较两个对象是否指向同一个内存地址。如果想比较两个对象的属性是否相等,就需要重写 equals() 方法,并根据对象的属性进行比较。 hashCode() 方法用于计算对象的哈希值,即将对象映射为一个整数。哈希值在 Java 经常用于散列表等数据结构,用于快速查找和比较对象。如果两个对象相等,它们的哈希值应该相同。因此,重写 equals() 方法的同时,也需要重写 hashCode() 方法。 在重写 equals() 和 hashCode() 方法时,需要遵循一些规则。比如,如果两个对象相等,它们的 hashCode() 方法应该返回相同的值;反之,如果两个对象的 hashCode() 值相等,它们不一定相等,还需要通过 equals() 方法进行比较。此外,hashCode() 方法不能依赖于对象的内存地址或时间戳等不稳定因素,应该根据对象的属性计算哈希值。 ### 回答2: JavaequalshashCode是非常重要的两个方法,在Java几乎所有的类都会覆盖它们,而且它们是非常重要的类比较和哈希计算的方法。 equals方法用于比较两个对象是否相等,通常比较两个对象的内容是否相同,他们是否具有相同的属性和值。 equals方法的默认实现是比较两个对象的引用是否相同,即两个对象是否是同一个对象,不过这个默认的实现并不能满足所有的需求,因为有时候即使两个对象的引用不同,但他们的内容依然相同,所以需要重写equals方法来实现内容的比较。 另一方面,hashCode方法则是将一个对象映射成一个整型的哈希值,通常用于快速查找或比较对象。 在Java,哈希表是非常常见的数据结构,而哈希值就是在哈希表用来寻找和比较对象的唯一标识,所以hashCode方法的实现对于哈希表的性能有着非常重要的影响。 需要注意的是,hashCode方法必须和equals方法保持一致性,也就是说如果两个对象的equals方法返回true,那么他们的hashCode方法返回的哈希值必须相同,反之亦然。 在实现equals方法时,一般会遵循一些基本原则,例如对称性,传递性,一致性等,同时还需要注意适当的判断null值和使用instanceof进行类型判断。在实现hashCode方法时,需要确保相同的对象始终返回相同的哈希值,同时也需要尽可能的让不同的对象返回不同的哈希值。 总之,JavaequalshashCode方法是非常重要的两个方法,深入理解其实现原理和使用方式,可以有效提高Java应用程序的性能和稳定性。 ### 回答3: Javaequals()和hashCode()是两个非常重要的方法,它们用于判断对象之间的相等性和排序性。 equals()方法是用来比较两个对象是否相等的,它的作用是比较两个对象的内容是否相等,而不是比较两个对象引用是否相等。在默认情况下,equals()方法会比较两个对象的引用,即判断两个对象是否指向同一个内存地址。但是,我们通常需要自己来重写equals()方法,以便在比较对象时只比较对象的内容。 hashCode()方法是Java的哈希函数,它将对象映射到一个整数值。这个整数值通常用于将对象存储到哈希表,可以快速地定位对象。如果两个对象相等,那么它们的hashCode()方法返回的整数值也应该相等。因此,在重写equals()方法时,我们也需要重写hashCode()方法。 在Java,如果两个对象调用equals()方法返回true,那么它们的hashCode()方法返回的整数值也必须相等。因此,重写hashCode()方法时,必须保证相等的对象返回相等的hashCode()值,否则将会影响哈希表的性能。同时,hashCode()方法的重写也应该考虑到对象的内容,以便产生尽可能不同的哈希值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值