Java 细品 重写equals方法 和 hashcode 方法

简介: Java 细品 重写equals方法 和 hashcode 方法

前言



在我们需要比较对象是否相等时,我们往往需要采取重写equals方法和hashcode方法。


该篇,就是从比较对象的场景结合通过代码实例以及部分源码解读,去跟大家品一品这个重写equals方法和hashcode方法。


正文



场景:


我们现在需要比较两个对象  Pig 是否相等  。


而Pig 对象里面包含 三个字段, name,age,nickName  ,我们现在只需要认为如果两个pig对象的name名字和age年龄一样,那么这两个pig对象就是一样的,nickName昵称不影响相等的比较判断。


代码示例:


Pig.java:


/**
 * @Author : JCccc
 * @CreateTime : 2020/4/21
 * @Description :
 **/
public class Pig {
    private  String name;
    private Integer age;
    private String nickName;
    public Pig() {
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public String getNickName() {
        return nickName;
    }
    public void setNickName(String nickName) {
        this.nickName = nickName;
    }
}


可以看到上面的Pig对象,没有重写equals方法 和 hashcode 方法,那么我们去比较两个Pig对象,就算都设置了三个一样的属性字段,都是返回 false:


    public static void main(String[] args) {
        Pig pig1=new Pig();
        pig1.setName("A");
        pig1.setAge(11);
        pig1.setNickName("a");
        String name= new String("A");
        Pig pig2=new Pig();
        pig2.setName(name);
        pig2.setAge(11);
        pig2.setNickName("B");
        System.out.println(pig1==pig2);  //false
        System.out.println(pig1.equals(pig2)); //false
        System.out.println(pig1.hashCode() ==pig2.hashCode()); //false
    }


为什么false? 很简单,因为pig1和pig2都是新new出来的,内存地址都是不一样的。


== : 比较内存地址  ,那肯定是false了 ;


equals: 默认调用的是Object的equals方法,看下面源码图,显然还是使用了== ,那就还是比较内存地址,那肯定是false了;


image.png


hashCode: 这是根据一定规则例如对象的存储地址,属性值等等映射出来的一个散列值,不同的对象存在可能相等的hashcode,但是概率非常小(两个对象equals返回true时,hashCode返回肯定是true;而两个对象hashCode返回true时,这两个对象的equals不一定返回true;  还有,如果两个对象的hashCode不一样,那么这两个对象一定不相等!)。


一个好的散列算法,我们肯定是尽可能让不同对象的hashcode也不同,相同的对象hashcode也相同。这也是为什么我们比较对象重写equals方法后还会一起重写hashcode方法。接下来会有介绍到。


好的,上面啰嗦了很多,接下来我们开始去重写equals方法和hashCode方法,实现我们这个Pig对象的比较,只要能保证name和age两个字段属性一致,就返回相等true。


首先是重写equals方法(看上去我似乎写的很啰嗦吧,我觉得这样去写更容易帮助新手去理解):


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Pig pig = (Pig) o;
        boolean nameCheck=false;
        boolean ageCheck=false;
        if (this.name == pig.name) {
            nameCheck = true;
        } else if (this.name != null && this.name.equals(pig.name)) {
            nameCheck = true;
        }
        if (this.age == pig.age) {
            ageCheck = true;
        } else if (this.age != null && this.age.equals(pig.age)) {
            ageCheck = true;
        }
       if (nameCheck && ageCheck){
           return true;
       }
       return  false;
    }


稍微对重写的代码做下解读,请看图:


image.png

事不宜迟,我们在重写了equals后,我们再比较下两个Pig对象:

image.png


可以看到,到这里好像已经能符合我们的比较对象逻辑了,但是我们还需要重写hashCode方法。


为什么?


原因1.


通用约定,


image.png


翻译:


/*请注意,通常需要重写{@code hashCode}
*方法,以便维护
*{@code hashCode}方法的常规约定,它声明
*相等的对象必须有相等的哈希码。
*/


原因2.


为了我们使用hashmap存储对象 (下面有介绍)


没错,就是文章开头我们讲到的,相同的对象的hashCode 的散列值最好保持相等, 而不同对象的散列值,我们也使其保持不相等。


而目前我们已经重写了equals方法,可以看到,只要两个pig对象的name和age都相等,那么我们的pig的equals就返回true了,也就是说,此时此刻,我们也必须使两个pig的hashCode 的散列值保持相等,这样才是对象相等的结果。


事不宜迟,我们继续重写hashCode方法:


    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + name.hashCode();
        result = 31 * result + age;
        return result;
    }


然后我们再比较下两个pig对象:


image.png


也就是说,最终我们重写了equals和hashCode方法后, Pig.java:


/**
 * @Author : JCccc
 * @CreateTime : 2020/4/21
 * @Description :
 **/
