JAVA基础—hash code

概述

哈希是计算机科学的一个基本概念。

在java中,一些最流行的集合背后都依靠着高效的哈希,就像HashMap和HashSet。

集合上的一些简单操作在某些情况下可能是低效的。举例说明,下列的操作触发了一个线性搜索,对于一个巨大的列表极有可能是非常低效的。

List<String> wordList = Arrays.asList("a","bc","de");
if(wordList.contains("bc")){
    out.println("this is bc!");
}

Java提供了一些数据结构以应对这种特定的问题。例如,几个Map接口都实现了hash tables。当使用哈希表的时候,这些集合会使用hashCode()方法为给定的key计算hash值。然后他们在内部使用这些值来存储数据,使数据访问操作更高效。

hashCode()是怎么工作的

简单的说,hashCode()是通过哈希算法产生一个整型值并且返回。

对象如果相等就必须返回相同的哈希值。不同的对象不需要返回不同的哈希值。

hashCode()方法的一般约定声明:

  • 在一个Java应用程序执行期间,如果用于作相等对比的对象上的信息没有被修改,无论在同一个对象上调用多少次hashCode()必须始终返回相同的值。这个值不需要在应用程序的一次执行中,与同一应用的另外的一次执行保持一致(即同一个应用的两次执行,hash值不一定保持一致)。
  • 如果两个对象通过equals(Object)方法确定是相等的,在两个对象上分别调用hashCode()方法必须生成相同的值。(即如果两个对象使用equals(Object)对比相等,则两个对象的哈希值一定相等)
  • 如果两个对象使用equals(java.lang.Object)方法确定两个对象不相等,在两个对象上分别调用hashCode()方法不需要产生不同的整数结果。然而,程序员应该知道为不同的对象生成不同的整数结果是可以提高hash表性能的。

原生的hashCode()的实现

一个完全遵守上述约定的原生hashCode()实现是非常简单的。为了演示它,我们定义一个简单的User类,并且覆盖原有方法的默认实现。

public class User {
    private long id;
    private String name;
    private String email;

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if (this.getClass() != o.getClass()) return false;
        User user = (User) o;
        return id == user.id
                && (name.equals(user.name)
                && email.equals(user.email));
    }
}

这个User类为equals()和hashCode()实现了自定义的实现,甚至,让hashCode()返回一个固定的值也完成没有任何问题。但是,这个实现降低了hash表的功能到几乎为0,因为每一个对象在各自的桶中都将存储相同的值。

在这个背景下,一个哈希表的搜索是线性执行的,不会给我们带来实际的提升。

提升hashCode()的实现

让我们能过Person类所包含的所有的字段来改善hashCode()的实现,以便它能对不同的对象产生不同的结果。

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

这个基础的哈希算法比前一个定义更好。这是因为它计算了对象的哈希值是通过name的hash值乘以email的哈希值再乘以id。

一般来说,我们以说这是一个合理的hashCode()的实现,只要我们保持equals()的实现与它一致。

高级的hashCode()实现

我们用来计算哈希值的哈希算法越好,就会更好的提升hash表的性能。让我们观察一个高级的实现,它使用两个素数以增加计算出的哈希值的唯一性。

@Override
public int hashCode() {
    int hash = 7;
    hash = 31 * hash + (int) id;
    hash = 31 * hash + (name == null ? 0 : name.hashCode());
    hash = 31 * hash + (email == null ? 0 : email.hashCode());
    return hash;
}

虽然我们需要理解hashCode()和equals()方法所扮演的角色,但是我们不需要每次都从头开始实现他们。这是因为更多的IDE工具能够生成自定义的hashCode()和equals()实现。并且从Java7开始,我们有一个Objects.hash()工具方法来使哈希得心应手。

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

使用idea来实现hashCode()和equals()

最新的idea已经可以选择使用Java7的hashCode方式来生成代码,如图:

public class User {
    private long id;
    private String name;
    private String email;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return id == user.id && name.equals(user.name) && email.equals(user.email);
    }

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

使用lombok注解完成hashCode()及equals()的实现

@EqualsAndHashCode 
public class User {
    private long id;
    private String name;
    private String email;
}

处理哈希冲突

哈希表的内在行为造就了一个相关方面的数据结构的问题:即使使用一个效的哈希算法,两个或更多的对象甚至在他们不相等的时候也可能有相等的哈希值。所以,他们的哈希值可能指向相同的桶,甚至即使它们有不同的hash表的键。

这种情形常被称为“哈希冲突”,有很多办法可来解决这种冲突,每一种都有优点与缺点。Java的HashMap使用单独的链式方法来处理冲突。

当两个或更多的对象指向同一个桶时,他们被简单的存储在链表中。在这种情形下,hash表是一个链表数组,并且每一个具有相同哈希的对象都被添加到数组中桶的索引指向在链表中。

在更严峻的情形下,几个桶就有一个链表,并且在表中搜索一个对象是线性执行的。

哈希冲突理论简单的的说明了实现高效hashCode()是多么重要。

Java8提出了一个有趣的改进型的HashMap的实现。如果一个桶的大小超过一定的阈值,就使用一个树图(tree map)来代替链表。这样做就实现了O(logn)搜索,而不是O(n)搜索。

总结

很明显,生成高效的hashCode()实现常常需要混合一些数学思想(素数和任意数),逻辑和基本的数学运算。不管怎样,我们能够实现高效的hashCode()而根本不需要依靠这些技术。我们只需要确认哈希算法,为不同对象生成不同的哈希值,并且确保它与equals()的实现一致。

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注