您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

对Java中HashCode方法的深入思考

时间:2019-08-12 11:19:30  来源:  作者:

前言

最近在学习 Go 语言,Go 语言中有指针对象,一个指针变量指向了一个值的内存地址。学习过 C 语言的猿友应该都知道指针的概念。Go 语言语法与 C 相近,可以说是类 C 的编程语言,所以 Go 语言中有指针也是很正常的。我们可以通过将取地址符&放在一个变量前使用就会得到相应变量的内存地址。

package main
import "fmt"
func main() {
 var a int= 20 /* 声明实际变量 */
 var ip *int /* 声明指针变量 */
 ip = &a /* 指针变量的存储地址 */
 fmt.Printf("a 变量的地址是: %xn", &a )
 /* 指针变量的存储地址 */
 fmt.Printf("ip 变量储存的指针地址: %xn", ip )
 /* 使用指针访问值 */
 fmt.Printf("*ip 变量的值: %dn", *ip )
}
复制代码

因为本人主要开发语言是 JAVA,所以我就联想到 Java 中没有指针,那么 Java 中如何获取变量的内存地址呢?

如果能获取变量的内存地址那么就可以清晰的知道两个对象是否是同一个对象,如果两个对象的内存地址相等那么无疑是同一个对象反之则是不同的对象。

很多人说对象的 HashCode 方法返回的就是对象的内存地址,包括我在《Java核心编程·卷I》的第5章内容中也发现说是 HashCode 其值就是对象的内存地址。

对Java中HashCode方法的深入思考

 

**但是 HashCode 方法真的是内存地址吗?**回答这个问题前我们先回顾下一些基础知识。

==和equals

在 Java 中比较两个对象是否相等主要是通过 ==号,比较的是他们在内存中的存放地址。Object 类是 Java 中的超类,是所有类默认继承的,如果一个类没有重写 Object 的 equals方法,那么通过equals方法也可以判断两个对象是否相同,因为它内部就是通过==来实现的。

//Indicates whether some other object is "equal to" this one.
public boolean equals(Object obj) {
 return (this == obj);
}
复制代码

**Tips:**这里额外解释个疑惑

我们学习 Java 的时候知道,Java 的继承是单继承,如果所有的类都继承了 Object 类,那么为何创建一个类的时候还可以extend其他的类?

这里涉及到直接继承和间接继承的问题,当创建的类没有通过关键字 extend 显示继承指定的类时,类默认的直接继承了Object,A --> Object。当创建的类通过关键字 extend 显示继承指定的类时,则它间接的继承了Object类,A --> B --> Object。

这里的相同,是说比较的两个对象是否是同一个对象,即在内存中的地址是否相等。而我们有时候需要比较两个对象的内容是否相同,即类具有自己特有的“逻辑相等”概念,而不是想了解它们是否指向同一个对象。

例如比较如下两个字符串是否相同String a = "Hello" 和 String b = new String("Hello"),这里的相同有两种情形,是要比较 a 和 b 是否是同一个对象(内存地址是否相同),还是比较它们的内容是否相等?这个具体需要怎么区分呢?

如果使用 == 那么就是比较它们在内存中是否是同一个对象,但是 String 对象的默认父类也是 Object,所以默认的equals方法比较的也是内存地址,所以我们要重写 equals方法,正如 String 源码中所写的那样。

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;
}
复制代码

这样当我们 a == b时是判断 a 和 b 是否是同一个对象,a.equals(b)则是比较 a 和 b 的内容是否相同,这应该很好理解。

JDK 中不止 String 类重写了equals 方法,还有数据类型 Integer,Long,Double,Float等基本也都重写了 equals 方法。所以我们在代码中用 Long 或者 Integer 做业务参数的时候,如果要比较它们是否相等,记得需要使用 equals 方法,而不要使用 ==。

因为使用 ==号会有意想不到的坑出现,像这种数据类型很多都会在内部封装一个常量池,例如 IntegerCache,LongCache 等等。当数据值在某个范围内时会直接从常量池中获取而不会去新建对象。

