重写equals和hashCode

一.Hashset、Hashmap、Hashtable与hashcode()和equals()的密切关系

java.lang.Object类中有两个非常重要的方法:详见:

equals和hashCode详解 

public boolean equals(Object obj)
public int hashCode()

Hashset是继承Set接口,Set接口又实现Collection接口,这是层次关系。那么Hashset、Hashmap、Hashtable中的存储操作是根据什么原理来存取对象的呢?

        下面以HashSet为例进行分析,我们都知道:在hashset中不允许出现重复对象,元素的位置也是不确定的。在hashset中又是怎样判定元素是否重复的呢?在java的集合中,判断两个对象是否相等的规则是:
         1.判断两个对象的hashCode是否相等

             如果不相等,认为两个对象也不相等,完毕
             如果相等,转入2
           (这一点只是为了提高存储效率而要求的,其实理论上没有也可以,但如果没有,实际使用时效率会大大降低,所以我们这里将其做为必需的。)

         2.判断两个对象用equals运算是否相等
            如果不相等,认为两个对象也不相等
            如果相等,认为两个对象相等(equals()是判断两个对象是否相等的关键)
            为什么是两条准则,难道用第一条不行吗?不行,因为前面已经说了,hashcode()相等时,equals()方法也可能不等,所以必须用第2条准则进行限制,才能保证加入的为非重复元素。

二.问题案例分析

我们都知道Hashset里的类都是无序,不能重复的

现在有student类,没有覆盖Object类的equals()和hashcode()方法

package com.my.test.base;

public class Student {
    
    private int id ;
    
    private String name;

    /**
     * 
     */
    public Student() {
        super();
    }

    /**
     * @param id
     * @param name
     */
    public Student(int id, String name) {
        super();
        this.id = id;
        this.name = name;
    }

    /**
     * @return the id
     */
    public int getId() {
        return id;
    }

    /**
     * @param id the id to set
     */
    public void setId(int id) {
        this.id = id;
    }

