关于java:面试题系列第4篇重写了equals方法为什么还要重写hashCode方法

5次阅读

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

《Java 面试题系列》:一个长常识又很有意思的专栏。深刻开掘、剖析源码、汇总原理、图文联合,打造公众号系列文章,面试与否均可晋升 Level。欢送继续关注【程序新视界】。本篇为第 4 篇。

外围问题:重写了 equals 办法,为什么还要重写 hashCode 办法?

这不仅仅是一道面试题,而且是关系到咱们的代码是否强壮和正确的问题。在后面两篇文章波及到了 equals 办法的底层解说:《说说 == 和 equals 的区别?你的答复可能是谬误的》和《Integer 等号判断的底细,你可能不晓得?》。

本篇文章,带大家从底层来剖析一下 hashcode 办法重写的意义以及如何实现。

回顾 equals 办法

咱们先回顾一下 Object 的 equals 办法实现,并简略汇总一下应用 equals 办法的法则。

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

通过下面 Object 的源代码,能够得出一个论断:如果一个类未重写 equals 办法,那么实质上通过“==”和 equals 办法比拟的成果是一样的,都是比拟两个对象的的内存地址。

后面两篇文章讲到 String 和 Integer 在比拟时的区别,关键点也是它们对 equals 办法的实现。

面试时总结一下就是:默认状况下,从 Object 类继承的 equals 办法与“==”齐全等价,比拟的都是对象的内存地址。但咱们能够重写 equals 办法,使其依照须要进行比拟,如 String 类重写了 equals 办法,比拟的是字符的序列,而不再是内存地址。

与 hashCode 办法的关系

那么 equals 办法与 hashCode 办法又有什么关系呢?咱们来看 Object 上 equals 办法的一段正文。

Note that it is generally necessary to override the hashCode method whenever this method 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 办法也重写,这样做能力保障不违反 hashCode 办法中“雷同对象必须有雷同哈希值”的约定。

此处只是揭示了咱们重写 hashCode 办法的必要性,那其中提到的 hashCode 办法设计约定又是什么呢?相干的内容定义在 hashCode 办法的注解局部。

hashCode 办法约定

对于 hashCode 办法的约定原文比拟多,大家间接看源码即可看到,这里汇总一下,共三条:

(1)如果对象在应用 equals 办法中进行比拟的参数没有批改,那么屡次调用一个对象的 hashCode()办法返回的哈希值应该是雷同的。

(2)如果两个对象通过 equals 办法比拟是相等的,那么要求这两个对象的 hashCode 办法返回的值也应该是相等的。

(3)如果两个对象通过 equals 办法比拟是不同的,那么也不要求这两个对象的 hashCode 办法返回的值是不雷同的。然而咱们应该晓得对于不同对象产生不同的哈希值对于哈希表 (HashMap 等) 可能进步性能。

其实,看到这里咱们理解了 hashCode 的实现规约,但还是不分明为什么实现 equals 办法须要重写 hashCode 办法。但咱们能够得出一条法则:hashCode 办法实际上必须要实现的一件事件就是,为 equals 办法认定为雷同的对象返回雷同的哈希值。

其实在下面规约中提到了哈希表,这也正是 hashCode 办法使用的场景之一,也是咱们为什么要重写的外围。

hashCode 利用场景

如果理解 HashMap 的数据结构,就会晓得它用到“键对象”的哈希码,当咱们调用 put 办法或者 get 办法对 Map 容器进行操作时,都是依据键对象的哈希码来计算存储地位的。如果咱们对哈希码的获取没有相干保障,就可能会得不到预期的后果。

而对象的哈希码的获取正是通过 hashCode 办法获取的。如果自定义的类中没有实现该办法,则会采纳 Object 中的 hashCode()办法。

在 Object 中该办法是一个本地办法,会返回一个 int 类型的哈希值。能够通过将对象的外部地址转换为整数来实现的,然而 Java 中没有强制要求通过该形式实现。

具体实现网络上有不同的说法,有说通过内置地址转换得来,也有说“OpenJDK8 默认 hashCode 的计算方法是通过和以后线程无关的一个随机数 + 三个确定值,使用 Marsaglia’s xorshift scheme 随机数算法失去的一个随机数”取得。

无论默认实现是怎么的,大多数状况下都无奈满足 equals 办法雷同,同时 hashCode 后果也雷同的条件。比方上面的示例重写与否差距很大。

public void test1() {
    String s = "ok";
    StringBuilder sb = new StringBuilder(s);
    System.out.println(s.hashCode() + " " + sb.hashCode());

    String t = new String("ok");
    StringBuilder tb = new StringBuilder(s);
    System.out.println(t.hashCode() + " " + tb.hashCode());
}

下面这段代码打印的后果为:

3548  1833638914
3548  1620303253

String 实现了 hashCode 办法,而 StringBuilder 并没有实现,这就导致即便值是一样的,hashCode 也不同。

上个示例中问题还不太显著,上面咱们以 HashMap 为例,看看如果没有实现 hashCode 办法会导致什么重大的结果。