如果要使用==,可以将这些数据包装类型转换为基本类型之后,再通过==来比较,因为基本类型通过==比较的是数值,但是在转换的过程中需要注意 NPE(NullPointException)的发生。

Object中的HashCode

equals 方法能比较两个对象的内容是否相等,因此可以用来查找某个对象是否在集合容器中,通常大致就是逐一去取集合中的每个对象元素与需要查询的对象进行equals比较,当发现某个元素与要查找的对象进行equals方法比较的结果相等时,则停止继续查找并返回肯定的信息,否则,返回否定的信息。

但是通过这种比较的方式效率很低,时间复杂度比较高。那么我们是否可以通过某种编码方式,将每一个对象都具有某个特定的码值,根据码值将对象分组然后划分到不同的区域,这样当我们需要在集合中查询某个对象时,我们先根据该对象的码值就能确定该对象存储在哪一个区域,然后再到该区域中通过equals方式比较内容是否相等,就能知道该对象是否存在集合中。

通过这种方式我们减少了查询比较的次数,优化了查询的效率同时也就减少了查询的时间。

这种编码方式在 Java 中就是 hashCode 方法,Object 类中默认定义了该方法, 它是一个 native 修饰的本地方法,返回值是一个 int 类型。

/**
 * Returns a hash code value for the object. This method is
 * supported for the benefit of hash tables such as those provided by
 * {@link java.util.HashMap}.
 * ...
 * As much as is reasonably practical, the hashCode method defined by
 * class {@code Object} does return distinct integers for distinct
 * objects. (This is typically implemented by converting the internal
 * address of the object into an integer, but this implementation
 * technique is not required by the
 * Java™ programming language.)
 *
 * @return a hash code value for this object.
 * @see java.lang.Object#equals(java.lang.Object)
 * @see java.lang.System#identityHashCode
 */
public native int hashCode();
复制代码

从注释的描述可以知道,hashCode 方法返回该对象的哈希码值。它可以为像 HashMap 这样的哈希表有益。Object 类中定义的 hashCode 方法为不同的对象返回不同的整形值。具有迷惑异议的地方就是This is typically implemented by converting the internal address of the object into an integer这一句,意为通常情况下实现的方式是将对象的内部地址转换为整形值。

如果你不深究就会认为它返回的就是对象的内存地址,我们可以继续看看它的实现,但是因为这里是 native 方法所以我们没办法直接在这里看到内部是如何实现的。native 方法本身非 java 实现,如果想要看源码,只有下载完整的 jdk 源码,Oracle 的 JDK 是看不到的,OpenJDK 或其他开源 JRE 是可以找到对应的 C/C++ 代码。我们在 OpenJDK 中找到 Object.c 文件,可以看到hashCode 方法指向 JVM_IHashCode 方法来处理。

static JNINativeMethod methods[] = {
 {"hashCode", "()I", (void *)&JVM_IHashCode},
 {"wait", "(J)V", (void *)&JVM_MonitorWait},
 {"notify", "()V", (void *)&JVM_MonitorNotify},
 {"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
 {"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
复制代码

而JVM_IHashCode方法实现在 jvm.cpp中的定义为:

JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle)) 
 JVMWrApper("JVM_IHashCode"); 
 // as implemented in the classic virtual machine; return 0 if object is NULL 
 return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ; 
JVM_END 
复制代码

这里是一个三目表达式,真正计算获得 hashCode 值的是ObjectSynchronizer::FastHashCode,它具体的实现在synchronizer.cpp中,截取部分关键代码片段。

intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
 if (UseBiasedLocking) {
 
 ......
 
 // Inflate the monitor to set hash code
 monitor = ObjectSynchronizer::inflate(Self, obj);
 // Load displaced header and check it has hash code
 mark = monitor->header();
 assert (mark->is_neutral(), "invariant") ;
 hash = mark->hash();
 if (hash == 0) {
 hash = get_next_hash(Self, obj);
 temp = mark->copy_set_hash(hash); // merge hash code into header
 assert (temp->is_neutral(), "invariant") ;
 test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark);
 if (test != mark) {
 // The only update to the header in the monitor (outside GC)
 // is install the hash code. If someone add new usage of
 // displaced header, please update this code
 hash = test->hash();
 assert (test->is_neutral(), "invariant") ;
 assert (hash != 0, "Trivial unexpected object/monitor header usage.");
 }
 }
 // We finally get the hash
 return hash;
}
复制代码