    /**
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "Student [id=" + id + ", name=" + name + "]";
     }   
    
}

测试类

package com.my.test.base;

import java.util.HashSet;
import java.util.Set;

public class TestStudent {
    public static void main(String[] args) {
        
        //创建3个Student 实例
        Student stu1 = new Student(1, "joe1");
        Student stu2 = new Student(2, "joe2");
        Student stu3 = new Student(1, "joe1");
        
        //查看他们的HashCode
        System.out.println("stu1 hashCode " + stu1.hashCode());
        System.out.println("stu2 hashCode " + stu2.hashCode());
        System.out.println("stu3 hashCode " + stu3.hashCode());
        
        //创建HashSet实例
        Set<Object> students = new HashSet<Object>();
        
        //添加对象
        students.add(stu1);
        students.add(stu2);
        students.add(stu3);
        
        //遍历
        for (Object object : students) {
            System.out.println(object);
        }
    }

}

输出结果

stu1 hashCode 366712642
stu2 hashCode 1829164700
stu3 hashCode 2018699554
Student [id=1, name=joe1]
Student [id=1, name=joe1]
Student [id=2, name=joe2]

我们可以看到两个一模一样的对象stu1和stu3都被添加进去了

为什么hashset添加了相等的元素呢,这是不是和hashset的原则违背了呢?

回答是:没有。

因为在根据hashcode()对两次建立的new Student(1,“joe1”)对象进行比较时,生成的是不同的哈希码值,所以hashset把他当作不同的对象对待了,当然此时的equals()方法返回的值也不等。

   为什么会生成不同的哈希码值呢?

上面我们在比较s1和s2的时候不是生成了同样的哈希码吗?原因就在于我们自己写的Student类并没有重新自己的hashcode()和equals()方法,所以在比较时,是继承的object类中的hashcode()方法,而object类中的hashcode()方法是一个本地方法,比较的是对象的地址(引用地址),使用new方法创建对象,两次生成的当然是不同的对象了,造成的结果就是两个对象的hashcode()返回的值不一样,所以Hashset会把它们当作不同的对象对待。

        怎么解决这个问题呢?答案是:在Student类中重新hashcode()和equals()方法。

重写equals()和hashcode()小结:

 1.重点是equals,重写hashCode只是技术要求(为了提高效率)
    2.为什么要重写equals呢?因为在java的集合框架中,是通过equals来判断两个对象是否相等的
    3.在hibernate中,经常使用set集合来保存相关对象,而set集合是不允许重复的。在向HashSet集合中添加元素时,其实只要重写equals()这一条也可以。但当hashset中元素比较多时,或者是重写的equals()方法比较复杂时,我们只用equals()方法进行比较判断,效率也会非常低,所以引入了hashCode()这个方法,只是为了提高效率,且这是非常有必要的。 

三.重写equals()方法

hashCode()和equals()方法可以说是Java完全面向对象的一大特色.它为我们的编程提供便利的同时也带来了很多危险.这篇文章我们就讨论一下如何正解理解和使用这2个方法. 

如何重写equals()方法
如果你决定要重写equals()方法,那么你一定要明确这么做所带来的风险,并确保自己能写出一个健壮的equals()方法.一定要注意的一点是,在重写equals()后,一定要重写hashCode()方法.具体原因稍候再进行说明.
我们先看看 JavaSE 7 Specification中对equals()方法的说明:

It is reflexive: for any non-null reference value x, x.equals(x) should return true.
It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
For any non-null reference value x, x.equals(null) should return false.

这段话用了很多离散数学中的术数.简单说明一下:
1. 自反性:A.equals(A)要返回true.
2. 对称性:如果A.equals(B)返回true, 则B.equals(A)也要返回true.
3. 传递性:如果A.equals(B)为true, B.equals(C)为true, 则A.equals(C)也要为true. 说白了就是 A = B , B = C , 那么A = C.
4. 一致性:只要A,B对象的状态没有改变,A.equals(B)必须始终返回true.
5. A.equals(null) 要返回false.

相信只要不是专业研究数学的人,都对上面的东西不来电.在实际应用中我们只需要按照一定的步骤重写equals()方法就可以了.为了说明方便,我们先定义一个程序员类(Coder):

我们想要的是,如果2个学生对象的name和id都是相同的,那么我们就认为这两个学生是一个人.这时候我们就要重写其equals()方法.因为默认的equals()实际是判断两个引用是否指向内在中的同一个对象,相当于 == . 重写时要遵循以下三步:
1. 判断是否等于自身.
if(obj == this){
            return true;
        }

2. 使用instanceof运算符判断 obj是否为Student类型的对象.

if(!(obj instanceof Student)){
            return false;
 }

3. 比较Student类中你自定义的数据域,name和id,一个都不能少.

Student student = (Student) obj;
return student.id == id && student.name.equals(name);

看到这有人可能会问,第3步中有一个强制转换,如果有人将一个Integer类的对象传到了这个equals中,那么会不会扔ClassCastException呢?这个担心其实是多余的.因为我们在第二步中已经进行了instanceof 的判断,如果other是非Coder对象,甚至other是个null, 那么在这一步中都会直接返回false, 从而后面的代码得不到执行的机会.
上面的三步也是<Effective Java>中推荐的步骤,基本可保证万无一失. 

注意注意此处有bug,如果name为null,则会抛出异常:java.lang.NullPointerException,hashCode处同样会有此情况

看Objects里的解决方法

  /**
     * Returns {@code true} if the arguments are equal to each other
     * and {@code false} otherwise.
     * Consequently, if both arguments are {@code null}, {@code true}
     * is returned and if exactly one argument is {@code null}, {@code
     * false} is returned.  Otherwise, equality is determined by using
     * the {@link Object#equals equals} method of the first
     * argument.
     *
     * @param a an object
     * @param b an object to be compared with {@code a} for equality
     * @return {@code true} if the arguments are equal to each other
     * and {@code false} otherwise
     * @see Object#equals(Object)
     */
    public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }

所以此处可以改版为:

 @Override
        public boolean equals(Object obj) {
            // TODO Auto-generated method stub

            if (obj == this) {
                return true;
            }

            if (!(obj instanceof Student)) {
                return false;
            }

            Student student = (Student) obj;
            
            return student.id == id && (name == student.getName()||(name!=null&&name.equals(student.getName())));
        }

