对Java中HashCode方法的深入思考
前言
最近在学习 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 变量的地址是: %x\n", &a )
/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )
}
因为本人主要开发语言是 Java,所以我就联想到 Java 中没有指针,那么 Java 中如何获取变量的内存地址呢?
如果能获取变量的内存地址那么就可以清晰的知道两个对象是否是同一个对象,如果两个对象的内存地址相等那么无疑是同一个对象反之则是不同的对象。
很多人说对象的 HashCode 方法返回的就是对象的内存地址,包括我在《Java核心编程·卷I》的第5章内容中也发现说是 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 中对象的地址是会变化的,因此获取实际内存地址的意义不大。
当然以上是博主本人自己的观点,如果猿友有其他不同的意见或见解也可以留言,大家一起共同探讨。
对Java中HashCode方法的深入思考的更多相关文章
- Java中hashCode()方法以及HashMap()中hash()方法
Java的Object类中有一个hashCode()方法: public final native Class<?> getClass(); public native int hashC ...
- java中hashCode方法与equals方法的用法总结
首先,想要明白hashCode的作用,必须要先知道Java中的集合. 总的来说,Java中的集合(Collection)有两类,一类是List,再有一类是Set. 前者集合内的元素是有序的,元素可以重 ...
- java中hashCode()方法的作用
hashcode方法返回该对象的哈希码值. hashCode()方法可以用来来提高Map里面的搜索效率的,Map会根据不同的hashCode()来放在不同的位置,Map在搜索一个对象的时候先 ...
- Java中hashCode方法的理解以及此小结的总结练习(代码)
笔记: “散列码”就是用来把一堆对象散到各自的队列里去的一种标识码. 举个形象一点的例子,一年有 365 天,从 1 编号到 365,下面我定义一种编码方法,每个人按照他生日那天的编号作为他的标识码, ...
- Java中hashCode的作用
转 http://blog.csdn.net/fenglibing/article/details/8905007 Java中hashCode的作用 2013-05-09 13:54 64351人阅 ...
- java 中hashcode和equals 总结
一.概述 在Java中hashCode的实现总是伴随着equals,他们是紧密配合的,你要是自己设计了其中一个,就要设计另外一个.当然在多数情况下,这两个方法是不用我们考虑的,直 ...
- Java中hashcode的理解
Java中hashcode的理解 原文链接http://blog.csdn.net/chinayuan/article/details/3345559 怎样理解hashCode的作用: 以 java. ...
- Java中的方法应用
一.如何定义java中的方法 所谓方法,就是用来解决一类问题的代码的有序组合,是一个功能模块. 语法: 1. 访问修饰符:方法允许被访问的权限范围, 可以是 public.protected.priv ...
- c#和java中的方法覆盖——virtual、override、new
多态和覆盖 多态是面向对象编程中最为重要的概念之一,而覆盖又是体现多态最重要的方面.对于像c#和java这样的面向对象编程的语言来说,实现了在编译时只检查接口是否具备,而不需关心最终的实现,即最终的实 ...
随机推荐
- Python爬虫入门:爬取豆瓣电影TOP250
一个很简单的爬虫. 从这里学习的,解释的挺好的:https://xlzd.me/2015/12/16/python-crawler-03 分享写这个代码用到了的学习的链接: BeautifulSoup ...
- HDU 2795:Billboard(线段树)
http://acm.hdu.edu.cn/showproblem.php?pid=2795 Billboard Problem Description At the entrance to th ...
- python 中的__name__ == "__main__"(转)
有句话经典的概括了这段代码的意义: “Make a script both importable and executable” 意思就是说让你写的脚本模块既可以导入到别的模块中用,另外该模块自己也可 ...
- abview查找范例时说 NI服务器未定位 这是怎么回事?
NI服务定位器未运行在使用labview查找范例时,出现“NI服务定位器未运行”的对话框,而后不能够打开范例.有以下方法.方法一:是防火墙服务禁用造成的,在防火墙规则里面找,把和NI相关的都允许. 方 ...
- 《深入浅出MFC》下载
百度云及其他网盘下载地址:点我 编辑推荐 <深入浅出MFC>内含光盘一片,书中所有原始码与可执行文件尽在其中. 作者简介 侯俊杰,先生不知何许人也,闲静少言,不慕荣利.好读书,求甚解:每有 ...
- 求1-2/3+3/5-4/7+......49/97和(C语言实现)
一.功能需求 求1 - 2/3 + 3/5 - 4/7 + ......49/97的和 C语言等级考试中也有涉及到类似的需求. 二.代码分析 仔细查看功能需求,可以发现这个等式的三个规律: 1.从每一 ...
- MyBatis 存储过程
From<MyBatis从入门到精通> <!-- 6.2 存储过程 6.2.1 第一个存储过程 delimiter ;; create procedure 'select_user_ ...
- Mac上pycharm集成pyspark
前提: 1.已经安装好spark.我的是spark2.2.0. 2.已经有python环境,我这边使用的是python3.6. 一.安装py4j 使用pip,运行如下命令: pip install p ...
- C# 与 JS 之间传值在 cshtml页面中
@{ string It = "sss"; ; } @functions{ string Mod = "ajssaioi"; public string Itm ...
- Excel催化剂开源第11波-动态数组函数技术开源及要点讲述
在Excel催化剂中,大量的自定义函数使用了动态数组函数效果,虽然不是原生的Excel365版效果(听说Excel2019版取消了支持动态数组函数,还没求证到位,Excel365是可以用,但也仅限于部 ...