从以上代码片段中可以发现,实际计算hashCode的是 get_next_hash,还在这份文件中我们搜索get_next_hash,得到他的关键代码。

static inline intptr_t get_next_hash(Thread * Self, oop obj) {
 intptr_t value = 0 ;
 if (hashCode == 0) {
 // This form uses an unguarded global Park-Miller RNG,
 // so it's possible for two threads to race and generate the same RNG.
 // On MP system we'll have lots of RW access to a global, so the
 // mechanism induces lots of coherency traffic.
 value = os::random() ;
 } else
 if (hashCode == 1) {
 // This variation has the property of being stable (idempotent)
 // between STW operations. This can be useful in some of the 1-0
 // synchronization schemes.
 intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
 value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
 } else
 if (hashCode == 2) {
 value = 1 ; // for sensitivity testing
 } else
 if (hashCode == 3) {
 value = ++GVars.hcSequence ;
 } else
 if (hashCode == 4) {
 value = cast_from_oop<intptr_t>(obj) ;
 } else {
 // Marsaglia's xor-shift scheme with thread-specific state
 // This is probably the best overall implementation -- we'll
 // likely make this the default in future releases.
 unsigned t = Self->_hashStateX ;
 t ^= (t << 11) ;
 Self->_hashStateX = Self->_hashStateY ;
 Self->_hashStateY = Self->_hashStateZ ;
 Self->_hashStateZ = Self->_hashStateW ;
 unsigned v = Self->_hashStateW ;
 v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
 Self->_hashStateW = v ;
 value = v ;
 }
 value &= markOopDesc::hash_mask;
 if (value == 0) value = 0xBAD ;
 assert (value != markOopDesc::no_hash, "invariant") ;
 TEVENT (hashCode: GENERATE) ;
 return value;
}
复制代码

从get_next_hash的方法中我们可以看到,如果从0开始算的话,这里提供了6种计算 hash 值的方案,有自增序列,随机数,关联内存地址等多种方式,其中官方默认的是最后一种,即随机数生成。可以看出 hashCode 也许和内存地址有关系,但不是直接代表内存地址的,具体需要看虚拟机版本和设置。

equals和hashCode

equals 和 hashCode 都是 Object 类拥有的方法,包括 Object 类中的 toString 方法打印的内容也包含 hashCode 的无符号十六进制值。

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

由于需要比较对象内容,所以我们通常会重写 equals 方法,但是重写 equals 方法的同时也需要重写 hashCode 方法,有没有想过为什么?

因为如果不这样做的话,就会违反 hashCode 的通用约定,从而导致该类无法结合所有基于散列的集合一起正常工作,这类集合包括 HashMap 和 HashSet。

这里的通用约定,从 Object 类的 hashCode 方法的注释可以了解,主要包括以下几个方面,

  • 在应用程序的执行期间,只要对象的 equals 方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用,hashCode 方法都必须始终返回同一个值。
  • 如果两个对象根据 equals 方法比较是相等的,那么调用这两个对象中的 hashCode 方法都必须产生同样的整数结果。
  • 如果两个对象根据 equals 方法比较是不相等的,那么调用者两个对象中的 hashCode 方法,则不一定要求 hashCode 方法必须产生不同的结果。但是给不相等的对象产生不同的整数散列值,是有可能提高散列表(hash table)的性能。

从理论上来说如果重写了 equals 方法而没有重写 hashCode 方法则违背了上述约定的第二条,相等的对象必须拥有相等的散列值

但是规则是大家默契的约定,如果我们就喜欢不走寻常路,在重写了 equals 方法后没有覆盖 hashCode 方法,会产生什么后果吗?

