关于java:Java中的equals和hashCode-超细节篇

22次阅读

共计 7941 个字符,预计需要花费 20 分钟才能阅读完成。

前言

大家好啊,我是汤圆,明天给大家带来的是《Java 中的 equals()和 hashCode() – 具体篇》,心愿对大家有帮忙,谢谢

文章纯属原创,集体总结不免有过错,如果有,麻烦在评论区回复或后盾私信,谢啦

简介

说到 equals 和 hashCode,首先要说下 Object

咱们都晓得,这个 Object 是 Java 所有类的超类,其余类都是从 Object 间接或间接继承而来的

而 Object 中自带的 equals 和 hashCode 办法就是明天咱们要议论的话题

目录

  • 什么是 equals()办法
  • 什么是 hashCode()办法
  • equals 和 hashCode 有啥关系
  • 等等

    注释

    PS:注释可能比拟长,有点像是一层层在剥洋葱,所以会显得有点啰嗦,须要看论断的能够间接跳到文末看总结

什么是 equals 办法

equals 办法用来比拟两个对象的属性是否相等,也能够说是比拟两个援用对象是否为同一个对象(因为 Object 中的 equals 就是这个意

思,如果你没有覆写 equals 办法,那么就能够这么说)

因为在 Object 中没有属性,所以就只比拟了两个援用指向的对象是否相等

只有对象不相等,那么就返回 false(其实这样对子类来说是很不敌对的,太相对了,请往下看)

代码如下所示:

public class Object {public boolean equals(Object obj) {
        // 能够看到,官网括号的写法很标准(向老人家学习)return (this == obj);
    }
}

然而咱们平时在定义类时,都或多或少会蕴含几个属性

比方上面的例子

public class EqualsDemo {
    private int m;
    // 省略 getter,setter,constructor(m)
    public static void main(String[] args) {EqualsDemo demo1 = new EqualsDemo(1);
        EqualsDemo demo2 = new EqualsDemo(1);
        // 这里冀望返回 true,理论却是 false
        System.out.println(demo1.equals(demo2));
    }

    // 这里连续 Object 的写法,只单纯地比拟两个援用指向的对象是否相等
    @Override
    public boolean equals(Object o) {return this == o;}
}

其中定义了一个根本类型的属性 int m;

而后两个实例 demo1 和 demo2 都领有雷同的属性 m = 1;

然而 equals 办法却返回 false

起因就是,equals 办法没有正确地编写

equals 怎么才算正确编写呢?

咱们应该把属性也进行比对,而不是单纯地比拟对象的援用

(这就好比咱们选一半,不能只看外在,而是要外在内在一起看,那样就。。。就都单着吧)

修改后的代码如下所示:

public class EqualsDemo {
    private int m;
    // 省略 getter,setter,constructor(m)
    public static void main(String[] args) {EqualsDemo demo1 = new EqualsDemo(1);
        EqualsDemo demo2 = new EqualsDemo(1);
        // 这时就会返回 true
        System.out.println(demo1.equals(demo2));
    }

    @Override
    public boolean equals(Object o) {if (this == o) return true;
        // 加了上面这两行,对属性进了比对
        EqualsDemo that = (EqualsDemo) o;
        return m == that.m;
    }
}

下面看起来如同没什么问题了,然而理论运行却很容易呈现 空指针异样或者类型转换异样

因为 equals 办法中,咱们在 强转之前没有对参数 o 进行查看

查看什么呢?

查看两个中央:

  1. 首先要确保 o 不能为空 null
  2. 其次确保 o 是 EqualsDemo 类或者子类(父类行不行?不行,父类没有子类特有的属性,强转还是会报错)

代码如下:


public class EqualsDemo {
    private int m;
    // 省略 getter,setter,constructor(m)
    public static void main(String[] args) {EqualsDemo demo1 = new EqualsDemo(1);
        EqualsDemo demo2 = new EqualsDemo(1);
        System.out.println(demo1.equals(demo2));
    }