或者干脆使用Objects工具类

@Override
        public boolean equals(Object obj) {
            // TODO Auto-generated method stub

            if (obj == this) {
                return true;
            }

            if (!(obj instanceof Student)) {
                return false;
            }

            Student student = (Student) obj;
            
            return student.id == id && Objects.equals(name, student.getName());
        }

四.重写hashCode()方法

在JavaSE 7 Specification中指出,
"Note that it is generally necessary to override the hashCode method whenever this method(equals) is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes."

如果你重写了equals()方法,那么一定要记得重写hashCode()方法.我们在大学计算机数据结构课程中都已经学过哈希表(hash table)了,hashCode()方法就是为哈希表服务的.
当我们在使用形如HashMap, HashSet这样前面以Hash开头的集合类时,hashCode()就会被隐式调用以来创建哈希映射关系.稍后我们再对此进行说明.这里我们先重点关注一下hashCode()方法的写法.

1.最简单最作死的写法比如可以这样写:

public int hashCode(){  
   return 1; //等价于hashcode无效  
}

这样做的效果就是在比较哈希码的时候不能进行判断,因为每个对象返回的哈希码都是1,每次都必须要经过比较equals()方法后才能进行判断是否重复,这当然会引起效率的大大降低。

2.进化版本 

public int hashCode() {
        return id * name.hashCode();
    }

    public boolean equals(Object o) {
        Student s = (Student) o;
        return id == s.id && name.equals(s.name);
    }

 3.推荐版本

<Effective Java>中给出了一个能最大程度上避免哈希冲突的写法,但我个人认为对于一般的应用来说没有必要搞的这么麻烦.如果你的应用中HashSet中需要存放上万上百万个对象时,那你应该严格遵循书中给定的方法.如果是写一个中小型的应用,那么下面的原则就已经足够使用了:
要保证Student对象中所有的成员都能在hashCode中得到体现.

    @Override
    public int hashCode() {
        // TODO Auto-generated method stub
        
        int result = 17;
        
        result = result*31 +name.hashCode();
        
        result = result*31 +id;
        
        return result;
    }

注意注意此处有bug,如果name为null,则会抛出异常:java.lang.NullPointerException,equals处同样会有此情况

解决办法:

result = result * 31 + name == null ? 0 : name.hashCode();

或者

使用Objects工具类

 @Override
        public int hashCode() {
            // TODO Auto-generated method stub            

            return Objects.hash(id,name);
        }

其中int result = 17你也可以改成20, 50等等都可以.看到这里我突然有些好奇,想看一下String类中的hashCode()方法是如何实现的.查文档知:

"Returns a hash code for this string. The hash code for a String object is computed as

 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

using int arithmetic, where s[i] is the ith character of the string, n is the length of the string, and ^ indicates exponentiation. (The hash value of the empty string is zero.)"
对每个字符的ASCII码计算n - 1次方然后再进行加和,可见Sun对hashCode的实现是很严谨的. 这样能最大程度避免2个不同的String会出现相同的hashCode的情况.

五。jdk1.8equals()和hashcode()方法

//euqals

    /**
     * Compares this string to the specified object.  The result is {@code
     * true} if and only if the argument is not {@code null} and is a {@code
     * String} object that represents the same sequence of characters as this
     * object.
     *
     * @param  anObject
     *         The object to compare this {@code String} against
     *
     * @return  {@code true} if the given object represents a {@code String}
     *          equivalent to this string, {@code false} otherwise
     *
     * @see  #compareTo(String)
     * @see  #equalsIgnoreCase(String)
     */
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }


//hashCode
/**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using {@code int} arithmetic, where {@code s[i]} is the
     * <i>i</i>th character of the string, {@code n} is the length of
     * the string, and {@code ^} indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

六.重写equals()而不重写hashCode()的风险

在Oracle的Hash Table实现中引用了bucket的概念.如下图所示:


从上图中可以看出,带bucket的hash table大致相当于哈希表与链表的结合体.即在每个bucket上会挂一个链表,链表的每个结点都用来存放对象.Java通过hashCode()方法来确定某个对象应该位于哪个bucket中,然后在相应的链表中进行查找.在理想情况下,如果你的hashCode()方法写的足够健壮,那么每个bucket将会只有一个结点,这样就实现了查找操作的常量级的时间复杂度.即无论你的对象放在哪片内存中,我都可以通过hashCode()立刻定位到该区域,而不需要从头到尾进行遍历查找.这也是哈希表的最主要的应用.

如:
当我们调用HashSet的put(Object o)方法时,首先会根据o.hashCode()的返回值定位到相应的bucket中,如果该bucket中没有结点,则将 o 放到这里,如果已经有结点了, 则把 o 挂到链表末端.同理,当调用contains(Object o)时,Java会通过hashCode()的返回值定位到相应的bucket中,然后再在对应的链表中的结点依次调用equals()方法来判断结点中的对象是否是你想要的对象. 

下面我们通过一个例子来体会一下这个过程:
我们先创建2个新的Student对象: 
        //创建3个Student 实例
        Student stu1 = new Student(1, "joe1");
        Student stu2 = new Student(1, "joe1");        

假定我们已经重写了Student的equals()方法而没有重写hashCode()方法:

然后我们构造一个HashSet,将stu1 对象放入到set中:

//创建HashSet实例
        Set<Object> students = new HashSet<Object>();
        
        //添加对象
        students.add(stu1);
再执行:
System.out.println(set.contains(c2));

我们期望contains(c2)方法返回true, 但实际上它返回了false.
c1和c2的name和age都是相同的,为什么我把c1放到HashSet中后,再调用contains(c2)却返回false呢?这就是hashCode()在作怪了.因为你没有重写hashCode()方法,所以HashSet在查找c2时,会在不同的bucket中查找.比如c1放到05这个bucket中了,在查找c2时却在06这个bucket中找,这样当然找不到了.因此,我们重写hashCode()的目的在于,在A.equals(B)返回true的情况下,A, B 的hashCode()要返回相同的值.

我让hashCode()每次都返回一个固定的数行吗

有人可能会这样重写:

@Override
    public int hashCode() {
        return 10;
 
    }

如果这样的话,HashMap, HashSet等集合类就失去了其 "哈希的意义".用<Effective Java>中的话来说就是,哈希表退化成了链表.如果hashCode()每次都返回相同的数,那么所有的对象都会被放到同一个bucket中,每次执行查找操作都会遍历链表,这样就完全失去了哈希的作用.所以我们最好还是提供一个健壮的hashCode()为妙.
七.其他推荐的写法

对于JDK7及更新版本,你可以是使用java.util.Objects 来重写 equals 和 hashCode 方法,代码如下

import java.util.Objects;
 
public class User {
    private String name;
    private int age;
    private String passport;
 
    //getters and setters, constructor
 
    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof User)) {
            return false;
        }
        User user = (User) o;
        return age == user.age &&
                Objects.equals(name, user.name) &&
                Objects.equals(passport, user.passport);
    }
 
    @Override
    public int hashCode() {
        return Objects.hash(name, age, passport);
    }
 
}

3.Apache Commons Lang

或者,您可以使用Apache Commons LangEqualsBuilder 和HashCodeBuilder 方法。代码如下

import org.apache.commons.lang3.builder;
 
public class User {
    private String name;
    private int age;
    private String passport;
    //getters and setters, constructor
 
     @Override
    public boolean equals(Object o) {
 
        if (o == this) return true;
        if (!(o instanceof User)) {
            return false;
        }
        User user = (User) o;
 
        return new EqualsBuilder()
                .append(age, user.age)
                .append(name, user.name)
                .append(passport, user.passport)
                .isEquals();
    }
    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 37)
                .append(name)
                .append(age)
                .append(passport)
                .toHashCode();
    }
}

 参考:https://blog.csdn.net/neosmith/article/details/17068365

      https://blog.csdn.net/zzg1229059735/article/details/51498310

原文地址:https://www.cnblogs.com/lukelook/p/11280718.html