我们自定义一个 Student 类,并且重写了 equals 方法,但是我们没有重写 hashCode 方法,那么当调用 Student 类的 hashCode 方法的时候,默认就是调用超类 Object 的 hashCode 方法,根据随机数返回的一个整型值。

public class Student {
 private String name;
 private String gender;
 public Student(String name, String gender) {
 this.name = name;
 this.gender = gender;
 }
 //省略 Setter,Gettter
 
 @Override
 public boolean equals(Object anObject) {
 if (this == anObject) {
 return true;
 }
 if (anObject instanceof Student) {
 Student anotherStudent = (Student) anObject;
 if (this.getName() == anotherStudent.getName()
 || this.getGender() == anotherStudent.getGender())
 return true;
 }
 return false;
 }
}
复制代码

我们创建两个对象并且设置属性值一样,测试下结果:

public static void main(String[] args) {
 Student student1 = new Student("小明", "male");
 Student student2 = new Student("小明", "male");
 System.out.println("equals结果:" + student1.equals(student2));
 System.out.println("对象1的散列值:" + student1.hashCode() + ",对象2的散列值:" + student2.hashCode());
}
复制代码

得到的结果

equals结果:true
对象1的散列值:1058025095,对象2的散列值:665576141
复制代码

我们重写了 equals 方法,根据姓名和性别的属性来判断对象的内容是否相等,但是 hashCode 由于是调用 Object 类的 hashCode 方法,所以打印的是两个不相等的整型值。

如果这个对象我们用 HashMap 存储,将对象作为 key,熟知 HashMap 原理的同学应该知道,HashMap 是由数组 + 链表的结构组成,这样的结果就是因为它们 hashCode 不相等,所以放在了数组的不同下标,当我们根据 Key 去查询的时候结果就为 null。

public static void main(String[] args) {
 Student student1 = new Student("小明", "male");
 Student student2 = new Student("小明", "male");
 
 HashMap<Student, String> hashMap = new HashMap<>();
 hashMap.put(student1, "小明");
 String value = hashMap.get(student2);
 System.out.println(value); 
}
复制代码

输出结果

null
复制代码

得到的结果我们肯定不满意,这里的 student1 和 student2 虽然内存地址不同,但是它们的逻辑内容相同,我们认为它们应该是相同的。

这里如果不好理解,猿友可以将 Student 类换成 String 类思考下,String 类是我们常常作为 HashMap 的 Key 值使用的,试想如果 String 类只重写了 equals 方法而没有重写 HashCode 方法,这里将某个字符串 new String("s") 作为 Key 然后 put 一个值,但是再根据 new String("s") 去 Get 的时候却得到 null 的结果,这是难以让人接受的。

所以无论是理论的约定上还是实际编程中,我们重写 equals 方法的同时总要重写 hashCode 方法,请记住这点

虽然 hashCode 方法被重写了,但是如果我们想要获取原始的 Object 类中的哈希码,我们可以通过 System.identityHashCode(Object a)来获取,该方法返回默认的 Object 的 hashCode 方法值,即使对象的 hashCode 方法被重写了也不影响。

public static native int identityHashCode(Object x);
复制代码

总结

如果 HashCode 不是内存地址,那么 Java 中怎么获取内存地址呢?找了一圈发现没有直接可用的方法。

后来想想也许这是 Java 语言编写者认为没有直接获取内存地址的必要吧,因为 Java 是一门高级语言相对于机器语言的汇编或者 C 语言来说更抽象并隐藏了复杂性,因为毕竟是在 C 和 C++ 的基础上进一步封装的。而且由于自动垃圾回收机制和对象年龄代的问题,Java 中对象的地址是会变化的,因此获取实际内存地址的意义不大。

当然以上是博主本人自己的观点,如果猿友有其他不同的意见或见解也可以留言,大家一起共同探讨。