    @Override
    public boolean equals(Object o) {if (this == o) return true;
        // 加了这一行判断
        if (!(o instanceof EqualsDemo)) return false;
        EqualsDemo that = (EqualsDemo) o;
        return m == that.m;
    }
}

下面用到了 instanceof 来判断(别,我晓得你要说啥,敌人咱先往下看)

instanceof 的用法是 A instanceof B,用来判断 A 是否为 B 类或者 B 的子类

这样就能够避免空指针和转换异样的呈现

所以 equals 判断的内容总结下来就是三步

  1. 判断两个援用指向的对象是否相等
  2. 判断传来的参数是否为以后类或者以后类的子类
  3. 比拟各个属性值是否相等

如果属性是对象的援用,那第三步该怎么比呢?

那就有点像套娃了(什么?没听过套娃?强烈推荐你去看陈翔六点半,外面有很多套娃的案例【您的账户已到账 0.5 毛】

比方上面的代码


public class EqualsDemo {
    private int m;
    private String str;

    public static void main(String[] args) {EqualsDemo demo1 = new EqualsDemo(1, "JavaLover1");
        EqualsDemo demo2 = new EqualsDemo(1, "JavaLover1");
        System.out.println(demo1.equals(demo2));
    }

    @Override
    public boolean equals(Object o) {if (this == o) return true;
        if (!(o instanceof EqualsDemo)) return false;
        EqualsDemo demo = (EqualsDemo) o;
        // 改了这行
        return m == demo.m && str.equals(demo.str);
    }
}

能够看到,多了一个 String 对象援用作为属性

那咱们在比拟的时候,依据套娃的准则,再次 利用 String 对象的 equals 办法进行比拟即可

其余的局部都一样

好了,当初 equals 办法写完了,我上个厕所先

真的写完了吗?我不信(脑补 ing。。。)

因为还是有潜在的空指针异样

构想一下,下面 str 真的会存在吗?如果 str 为 null 怎么办?

所以咱们还要对 str 进行空指针判断,不过不须要咱们来做,而是通过 Objects 这个工具类(Java7 诞生的一个工具类),它内置的 equals

办法能够帮你在比拟两个对象的同时加上 null 判断

Objects.equals 办法如下:

public final class Objects {public static boolean equals(Object a, Object b) {return (a == b) || (a != null && a.equals(b));
    }
}

改了当前的 equals()最终代码如下:

 @Override
    public boolean equals(Object o) {if (this == o) return true;
        if (!(o instanceof EqualsDemo)) return false;
        EqualsDemo demo = (EqualsDemo) o;
        return m == demo.m && Objects.equals(str,demo.str);
    }

好了,万事俱备了,只欠东风

东风?什么东风?

东风就是你的父亲啊

如果是在子类中定义 equals,那么还要思考到父类(如果间接继承自 Object 则不思考)

改了之后的代码如下:


@Override
public boolean equals(Object o) {if (this == o) return true;
        if (!(o instanceof EqualsDemo)) return false;
        // 加了这一行
        if(!super.equals(o)) return false;
        EqualsDemo demo = (EqualsDemo) o;
        return m == demo.m && Objects.equals(str,demo.str);
        }

你可能想晓得,为啥放到第三行?

那是因为前两行属于最外侧的判断

你能够这样想,如果传来的对象 o 是父类的对象,那么父类 super 的判断放在这个地位就很适合了

(因为此时 o instanceof EqualsDemo 必定返回 false, 这样就省去了 super.equals()的判断)

好了,我累了,能够完结了吗?

等一下,地球还没有覆灭,事件也还没有完结。

下面的 instanceof 有个很大的缺点,就是违反了 equals 的对称性

上面咱们顺藤摸瓜,来说下 equals 办法标准的 5 个个性:

  1. 自反性:就是本人反过来跟本人比,要返回 true;比方 x.equals(x) == true
  2. 对称性:就是 x.equals(y) == true 时,也要 y.equals(y) == true
  3. 传递性:就是 x.equals(y) == true, 同时 y.equals(z) == true,那么 x.equals(z) == true
  4. 一致性:就是传说中的幂等性,即 x.equals(y)调用屡次,都应该返回一样的后果
  5. 非空和空比拟则返回 false 的个性:就是 x.equals(y)中,如果 x 非空,而 y 空,则返回 false

好了,回到 instanceof,下面提到它没有满足对称性

是因为用了 instanceof 来做比拟的话,Son.equals(Father)永远不会为真,而 Father.equals(Son)却有可能为真,这就不对称了

所以罗唆就让 Father.equals(Son)也永远不为真

那要怎么做呢?

答案就是 instanceof 的弟弟:getClass

instanceof用来判断是否为以后类或者子类

getClass 只用来判断是否为以后类

改了之后,代码如下

public boolean equals(Object o) {if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        if(!super.equals(o)) return false;
        EqualsDemo demo = (EqualsDemo) o;
        return m == demo.m && Objects.equals(str,demo.str);
        }

好了,看敌人们也累了,明天的 equals 办法就先到这里了

上面总结下 equals 办法的核心内容,总共就是五步(这次真的是完结了):

  1. 判断两个援用指向的对象是否相等
  2. 判断传来的参数是否为空
  3. 判断传来的参数是否属于以后类
  4. 如果有继承父类,则也须要调用父类的 super.equals()办法(Object 除外)
  5. 最初比拟各个属性值是否相等(如果属性为对象援用,则须要通过 Objects.equals(a,b)办法来比拟援用对象的属性值)

什么是 hashCode()办法

hashCode 也叫散列码(哈希码),它用来计算对象中所有属性的散列值

对于散列这里就不开展了,咱们在这里只须要晓得两点:

  1. 散列值为整数,能够为负值
  2. 散列值能够用来确定元素在散列表中的地位(有可能两个元素领有雷同的散列值,这个就是散列抵触)

在 Object 中,hashCode()是一个本地办法,因为 Object 没有属性,所以默认返回的是 对象的内存地址

代码如下所示:

public class Test2 {public static void main(String[] args) {Object t = new Object();
        int a = t.hashCode();
        System.out.println(Integer.toHexString(a)); // 输入 4554617c
    }
}

其中 4554617c 就是对象 a 的内存地址, 这里转成 16 进制显示(是因为通常地址都是用 16 进制显示的,比方咱们电脑的 Mac 地址)

上面总结下 hashCode 的几个个性:

  1. 一致性:无论 hashCode 调用多少次,都应该返回一样的后果(这一点跟 equals 很像)
  2. 追随性(本人编的一个性):如果两个对象的 equals 返回为真,那么 hashCode 也应该相等
  3. 反过来,如果两个对象的 equals 返回为假,那么 hashCode 有可能相等,然而如果散列的足够好,那么通常来说 hashCode()也不应该相等
  4. 覆写 equals 办法时,肯定要覆写 hashCode 办法

equals 和 hashCode 有什么分割呢?

hashCode 和 equals 能够说相辅相成的,他俩独特合作用来判断两个对象是否相等

如果离开来看的话,他俩是没什么分割的,然而因为某些起因导致被分割上了(比方 HashMap 这个小月老)

上面来细说一下

咱们晓得 HashMap 汇合中的 key 是不能反复的,那它是怎么判断反复的呢?

就是通过 equals 和 hashCode 来判断的

上面是局部源码

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

能够看到,map 先进行 hash 判断,而后进行 equals 判断

也就是说,hash 是前提,如果 hash 都不相等,那 equals 就不必比拟了(先计算 hash 的一个起因是计算 hash 比 equals 快得多)

所以咱们在自定义对象时,如果覆写了 equals,那么肯定要记得覆写 hashCode,(当然,假如这里的自定义对象是用来作为 map 中的 key 键的)

覆写代码如下:

    @Override
    public boolean equals(Object o) {if (this == o) return true;
        if(getClass() != o.getClass()) return false;
        if(!super.equals(o)) return false;
        EqualsDemo demo = (EqualsDemo) o;
        return m == demo.m && Objects.equals(str,demo.str);
    }

    @Override
    public int hashCode() {return Objects.hash(m, str);
    }

其中 Objects.hash 有点相似于下面的 Objects.equals()办法,很实用

如果只覆写了 equals,没有覆写 hashCode, 会咋样呢?

后果就是:

当你创立两个对象(属性统一,然而内存地址不统一),作为 key 放到 map 中时就会被当成两个 key 来寄存

同理可得,获取数据 value 的时候,也是不统一的

上面是只覆写 equals 没覆写 hashCode 的代码:能够看到,两次取到的值是不一样的


public class HashCodeDemo{public static void main(String[] args) {
        // 两个对象的属性都为 n = 1
        HashCodeDemo demo1 = new HashCodeDemo(1);
        HashCodeDemo demo2 = new HashCodeDemo(1);
        Map<HashCodeDemo, Integer> map = new HashMap<>();
        map.put(demo1, 1);
        map.put(demo2, 2);
        System.out.println(map.get(demo1)); // 输入 1
        System.out.println(map.get(demo2)); // 输入 2
    }

    private int n;

    public HashCodeDemo(int n) {this.n = n;}

    @Override
    public boolean equals(Object o) {if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        HashCodeDemo that = (HashCodeDemo) o;
        return n == that.n;
    }

    public int getN() {return n;}

    public void setN(int n) {this.n = n;}
}

同时覆写 equals 和 hashCode 的代码:能够看到,两次取到的值都是一样的


public class HashCodeDemo{public static void main(String[] args) {HashCodeDemo demo1 = new HashCodeDemo(1);
        HashCodeDemo demo2 = new HashCodeDemo(1);
        Map<HashCodeDemo, Integer> map = new HashMap<>();
        map.put(demo1, 1);
        // 第二次会笼罩第一次的值,因为 key 相等(equals 和 hashCode 都相等)map.put(demo2, 2);
        System.out.println(map.get(demo1)); // 输入 2
        System.out.println(map.get(demo2)); // 输入 2
    }

    private int n;

    public HashCodeDemo(int n) {this.n = n;}

    @Override
    public boolean equals(Object o) {if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        HashCodeDemo that = (HashCodeDemo) o;
        return n == that.n;
    }

    @Override
    public int hashCode() {return Objects.hash(m, str);
    }
    
    public int getN() {return n;}

    public void setN(int n) {this.n = n;}
}

HashSet 汇合也是同理,因为它外部的就是依赖 HashMap 实现的(这个后面有简略介绍过,感兴趣的能够回顾一下)

总结

  • equals 办法的个性:
  1. 自反性:就是本人反过来跟本人比,要返回 true;比方 x.equals(x) == true
  2. 对称性:就是 x.equals(y) == true 时,也要 y.equals(y) == true
  3. 传递性:就是 x.equals(y) == true, 同时 y.equals(z) == true,那么 x.equals(z) == true
  4. 一致性:就是传说中的幂等性,即 x.equals(y)调用屡次,都应该返回一样的后果
  5. 非空和空比拟则返回 false 的个性:就是 x.equals(y)中,如果 x 非空,而 y 空,则返回 false
  • hashCode 的个性以及和 equals 的分割
  1. 一致性:无论 hashCode 调用多少次,都应该返回一样的后果(这一点跟 equals 很像)
  2. 追随性(本人编的一个性):如果两个对象的 equals 返回为真,那么 hashCode 也应该相等
  3. 反过来,如果两个对象的 equals 返回为假,那么 hashCode 有可能相等,然而如果散列的足够好,那么通常来说 hashCode()也不应该相等
  4. 覆写 equals 办法时,肯定要覆写 hashCode 办法

后记

最初,感激大家的观看,谢谢

正文完
 0