今天翻“Effective Java”的时候看到了改写hashCode()方法的三条约定,突然想到了某些问题,故记录之。
1.在每个改写了equasl方法的类中,你也必须要改写hashCode方法。
2.如果equals相等,两个对象的hashCode必须相等。
3.不相等的对象倾向于产生
不相等的散列码,但是不相等的对象可以有相等的散列码。
我在写代码的时候曾遇到过如下情况:
class Person {
String name ;
int age ;
public Person (String name , int age ){
this . name = name ;
this . age = age ;
}
public boolean equals (Object o ){
if (o instanceof Person ){
Person p=( Person )o ;
return name . equals (p. name ) && age == p. age ;
}
else return false ;
}
}
public class Test {
public static void main (String ... s){
HashSet < Person > s = new HashSet < Person >();
s . add (new Person ("hello" , 23 ));
System.out.println(
s.contains(new Person("hello" , 23 )));
}
}
这段代码很simple很naive,但是当看到结果的时候依然迷惑了~因为我所期望的结果是输出true,但很不幸的是这段代码的结果输出为false。更让我感到困惑的是,如果对这两个对象调用equals方法比较,他们必然是相等的。
于是我便去参阅jdk中HashSet类的源码,才知道原来HashSet其实就是一个HashMap。众所周知HashMap中存放的是键值对,
因此所有的键都必须是唯一的,在HashMap中不可能同时存在两个相同的键。所以HashSet便是利用这个特性,将add进这个Set的对象都当做是
Map的某个key。下面是HashSet实现类的代码片段:
public class HashSet < E >{
private transient HashMap < E , Object > map ;
private static final Object PRESENT = new Object ();
public HashSet () {
map = new HashMap < E , Object >();
}
public boolean add (E e ) {
return map . put (e , PRESENT )== null ;
}
public boolean contains (Object o ) {
return map . containsKey (o );
}
……
}
public boolean add (E e ) {
return map . put (e , PRESENT )== null ;
}
// 首次调用add方法添加元素e的时候,map中会建立 e--->PRESENT 的键值对。
// 由于此前map中不存在e作为key,因此会返回一个null值,此时add方法调用成功,返回true。
// 首次调用add方法添加元素e的时候,map中已存在 e--->PRESENT ,因此会把这个PRESENT
// 当成是旧值返回,由于PRESENT!=null,故add方法返回false。
可以清楚的看出来,HashSet类中包含了一个map,而且是HashMap,同时还保存了一个空对象PRESENT
。每当我们调用add方法的时候,实质上就是在hashMap中插入一对键值。此时充当键值的是我们想插入Set的元素E,而充当值的,正是PRESENT
。其实这个结构挺囧的,因为HashMap中存放的所有值,都是这个PRESENT
的引用。
参阅javaAPI手册,HashMap.put方法有一个返回值V。这个V是与key关联的旧值,如果key没有任何映射关系,则返回null。
现在回归最开始的问题,这个问题可以归结如下。
public class Test {
public static void main (String ... s){
Person p1 = new Person ("hello" , 23 );
Person p2 = new Person ("hello" , 23 );
System . out . println (p1 . equals (p2 )); //输出true
HashSet < Person > s = new HashSet < Person >();
s . add (p1 );
System . out . println (s. contains (p2 )); //输出false
}
}
第一个输出true很好理解,第二个输出false有点费解。
当执行完 s
.
add
(p1
) 之后,s中的map里已经包含了 P1
--->PRESENT
的键值对。然后对这个map进行查询,发现不含有p2键。这说明有两个equals相等的对象,正在一个hashMap中充当着不同的键,这很不合理!那么HashMap中的键究竟是根据什么来判断他们是否相同呢?不妨来研究一下HashMap类:
public class HashMap < K , V >{
transient Entry [] table ; //这是一个Entry数组,注意Entry其实是一个链表
public V put (K key , V value ) {
if (key == null )
return putForNullKey (value ); //key是null的情况
// 如果key不是null,首先调用hash方法进一步散列。然后通过indexFor 函数将
// 所得的hash码映射到 [0,table.length-1] 区间,得到的映射值为i。
// 遍历table[i]这个链表,如果已经有某个Entry 结点的key与所要插入的key相等,
// 那么修改这个Entry结点,用value替换掉原来的旧值
// 如果table[i]链表中尚不存在这样键为key的Entry,则生成一个这样的结点,
// 插入到table[i]的首个结点之前
int hash = hash (key . hashCode ());
int i = indexFor (hash , table . length );
for (Entry < K , V > e = table [ i ]; e != null ; e = e . next ) {
Object k ;
if (e . hash == hash && ((k = e . key ) == key || key . equals (k ))) {
V oldValue = e . value ;
e . value = value ;
e . recordAccess (this );
return oldValue ;
}
}
modCount ++;
addEntry (hash , key , value , i );
return null ;
}
/**
* 如果key是null,那么这个value一定是放在table [ 0 ] 所在的链表里的
* 如果table [ 0 ] 这个链表中本来就存在Entry,那么会遍历这个链表,直到找到key是null的
* 结点,用新的value替换掉旧的,返回旧值
* 如果table [ 0 ] 这个链表中为空,或者不存在key为null的Entry结点,那么会调用addEntry
* 方法,该方法创建一个结点,插入到table [ 0 ] 链表的首个结点之前
*/
private V putForNullKey (V value ) {
for (Entry < K , V > e = table [ 0 ]; e != null ; e = e . next ) {
if (e . key == null ) {
V oldValue = e . value ;
e . value = value ;
e . recordAccess (this );
return oldValue ;
}
}
modCount ++;
addEntry (0 , null , value , 0 );
return null ;
}
//h ash方法用于将对象的hashCode进一步散列,具体的算法看不懂
static int hash (int h ) {
h ^= (h >>> 20 ) ^ (h >>> 12 );
return h ^ (h >>> 7 ) ^ (h >>> 4 );
}
static int indexFor (int h , int length ) {
return h & (length - 1 );
}
}
阅读完HashMap的代码片段,至少能够确定一点,用对象A作为键去关联另一个对象B,可以说其实是建立了对象的A的散列码和对象B之间的映射。因此在上面一个set的例子中,两个Person对象P1与P2虽然用equals方法相等,但是由于没有覆写hashCode方法,所以P1和P2返回的散列码并不一样。故P1与P2可以在hashMap中充当不同的键。
回到刚开始的三点建议~当我们改写了equals方法的时候,还需要改写hashCode方法。并且需要保证如果equals方法相等,那么hashCode也一定要相等。这两条建议其实是强制保证我们代码的语义与真正执行结果之间的一致性。因为很多时候,equals方法并不简单比较引用是否相等,我们更需要的是一个能够进行对象内容比较的equals方法。所以如果我们认为两个对象相等了,正如P1和P2,那么我们要确保
s
.
add
(p1
)后s.
contains
(p2
)一定能够返回true.
至于第三点建议,如果不相等对象产生了相同的散列码也没有问题。用不同散列码的好处就在于可以将键散列到Entry数组的不同位置,我们下次访问的时候,仅仅是访问数组的某一个单元,消耗的是单位时间。但是如果在hashMap中用这些相同散列码的对象作为键值,那么生成的键值对肯定在数组某个单元包含的同一串链表上,查找或者修改这样一个链表是很费时的。
分享到:
相关推荐
hashCode 的用法详解 hashCode 是一种用于查找和排序的机制,在数据结构中 plays a crucial role。下面我们将对 hashCode 的用法进行详细的解释。 hashCode 的作用 hashCode 的主要作用是用于查找和排序。在查找...
### 深入理解 HashCode 方法 #### 一、HashCode 的基本概念与作用 在 Java 编程语言中,`HashCode` 是一个非常重要且基础的概念。简单来说,`HashCode` 是一个整数值,用于快速定位对象的位置。在 Java 中,每一个...
Java中的hashCode 在Java中,hashCode是Object类中的一个方法,它的作用是返回对象的哈希码值。哈希码值是一个整数,它可以用于在哈希表中快速地定位对象。然而,实际上,hashCode根本不能代表object的内存地址。 ...
"Java中Hashcode的作用" Hashcode是Java编程语言中一个非常重要的概念,它在equals方法中扮演着关键角色。在Java中,每个对象都具有一个独特的Hashcode,它可以用来标识对象的身份。但是Hashcode是什么?它是如何...
在Java编程语言中,`equals()` 和 `hashCode()` 方法是Object类中的两个核心方法,所有类都默认继承自Object类。这两个方法在处理对象比较和集合操作时起着至关重要的作用。当我们创建自定义类并需要对对象进行精确...
### HashCode的作用详解 #### 一、HashCode的基本概念 在Java中,`hashCode()` 方法是 `Object` 类的一个重要成员方法,它返回一个整数,这个整数通常用来表示对象的哈希值。哈希值在Java集合框架中扮演着至关重要...
《深入HashCode》 在计算机科学领域,特别是在Java和许多其他面向对象编程语言中,`hashCode()`方法是一个至关重要的概念。这个方法是每个对象都具备的,它返回一个整数值,通常用于快速比较对象或者在哈希表(如...
equals 与 hashCode 方法讲解 equals 方法和 hashCode 方法是 Java 语言中两个重要的方法,它们都是在 Object 类中定义的。equals 方法用于比较两个对象是否相等,而 hashCode 方法用于返回对象的哈希码。 在 Java...
### hashCode的作用 在Java编程语言中,`hashCode`方法是一个重要的概念,主要用于对象的查找与存储,尤其是在集合框架中有着广泛的应用。为了更好地理解`hashCode`的作用及其在实际开发中的重要性,我们可以从以下...
equals()和hashcode()这两个方法都是从object类中继承过来的。当String 、Math、还有Integer、Double。。。。等这些封装类在使用equals()方法时,已经覆盖了object类的equals()方法.
在Java编程语言中,`hashCode()` 和 `equals()` 是两个非常重要的方法,它们主要用于对象的比较和哈希表(如HashMap)的操作。标题提到的"HashCode相同equals不同的2位字符集合算法"涉及到的是一个特定场景:两个...
PPT浅析hashcode定义和作用;和简单的代码演示PPT.很简单的
java中hashCode()的理解
本文介绍了Java语言不直接支持关联数组,可以使用任何对象作为一个索引的数组,但在根Object类中使用 hashCode()方法明确表示期望广泛使用HashMap。理想情况下基于散列的容器提供有效插入和有效检索;直接在对象模式...
1,如果两个对象相同,那么它们的hashCode值一定要相同; 2,如果两个对象的hashCode相同,它们并不一定相同 上面说的对象相同指的是用eqauls方法比较。 3,HashCode码不唯一
### 复写`hashCode()`方法与`equals()`方法 在Java编程中,`hashCode()`方法与`equals()`方法是对象比较中的两个非常重要的方法。它们主要用于判断对象是否相等以及对象的散列值,这对于集合类(如`HashSet`)来说...
Java重写equals同时需要重写hashCode的代码说明,以及如何重写hashCode方法,此代码演示按照effective java书籍说明的重写思路。代码中演示了使用集合存储对象,并且对象作为key,需重写equals和hashCode.
在Java编程语言中,`hashCode()`和`equals()`方法是对象身份验证的关键组成部分,它们主要用于对象的比较和哈希表(如HashMap、HashSet等)的操作。理解这两个方法的工作原理对于编写高效和可靠的代码至关重要。 ...
本主题将深入探讨如何利用反射技术绕过编译器的一些限制,并介绍hashcode在高级应用中的用法。 首先,让我们理解反射的基本概念。在Java中,反射提供了一种方式,使我们能够在运行时动态地获取类的信息(如类名、...
### Java中`hashCode()`与`equals()`方法详解 #### 前言 在Java编程语言中,`hashCode()`和`equals()`方法是非常重要的概念,它们不仅对于深入理解Java内存管理至关重要,也是实现自定义类的关键部分之一。本文将...