Tags:Java HashCode   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
前言最近在学习 Go 语言,Go 语言中有指针对象,一个指针变量指向了一个值的内存地址。学习过 C 语言的猿友应该都知道指针的概念。Go 语言语法与 C 相近,可以说是类 C 的编程语...【详细内容】
2019-08-12  Tags: Java HashCode  点击:(214)  评论:(0)  加入收藏
这是一篇关于hashCode方法,可变对象和内存泄漏问题的文章。1. 重写 hashCode() 和 equals() 的契约每个 java 对象都有两个非常重要的方法,比如 hashCode() 和 equals() 方...【详细内容】
2019-08-09  Tags: Java HashCode  点击:(214)  评论:(0)  加入收藏
▌简易百科推荐
面向对象的特征之一封装 面向对象的特征之二继承 方法重写(override/overWrite) 方法的重载(overload)和重写(override)的区别: 面向对象特征之三:多态 Instanceof关键字...【详细内容】
2021-12-28  顶顶架构师    Tags:面向对象   点击:(2)  评论:(0)  加入收藏
一、Redis使用过程中一些小的注意点1、不要把Redis当成数据库来使用二、Arrays.asList常见失误需求:把数组转成list集合去处理。方法:Arrays.asList 或者 Java8的stream流式处...【详细内容】
2021-12-27  CF07    Tags:Java   点击:(3)  评论:(0)  加入收藏
文章目录 如何理解面向对象编程? JDK 和 JRE 有什么区别? 如何理解Java中封装,继承、多态特性? 如何理解Java中的字节码对象? 你是如何理解Java中的泛型的? 说说泛型应用...【详细内容】
2021-12-24  Java架构师之路    Tags:JAVA   点击:(5)  评论:(0)  加入收藏
大家好!我是老码农,一个喜欢技术、爱分享的同学,从今天开始和大家持续分享JVM调优方面的经验。JVM调优是个大话题,涉及的知识点很庞大 Java内存模型 垃圾回收机制 各种工具使用 ...【详细内容】
2021-12-23  小码匠和老码农    Tags:JVM调优   点击:(12)  评论:(0)  加入收藏
前言JDBC访问Postgresql的jsonb类型字段当然可以使用Postgresql jdbc驱动中提供的PGobject,但是这样在需要兼容多种数据库的系统开发中显得不那么通用,需要特殊处理。本文介绍...【详细内容】
2021-12-23  dingle    Tags:JDBC   点击:(13)  评论:(0)  加入收藏
Java与Lua相互调用案例比较少,因此项目使用需要做详细的性能测试,本内容只做粗略测试。目前已完成初版Lua-Java调用框架开发,后期有时间准备把框架进行抽象,并开源出来,感兴趣的...【详细内容】
2021-12-23  JAVA小白    Tags:Java   点击:(11)  评论:(0)  加入收藏
Java从版本5开始,在 java.util.concurrent.locks包内给我们提供了除了synchronized关键字以外的几个新的锁功能的实现,ReentrantLock就是其中的一个。但是这并不意味着我们可...【详细内容】
2021-12-17  小西学JAVA    Tags:JAVA并发   点击:(11)  评论:(0)  加入收藏
一、概述final是Java关键字中最常见之一,表示“最终的,不可更改”之意,在Java中也正是这个意思。有final修饰的内容,就会变得与众不同,它们会变成终极存在,其内容成为固定的存在。...【详细内容】
2021-12-15  唯一浩哥    Tags:Java基础   点击:(17)  评论:(0)  加入收藏
1、问题描述关于java中的日志管理logback,去年写过关于logback介绍的文章,这次项目中又优化了下,记录下,希望能帮到需要的朋友。2、解决方案这次其实是碰到了一个问题,一般的情况...【详细内容】
2021-12-15  软件老王    Tags:logback   点击:(19)  评论:(0)  加入收藏
本篇文章我们以AtomicInteger为例子,主要讲解下CAS(Compare And Swap)功能是如何在AtomicInteger中使用的,以及提供CAS功能的Unsafe对象。我们先从一个例子开始吧。假设现在我们...【详细内容】
2021-12-14  小西学JAVA    Tags:JAVA   点击:(22)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条