美文网首页
Java中equals和hashcode的使用

Java中equals和hashcode的使用

作者: 文景大大 | 来源:发表于2019-05-10 18:13 被阅读0次

关于equals的用法,在另一篇文章《Java中==和equals的使用区别》中已经详细介绍过了,本文主要讲解equals和hashcode在一起使用时的注意事项。

一、我可以不重写hashcode吗?

我们在重写对象的equals方法的时候,并没有要求重写hashcode方法,那如果我们不去管hashcode方法,能正常进行对象的equals比较吗?

答案是肯定的。

public class Fruit {

    private String name;
    private Integer weight;

    public Fruit(){}

    public Fruit(String name, Integer weight){
        this.name = name;
        this.weight = weight;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Fruit fruit = (Fruit) o;
        return Objects.equals(name, fruit.name) &&
                Objects.equals(weight, fruit.weight);
    }

    // getters and setters...
    public static void main(String[] args) throws Exception {

        Fruit apple1 = new Fruit("apple",12);
        Fruit apple2 = new Fruit("apple",12);

        // print true
        System.out.println(apple1.equals(apple2));
    }

在如上两个对象的equals比较中,我们没有用到hashcode,也没有重写它,照样可以正常使用。

二、什么情况下不重写hashcode会出现问题?

但是,如果我们在散列表这种数据结构中不重写hashcode的话,会发生意料之外的情况,比如下面这个例子。

Java中的HashSet是被设计为不能添加重复的相同对象的,即hashcode相同且内容也相同的对象,如果hashcode是不同的,那么即使对象的内容相同,hashset也认为它们是不同的。

// 执行hashset.add方法时的部分源码
if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

我们还是使用上面的Fruit类来做演示,其中只是重写了equals方法,并没有重写hashcode方法。

    public static void main(String[] args) throws Exception {

        Fruit apple1 = new Fruit("apple",12);
        Fruit apple2 = new Fruit("apple",12);

        // apple1 hashcode-603742814
        System.out.println("apple1 hashcode-" + apple1.hashCode());
        // apple2 hashcode-1067040082
        System.out.println("apple2 hashcode-" + apple2.hashCode());

        Set<Fruit> fruitSet = new HashSet<>(16);
        fruitSet.add(apple1);
        fruitSet.add(apple2);

        // print 2
        System.out.println(fruitSet.size());
    }

由于apple1和apple2的hashcode不同,因此,虽然它们的内容是相同的,但是仍然会被重复添加到hashset中。

如果我们在Fruit中重写了hashcode,使得内容相同的对象具有相同的hashcode值,那么这个问题就能得到解决:

    // 在Fruit类中添加如下代码
    @Override
    public int hashCode() {
        return Objects.hash(name, weight);
    }
    public static void main(String[] args) throws Exception {

        Fruit apple1 = new Fruit("apple",12);
        Fruit apple2 = new Fruit("apple",12);

        // apple1 hashcode--1411060813
        System.out.println("apple1 hashcode-" + apple1.hashCode());
        // apple2 hashcode--1411060813
        System.out.println("apple2 hashcode-" + apple2.hashCode());

        Set<Fruit> fruitSet = new HashSet<>(16);
        fruitSet.add(apple1);
        fruitSet.add(apple2);

        // print 1
        System.out.println(fruitSet.size());
    }

以上例子只是使用散列表中的hashset作为案例进行说明,其它的散列表也是有相同的问题的,比如hashtable、hashmap等。

所以,在使用散列表类型的数据结构增删查改对象的时候,该对象一定要重写equals方法和hashcode方法。

三、为什么要存在hashcode?

有人就会问,散列表为什么要这么设计,不是让我们开发变得很不方便了么?

其实,散列表这么设计恰恰是为了提高效率,至少是为了提高查找的效率。当一个对象需要存储到散列表中的时候,存储的地址是由该对象的哈希值计算出来的。

区别于链表,散列表中对象的存储并不是线性的,而是根据对象的哈希值计算得到一个散列表空间的地址,也许是在散列表空间的头部,也许是在中间或者尾部。

当我们需要在散列表中查找是否存在某个对象的时候,就根据对象计算哈希值,再得到地址,去散列表中看看是否该地址已经有对象了,如果没有,那么就认为还不存在这个对象;如果已经有对象了,也不代表两个对象就一定相同,还需要对它们的值进行比对。

所以,我们看出,使用散列表进行查找是效率非常高的,瞬间就能定位到高概率存在的地址上进行进一步的比对,复杂度为O(1),而链表则慢得多,查找复杂度为O(n)。

四、关于hashcode使用的一些规则

关于hashcode的详细过程比较复杂,本文不作展开,以后会单独写文章予以介绍,这里只是列出几个使用规则。有兴趣的朋友也可以参考《数据结构》进行学习。

  • 相同内容的对象它们的hashcode一定相同;
  • hashcode相同的对象它们的内容不一定是相同的;

五、我们该如何重写hashcode

首先,判断你创建的对象是否会存储到散列表性质的数据结构中,如果确定不会,那就不用重写hashcode,它只对散列表性质的数据结构有效。

其次,如果要用到散列表性质的数据结构,那么重写hashcode的时候要遵循上述两个hashcode的使用规则。即,在产生hashcode的时候,仅根据对象的内容来生成hashcode。如此,只要两个对象内容相同,势必产生一样的hashcode。

比如下面是用IDE自动生成的hashcode重写方法:

    @Override
    public int hashCode() {
        return Objects.hash(name, weight);
    }

如果两个不同的对象产生相同的hashcode怎么办?这个就不用我们操心了,散列表自己会有一套散列碰撞处理机制,它总是能帮助我们找到正确的对象的。详细处理机制可以参考《Java中的哈希表》

相关文章

网友评论

      本文标题:Java中equals和hashcode的使用

      本文链接:https://www.haomeiwen.com/subject/lqoaoqtx.html