HashMap的前置知识 - hashCode以及equals的前世今生

时间:2019-02-17
本文章向大家介绍HashMap的前置知识 - hashCode以及equals的前世今生,主要包括HashMap的前置知识 - hashCode以及equals的前世今生使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

以下内容基于 JDK1.8

一、Object 类(package java.lang)

hashCode 方法

hashCode 方法是在 Object 类就有的方法。

public native int hashCode();

不过这个方法是 native 方法,即本地方法,底层调用时,调用的是非java语言的代码。
该方法根据对象的物理存储地址(internal address)生成一个整数。

equals 方法

equals 方法也是 Object 类中就有的方法,源代码如下:

public boolean equals(Object obj) {
return (this == obj);
}

即简单的比较两者的引用是否相同。

== 和 equals 方法的区别
  1. 对于 ==,如果作用于基本数据类型的变量,则直接比较值是否相等;
    如果作用于引用类型的变量,则比较的是两者的引用。
  2. 对于 equals 方法,如果没有对 equals 方法进行重写,则比较的是对象的引用;
    如果重写了 equals 方法,则执行的是自定义的 equals 方法。例如 String 等类对 equals 方法进行了重写,比较的是对象的内容。

什么时候需要重写 equals 方法和 hashCode 方法

由于在 Java 中所有的类都继承自 Object 类,所以所有的类都包含这两个方法,包括我们自定义的类。
当我们设计一个类时,如果我们需要判断该类的两个实例对象是否相等,就需要重写 equals 方法。重写的 equals 方法需要满足如下五个规则:

  1. 自反性:对任意引用值 x,x.equals(x) 的返回值一定为true。
  2. 对称性:对于任何引用值 x , y,当且仅当 y.equals(x) 返回值为 true 时,x.equals(y) 的返回值也为true。
  3. 传递性:如果 x.equals(y) = true,y.equals(z) = true,则 x.equals(z) = true。
  4. 一致性:如果参与比较的对象没任何改变,则对象比较的结果也不应该有任何改变。
  5. 非空性:任何非空的引用值 x,x.equals(null) 的返回值一定为 false。
    如果这个类的对象会被存储在 HashMap 等数据结构中,那么就必须要重写 hashCode 方法和 equals 方法。

重写后的两个方法必须满足如下关系:

  1. 如果 equals 方法判断该类的两个实例对象相等,则两对象的 hashCode 方法的返回值也必须相等。
  2. 如果 equals 方法判断该类的两个实例对象不相等,则两对象的 hashCode 方法的返回值也尽量不等。

toString 方法(和主题不相关(^&^))

源代码如下:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

返回字符串,字符串内容为:类名+@+16进制的hashCode值

二、一些常见数据类型重写的 hashCode 方法以及 equals 方法

Integer 类

重写的 hashCode 方法

源代码如下:

@Override
public int hashCode() {
    return Integer.hashCode(value);
}
public static int hashCode(int value) {
    return value;
}

由此可见,Integer类的实例,调用其 hashCode 方法的返回值就是实例对象里所包含的整数值。

重写的 equals 方法

源代码如下:

private final int value;
public Integer(int value) {
    this.value = value;
}
public int intValue() {
    return value;
}
public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

从源代码中不难看出,Integer 中的 equals 方法首先判断被比较对象 obj 是否为 Integer 或其子类的实例,如果是则直接比较对象中所包含的整数值是否相等,是则返回 true,否则返回 false。

Double 类

重写的 hashCode 方法

源代码如下:

@Override
public int hashCode() {
    return Double.hashCode(value);
}
public static int hashCode(double value) {
    long bits = doubleToLongBits(value);
    return (int)(bits ^ (bits >>> 32));
}
/**
 * Returns a representation of the specified floating-point value
 * according to the IEEE 754 floating-point "double
 * format" bit layout.
 */
public static long doubleToLongBits(double value) {
    long result = doubleToRawLongBits(value);
    // Check for NaN based on values of bit fields, maximum
    // exponent and nonzero significand.
    if ( ((result & DoubleConsts.EXP_BIT_MASK) ==
          DoubleConsts.EXP_BIT_MASK) &&
         (result & DoubleConsts.SIGNIF_BIT_MASK) != 0L)
        result = 0x7ff8000000000000L;
    return result;
}

稍微有点复杂,懂不懂无所谓,大致意思理解了就行,问题不大,看一下就可以了,感兴趣的可以自己去查阅相关资料。

重写的 equals 方法

源代码如下:

public boolean equals(Object obj) {
    return (obj instanceof Double)
           && (doubleToLongBits(((Double)obj).value) ==
                  doubleToLongBits(value));
}

和 Integer 类中的 equals 方法类似,也是先判断被比较对象 obj 是否为 Double 或其子类的实例,如果是再进行值的比较。
这里给大家提个醒,示例代码如下:

Double a = 0d;
a.equals(0); //返回的是false
a == 0; //返回的是true

String 类

重写的 hashCode 方法

源代码如下:

/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/**
 * 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]
 */
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;
}

对于这段源代码,我们可以看到,String 底层是通过字符数组来操作管理字符串。注意,这个字符数组 value 是被声明为 final 的,表明 String 的设计者想让这个字符数组一旦被赋值就不再改变(由于 value 是引用类型,虽然它的值不能再改变,但是它所指向的地址里的内容可以改变,大家有兴趣可以去搜一下 final 关键字)。

而实际上也确实如此,其实从这段代码也可以看出设计者的这个意图,因为字符串的哈希码只在第一次调用 hashCode 方法时计算了一次,后面再调用 hashCode 方法都是直接返回 hash 的值。

有些童鞋可能会说源代码注释里明明说 hash 默认为0,但是也没见到哪里对它初始化为 0 呀(这是 Java 的一个小知识点,知道的可以跳过这段)。这是因为 int 型变量作局部变量时必须初始化,否则会编译报错,这是语法规定。但是作为类的成员变量时是默认初始化为 0 的。

从这段代码可以看到 String 类型哈希码的计算方式为 s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1](n 为字符串长度),源代码里使用了一点小技巧,绕了一个小弯弯。

重写的 equals 方法

源代码如下:

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

从源代码中可以看到,String 的 equals 方法首先比较两者的引用是否相等,不等的话判断被比较对象是否为 String及其子类的实例,如果是再优先判断两字符串长度是否相等,如果还为真,最后进行逐字符的比较。