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());

使用第二种方式的情况:

对象不具有比较性,或者是比较性并不是我们所需要的。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-13 02:57:22

Java中的equals()和hashCode()的相关文章

看Java中==、equals、hashCode的来龙去脉

我有一个哥们去参加了面试,面试官这样问一个Java问题: 你说一下java对象的equals方法调用什么方法呢?我这个哥们想了想,回答说"应该是比较的引用".听了这个答案之后,那个面试官摇头晃脑的说:"不对,你回答的不对,equals方法调用的是hashCode方法".于是乎,我那个技术还不错的哥们就悲壮地栽在这道题目上了. 今天晚上,西安历史上少有的热,那就好好总结一下这个题目的来龙去脉好了,也方便给后面的朋友们提个醒,不要栽在这么简单的问题上.你说这个面试官回答

Java中的equals和hashCode方法

本文转载自:Java中的equals和hashCode方法详解 Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要重写这两个方法. equals()和hashCode()方法是用来在同一类中做比较用的,尤其是在容器里如set存放同一类对象时用来判断放入的对象是否重复.这里我们首先要明白一个问题: equals()相等的两个对象,hashcode()一定相等,equals()不相等的两个对象,却并不能证明他们的h

Java 中的 ==, equals 与 hashCode 的区别与联系

一.概述 1.概念 == : 该操作符生成的是一个boolean结果,它计算的是操作数的值之间的关系 equals : Object 的 实例方法,比较两个对象的content是否相同 hashCode : Object 的 native方法 , 获取对象的哈希值,用于确定该对象在哈希表中的索引位置,它实际上是一个int型整数 二.关系操作符 == 1.操作数的值 基本数据类型变量 在Java中有八种基本数据类型: 浮点型:float(4 byte), double(8 byte) 整型:byt

Java中的equals和hashCode方法详解

Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要重写这两个方法,今天就来介绍一些这两个方法的作用. equals()和hashCode()方法是用来在同一类中做比较用的,尤其是在容器里如set存放同一类对象时用来判断放入的对象是否重复. 这里我们首先要明白一个问题: equals()相等的两个对象,hashcode()一定相等,equals()不相等的两个对象,却并不能证明他们的hashcode()不相等.换

【转】Java中==、equals、hashcode的区别与重写equals以及hashcode方法实例

原文地址:http://www.cnblogs.com/luankun0214/p/4421770.html 感谢网友的分享,记录下来只为学习. 1.重写equals方法实例   部分代码参考http://blog.csdn.net/wangloveall/article/details/7899948 重写equals方法的目的是判断两个对象的内容(内容可以有很多,比如同时比较姓名和年龄,同时相同的才是用一个对象)是否相同 如果不重写equals,那么比较的将是对象的引用是否指向同一块内存地址

Java中==、equals、hashcode的区别与重写equals以及hashcode方法实例

1.重写equals方法实例   部分代码参考http://blog.csdn.net/wangloveall/article/details/7899948 重写equals方法的目的是判断两个对象的内容(内容可以有很多,比如同时比较姓名和年龄,同时相同的才是用一个对象)是否相同 如果不重写equals,那么比较的将是对象的引用是否指向同一块内存地址,重写之后目的是为了比较两个对象的value值是否相等.特别指出利用equals比较八大包装对象(如int,float等)和String类(因为该

java中==和equals和hashcode的区别详解

一.相同点 都是用来进行值或对象的比较. 二.不同点 对于“==”而言,对于基本类型(char,byte,short,int,long,float,double,boolean),对比的是值,所以是相等的,对于引用对象,对比的是引用的对象的堆地址,例如: public class Main { public static void main(String[] args) throws InterruptedException { Object obj1 = new Object(); Objec

java中 == 和equals的使用与区别

java 的数据类型分为“基本数据类型” 和“引用数据类型”:   --->在基本数据类型的比较中,== 比的就是基本数据类型变量中所保存的值. --->在引用数据类型的比较中,== 才比较的是变量所指向的对象的地址. hashCode()返回该对象的哈希码值,该值通常是一个由该对象的内部地址转换而来的整数,它的实现主要是为了提高哈希表. 我们以引用数据类型为例,因为基本类型比较的是值,简单无须多言. 封装好的String 类型为例: (注意:ObjectL中有方法equals, hashco

【转】彻底弄懂Java中的equals()方法以及与&quot;==&quot;的区别

彻底弄懂Java中的equals()方法以及与"=="的区别 一.问题描述:今天在用Java实现需求的时候,发现equals()和“==”的功能傻傻分不清,导致结果产生巨大的偏差.所以,我决定花费时间把equals()和“==”的功能彻底弄懂,前事不忘后事之师嘛,分享给大家,希望对大家理解equals()和“==”的功能有所帮助. 二.分析探索解决问题的方法:1.Object 中的equals()方法: (1)通过查找API,说明如下: equalspublic boolean equ