哈希是计算机学科基本概念之一。
在java中,一些常用集合都基于高效哈希算法。如HashMap、HashSet。
本文我们将重点讨论hashCode()工作原理以及如何在集合中发挥作用。
一定场景下,常用集合搜索操作是非常低效的。举例,包含大量元素的list中触发线性搜索:
List words = Arrays.asList("Welcome", "to", "Baeldung");
if (words.contains("Baeldung")) {
System.out.println("Baeldung is in the list");
}
java中提供一些数据结构可以解决这类问题,如几个实现Map接口的哈希实现类。
使用哈希算法的集合通过hashCode()方法计算给定键的哈希值,然后内部使用该值去存储数据,为了查询操作更有效率。
简单地说,hashCode() 方法通过哈希算法生成一个整数值。
相对的对象(通过equals方法判断)返回相同哈希值,不同对象返回不同哈希值不是必须的。
hashCode()方法的算法约定为:
在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
两个相对的对象(通过equals方法判断)必须返回相同哈希值。
两个不相对的对象(通过equals方法判断),调用hashCode()方法返回值不是必须不相等。但开发者需了解,不同对象返回不同的哈希值会提升效率。
实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)
实际上有一个完全遵循上述契约的hashCode()方法实现非常简单。
为了演示,我们定义一个示例User类,它覆盖了该方法的默认实现:
public class User {
private long id;
private String name;
private String email;
// standard getters/setters/constructors
@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));
}
// getters and setters here
}
User类提供自定义equal和hashCode方法,完全遵循各自的约定,即使hashCode方法返回任何固定值也不是非法的。
然而,这个实现完全降低哈希表的功能,每个对象将保存在相同位置,单一桶中。这时哈希表查询将执行线性搜索,完全没有带来任何优势。
让我们稍微改善上述hashCode()方法的实现,使用User类的所有字段,为了能够让不相等的对象生成不同的结果。
@Override
public int hashCode() {
return (int) id * name.hashCode() * email.hashCode();
}
这个基本的哈希算法肯定要比前一个好得多,因为它只是通过将名称和电子邮件字段和id的哈希代码相乘来计算对象的哈希码。
一般来说,我们可以说这是一个合理的hashCode()实现,只要我们使equals()实现与它保持一致。
计算哈希值的哈希算法越好,哈希表的性能就越好。下面我们讨论标准实现,使用两个素数来增加计算哈希值的唯一性。
@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()实现。从java 7 之后,Objects.hash() 工具方法可以方便实现:
Objects.hash(name, email)
IntelliJ IDEA 生成下面实现:
@Override
public int hashCode() {
int result = (int) (id ^ (id >>> 32));
result = 31 * result + name.hashCode();
result = 31 * result + email.hashCode();
return result;
}
Eclipse生成实现如下:
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((email == null) ? 0 : email.hashCode());
result = prime * result + (int) (id ^ (id >>> 32));
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
除了上述基于IDE的hashCode实现,也可以通过其他工具进行有效实现,如Lombok,其maven依赖如下:
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombok-mavenartifactId>
<version>1.16.18.0version>
<type>pomtype>
dependency>
现在User类仅需要增加@EqualsAndHashCode注解就可以实现:
@EqualsAndHashCode
public class User {
// fields and methods here
}
类似使用 Apache Commons Lang’s HashCodeBuilder 类也可以生成 hashCode() 实现, commons-lang Maven 依赖如下:
<dependency>
<groupId>commons-langgroupId>
<artifactId>commons-langartifactId>
<version>2.6version>
dependency>
hashCode()方法实现如下:
public class User {
public int hashCode() {
return new HashCodeBuilder(17, 37).
append(id).
append(name).
append(email).
toHashCode();
}
}
一般来说,在实现hashCode()时没有通用的诀窍。强烈建议您阅读 Joshua Bloch’s Effective Java, ,它提供了实现高效散列算法的完整指南。
这里我们注意到所有这些实现都以某种形式使用数字31——这是因为31有一个很好的属性——它的乘法运算可以被比标准乘法运算快的位移所代替:
31 * i == (i << 5) - i
哈希表的内在行为支持这些数据结构的相关实现:即使哈希算法非常有效,两个或多个对象不相等,也有可能产生相同的哈希值。所以它们的哈希值指向相同的桶,虽然它们有不同的key值。
通常这种场景我们称为哈希冲突,存在多种方式可以处理,每个都尤其优劣。java HashMap采用单独链接方法处理冲突:
当两个或多个对象指向相同的桶,这些对象在桶中以linked List方式存储。这种情况下,哈希表是linked List的数组,每个相同哈希值对象被追加至桶的linked List中。
最坏情况下,几个桶仅绑定一个linked List,则在list中检索对象只能是线性搜索。哈希冲突说明为什么实现有效hashCode方法非常重要。
java 8 提供了HashMap的增强实现,如果桶大小超出一定阈值,会使用tree map代替 linked List,只有时间复杂度从O(n)提升至O(logn)。
为了测试标准hashCode方法功能实现,我们创建一个简单java应用,增加一些User对象至HashMap中,并通过日志消息查看输出结果。
这里是应用程序入口:
public class Application {
public static void main(String[] args) {
Map users = new HashMap<>();
User user1 = new User(1L, "John", "[email protected]");
User user2 = new User(2L, "Jennifer", "[email protected]");
User user3 = new User(3L, "Mary", "[email protected]");
users.put(user1, user1);
users.put(user2, user2);
users.put(user3, user3);
if (users.containsKey(user1)) {
System.out.print("User found in the collection");
}
}
}
hashCode方法实现如下:
public class User {
// ...
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());
logger.info("hashCode() called - Computed hash: " + hash);
return hash;
}
}
这里唯一需要强调的细节是,每当对象存储到HashMap中并通过containsKey()方法进行检查时,都会调用hashCode()并将计算出来的哈希值打印到控制台:
[main] INFO com.dataz.entities.User - hashCode() called - Computed hash: 1255477819
[main] INFO com.dataz.entities.User - hashCode() called - Computed hash: -282948472
[main] INFO com.dataz.entities.User - hashCode() called - Computed hash: -1540702691
[main] INFO com.dataz.entities.User - hashCode() called - Computed hash: 1255477819
User found in the collection
很明显,生成高效的hashCode()实现通常需要混合一些数学概念(即素数和任意数)、逻辑和基本的数学操作。
不管怎样只要我们能确保散列算法为不相等的对象生成不同的散列码并与equals()的实现保持一致,就完全有可能实现有效的hashCode()方法。