public class Pig {
    private  String name;
    private Integer age;
    private String nickName;
    public Pig() {
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public String getNickName() {
        return nickName;
    }
    public void setNickName(String nickName) {
        this.nickName = nickName;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Pig pig = (Pig) o;
        boolean nameCheck=false;
        boolean ageCheck=false;
        if (this.name == pig.name) {
            nameCheck = true;
        } else if (this.name != null && this.name.equals(pig.name)) {
            nameCheck = true;
        }
        if (this.age == pig.age) {
            ageCheck = true;
        } else if (this.age != null && this.age.equals(pig.age)) {
            ageCheck = true;
        }
       if (nameCheck && ageCheck){
           return true;
       }
       return  false;
    }
    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + name.hashCode();
        result = 31 * result + age;
        return result;
    }
}


看到这里,应该有不少人觉得,重写怎么有点麻烦,有没有简单点的模板形式的?

 

有的,其实在java 7 有在Objects里面新增了我们需要重新的这两个方法,所以我们重写equals和hashCode还可以使用java自带的Objects,如:


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Pig pig = (Pig) o;
        return Objects.equals(name, pig.name) &&
                Objects.equals(age, pig.age);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }


那么如果还是觉得有点麻烦呢?


那就使用lombok的注解,让它帮我们写,我们自己就写个注解!


导入lombok依赖:


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>


然后在Pig类上,使用注解:


image.png


然后编译后,可以看到lombok帮我们重写了equals和hashcode方法:


image.png

image.png


其实如果咱们使用到了lombok的话,用@Data注解应该基本就够用了,它结合了@ToString,@EqualsAndHashCode,@Getter和@Setter的功能。


ps:再啰嗦几句, 我们自己重写了hashCode方法后,能够确保两个对象返回的的hashCode散列值是不一样的,这样一来,


在我们使用hashmap 去存储对象, 在进行验重逻辑的时候,咱们的性能就特别好了。


为啥说这样性能好, 我们再啰嗦地补充一下hashmap在插入值时对key(这里key是对象)的验重:


HashMap中的比较key:


先求出key的hashcode(),比较其值是否相等;


-若相等再比较equals(),若相等则认为他们是相等 的。


-若equals()不相等则认为他们不相等。


如果只重写hashcode()不重写equals()方法,当比较equals()时只是看他们是否为 同一对象(即进行内存地址的比较),所以必定要两个方法一起重写。


而HashSet,


用来判断key是否相等的方法,其实也是调用了HashMap加入元素方法,再判断是否相等。


image.pngimage.png


好的,该篇介绍就到此吧。

 

ps:其实没有结束,我还想啰嗦一下,因为我们在重写hashcode方法的时候,我们看到了一个数字 31 。 想必会有很多人奇怪,为什么要写个31啊?


这里引用一下《Effective Java》 里面的解释:


之所以使用 31, 是因为他是一个奇素数。
如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算(低位补0)。
使用素数的好处并不很明显,但是习惯上使用素数来计算散列结果。 
31 有个很好的性能,即用移位和减法来代替乘法,
可以得到更好的性能: 31 * i == (i << 5)- i, 
现代的 VM 可以自动完成这种优化。这个公式可以很简单的推导出来。


那么可能还有眼尖的人看到了,为什么还要个数字17?  


这个其实道理一样,在《Effective Java》里,作者推荐使用的就是 基于17和31的散列码的算法 ,而在Objects里面的hash方法里,17换做了1 。  


好吧,这篇就真的到此结束吧。

相关文章
|
3天前
|
Java 数据处理 数据库
Java中equalsIgnoreCase方法的应用
Java中equalsIgnoreCase方法的应用
|
3天前
|
存储 安全 Java
详解Java中集合的List接口实现的ArrayList方法 | Set接口实现的HashSet方法
详解Java中集合的List接口实现的ArrayList方法 | Set接口实现的HashSet方法
|
3天前
|
安全 Java 索引
带你快速掌握Java中的String类和StringBuffer类(详解常用方法 | 区别 )
带你快速掌握Java中的String类和StringBuffer类(详解常用方法 | 区别 )
|
3天前
|
设计模式 Java API
如何处理Java中的方法过长?
如何处理Java中的方法过长?
|
3天前
|
Java
Java中的Object类 ( 详解toString方法 | equals方法 )
Java中的Object类 ( 详解toString方法 | equals方法 )
|
3天前
|
Java
Java面向对象 ( 类与对象 | 构造方法 | 方法的重载 )
Java面向对象 ( 类与对象 | 构造方法 | 方法的重载 )
|
1天前
|
Java 数据库连接 Android开发
Java中的内存泄漏及其排查方法
Java中的内存泄漏及其排查方法
|
1天前
|
Java
解决Java中的BindException异常的常见方法
解决Java中的BindException异常的常见方法