@Test
public void test2() {
    String hello = "hello";

    Map<String, String> map1 = new HashMap<>();
    String s1 = new String("key");
    String s2 = new String("key");
    map1.put(s1, hello);
    System.out.println("s1.equals(s2):" + s1.equals(s2));
    System.out.println("map1.get(s1):" + map1.get(s1));
    System.out.println("map1.get(s2):" + map1.get(s2));


    Map<Key, String> map2 = new HashMap<>();
    Key k1 = new Key("A");
    Key k2 = new Key("A");
    map2.put(k1, hello);
    System.out.println("k1.equals(k2):" + s1.equals(s2));
    System.out.println("map2.get(k1):" + map2.get(k1));
    System.out.println("map2.get(k2):" + map2.get(k2));
}

class Key {

    private String k;

    public Key(String key) {this.k = key;}

    @Override
    public boolean equals(Object obj) {if (obj instanceof Key) {Key key = (Key) obj;
            return k.equals(key.k);
        }
        return false;
    }
}

实例中定义了外部类 Key,其中实现了 equals 办法,但未实现 hashCode 办法。寄存于 Map 中的 value 值都是字符串“hello”。

代码分两段,第一段演示当 Map 的 key 通过实现了 hashCode 的 String 时是什么成果;第二段演示了当 Map 的 key 通过未实现 hashCode 办法的 Key 对象时是什么成果。

执行上述代码,打印后果如下:

s1.equals(s2):true
map1.get(s1):hello
map1.get(s2):hello
k1.equals(k2):true
map2.get(k1):hello
map2.get(k2):null

剖析后果能够看出,对于 String 作为 key 的 s1 和 s2 来说,通过 equals 比拟相等是天然的,取得的值也是雷同的。但 k1 和 k2 通过 equals 比拟是相等,但为什么在 Map 中取得的后果却不一样?实质上就是因为没有重写 hashCode 办法导致 Map 在存储和获取过程中调用 hashCode 办法取得的值不统一。

此时在 Key 类中增加 hashCode 办法:

@Override
public int hashCode(){return k.hashCode();
}

再次执行,便可失常取得对应的值。

s1.equals(s2):true
map1.get(s1):hello
map1.get(s2):hello
k1.equals(k2):true
map2.get(k1):hello
map2.get(k2):hello

通过下面的典型实例演示了不重写 hashCode 办法的潜在结果。简略看一下 HashMap 中的 put 办法。

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 通过哈希值来查找底层数组位于该地位的元素 p,如果 p 不为 null,则应用新的键值对来笼罩旧的键值对
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // (二者哈希值相等)且(二者地址值相等或调用 equals 认定相等)。if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 如果底层数组中存在传入的 Key,那么应用新传入的笼罩掉查到的
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

在上述办法中,put 办法在拿到 key 的第一步就对 key 对象调用了 hashCode 办法。暂且不看前面的代码,如果没有重写 hashCode 办法,就无奈确保 key 的 hash 值统一,后续操作就是两个 key 的操作了。

重写 hashCode 办法

理解了重写 hashCode 办法的重要性,也理解了对应的规约,那么上面咱们就聊聊如何优雅的重写 hashCode 办法。

首先,如果应用 IDEA 的话,那么间接应用快捷键即可。

生成的成果如下:

@Override
public boolean equals(Object o) {if (this == o) {return true;}
    if (o == null || getClass() != o.getClass()) {return false;}
    Key key = (Key) o;
    return Objects.equals(k, key.k);
}

@Override
public int hashCode() {return Objects.hash(k);
}

依据须要可对生成的办法外部实现进行批改。在下面的实例中用到了 java.util.Objects 类,它的 hash 办法的长处是如果参数为 null,就只返回 0,否则返回对象参数调用的 hashCode 的后果。Objects.hash 办法源码如下:

public static int hash(Object... values) {return Arrays.hashCode(values);
}

其中 Arrays.hashCode 办法源码如下:

public static int hashCode(Object a[]) {if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}

当然此处只有一个参数,也能够间接应用 Objects 类 hashCode 办法:

public static int hashCode(Object o) {return o != null ? o.hashCode() : 0;
}

如果是多个属性都参加 hash 值的状况倡议可应用第一个办法。只不过须要留神,在类构造(成员变量)变动时,同步增减办法外面的参数值。

小结

当咱们筹备面试时,始终在背诵“实现 equals 办法的同时也要实现 hashCode 办法”,牢记这些论断并没有错。但咱们也不能因为匆忙筹备面试题,而遗记了这些面试题之所以频繁呈现的起因是什么。当深刻摸索之后,会发现在那些干燥的论断背地还有这么多不容忽视的知识点,还有这么多有意思的设计与陷阱。

我是感觉越钻研越有意思,越钻研越发现自己已经的无知。你呢?关注一下,期待下一篇文章吧。

<center>程序新视界:精彩和成长都不容错过 </center>

正文完
 0