Java进阶——Java中的equals()和hashCode()

介绍

hashCode()equals()常在List、Map中常常用到这两个方法,List用的是equals()来决定List中存储的是否为同一个对象,Map用的是equals()hashCode()来决定Map中存储的是否为同一个对象。由于Map是key-value组成的,涉及到key和value的映射关系,那么就会用到这两个方法去确定它们的映射关系。在创建类过程中,我们一般不用去复写这两个方法,但是有些业务需求要求我们不得不去重写这两个方法来解决

equals()

创建Student实体类

public class Student {
    int id;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
}

对两个对象设置相同的id

Student s1 = new Student();
Student s2 = new Student();
s1.setId(10);
s2.setId(10);
Log.e("TAG", "" + s1.equals(s2));// false

List<Student> list = new ArrayList<>();
list.add(s1);
Log.e("TAG", "" + list.contains(s2));// false

在输出的结果中,两个对象比较的返回值为false,集合List也不是两个相同对象,可以看出这两个Student对象不是同一个对象。但很多种情况下,我们的业务需求要求我们同样的id只能存在一个对象,所以这个时候我们就必须重写我们的equals()方法,也可以通过AS自动生成equals()。重写equals()后,这个时候s1.equals(s2)输出结果为ture,list.contains(s2)输出结果也为ture。重写的equals()代码如下

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

    Student student = (Student) o;

    return id == student.id;
}

hashCode()

还是用上面的例子,不过这一次会对上次的例子做下修改

Set<Student> set = new HashSet<>();
set.add(s1);
set.add(s2);
Log.e("TAG", "size:" + set.size());// size:2

通过输出的结果得出,s1和s2相同的对象,按照我们的预期,size()应该是1才对,问题在哪里呢?由于Set和Map的集合类实现原理都是先根据key的hashCode()来计算存储位置的,确定完存储位置后才调用equal()来存储,所以在存储过程中size()返回2。这时候,我们就需要重写hashCode()来解决这个问题,用AS自动生成hashCode()。重写hashCode()后,Set的存储就保持一致了,size()的输出就为1了,到这里我们基本上解决了业务需求的问题。重写的equals()hashCode()代码如下

public class Student {
    int id;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        return id == student.id;
    }
    @Override
    public int hashCode() {
        return id;
    }
}

重写规则

AS自动生成的代码,重写的规则是怎么样的呢?我们通过多种类型的变量来看看具体的重写规则

public class Person {

    boolean personBoolean;
    byte personByte;
    char personChar;

    short personShort;
    long personLong;
    float personFloat;
    double personDouble;

    String personString = "AAA";
    String[] personStrings = new String[]{};

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

        Person person = (Person) o;

        if (personBoolean != person.personBoolean) return false;
        if (personByte != person.personByte) return false;
        if (personChar != person.personChar) return false;
        if (personShort != person.personShort) return false;
        if (personLong != person.personLong) return false;
        if (Float.compare(person.personFloat, personFloat) != 0) return false;
        if (Double.compare(person.personDouble, personDouble) != 0) return false;
        if (!personString.equals(person.personString)) return false;
        // Probably incorrect - comparing Object[] arrays with Arrays.equals
        return Arrays.equals(personStrings, person.personStrings);
    }

    @Override
    public int hashCode() {
        int result;
        long temp;
        result = (personBoolean ? 1 : 0);
        result = 31 * result + (int) personByte;
        result = 31 * result + (int) personChar;
        result = 31 * result + (int) personShort;
        result = 31 * result + (int) (personLong ^ (personLong >>> 32));
        result = 31 * result + (personFloat != +0.0f ? Float.floatToIntBits(personFloat) : 0);
        temp = Double.doubleToLongBits(personDouble);
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        result = 31 * result + personString.hashCode();
        result = 31 * result + Arrays.hashCode(personStrings);
        return result;
    }
}

equals()主要是通过对象值的判断,而hashCode()根据不同的数据类型生成的规则是不一样的。hashCode()的重写不能太过简单,否则哈希冲突过多。也不能太过复杂,否则计算复杂度过高,影响性能。其生成规则如下

  • 如果是boolean类型,计算(f?1:0)
  • 如果是byte、char、short或者int类型,计算(int)f
  • 如果是long类型,计算(int)(f^(f>>>32))
  • 如果是float类型,计算Float.floatToIntBits(f)
  • 如果是double类型,计算Double.doubleToLongBits(f),然后重复第三个步骤。
  • 如果是对象引用,并且该类的equals方法通过递归调用equals方法来比较这个域,同样为这个域递归的调用hashCode,如果这个域为null,则返回0。
  • 如果是数组,要把每一个元素当作单独的域来处理,递归的运用上述规则,如果数组域中的每个元素都很重要,那么可以使用Arrays.hashCode方法。

String源码中hashCode()

String类的hashcode()算法充分利用了字符串内部字符数组的所有字符

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;
}

生成hash码的算法在String类中如下所示,注意“s“是那个字符数组,n是字符串的长度

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

hashCode()为什么使用31数字作为乘子

31在数学中属于素数。素数的作用就是在使用的时候,如果我用一个数字来乘以这个素数,那么最终的出来的结果只能被素数本身和被乘数还有1来整除。这样子就会有人有疑问了,其他7,13,33这些素数为什么不可以使用呢?其实是有原因的,31是通过验证后,取的是一个折中的结果。具体有什么原因,我们可以取这两个观点进行分析:

  1. 选择数字31是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。同时,数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31 * i == (i << 5) – i,现代的 Java 虚拟机可以自动的完成这个优化。
  2. 由于hashCode()方法返回int值,其最大值就是65535,那么就可以打个比方:如果你对超过50,000个英文单词进行hashcode运算,并使用常数31,33,37,39和41作为乘子,每个常数算出的哈希值冲突数都小于7个,所以在上面几个常数中,常数31被Java实现所选用也就不足为奇了。

总结

  1. 存储在List的对象,是以equals()去判断是否为同一个对象
  2. 存储在Map的对象,是以key的hashCode()和value的equals()去判断是否为同一个对象
    原文作者:Hensen_
    原文地址: http://blog.csdn.net/qq_30379689/article/details/80531875
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