千锋扣丁学堂Java培训之深入理解HashCode方法

2019-08-12 13:55:44 4462浏览

今天千锋扣丁学堂Java培训老师给大家分享一篇关于Java中HashCode方法的相关资料,文中通过示例代码介绍的非常详细,下面我们一起来看一下吧。



1、0前言

在学习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方法真的是内存地址吗?**回答这个问题前我们先回顾下一些基础知识。

2|0==和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。

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

例如比较如下两个字符串是否相同Stringa="Hello"和Stringb=newString("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)的发生。

3|0Object中的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方法为不同的对象返回不同的整形值。具有迷惑异议的地方就是Thisistypicallyimplementedbyconvertingtheinternaladdressoftheobjectintoaninteger这一句,意为通常情况下实现的方式是将对象的内部地址转换为整形值。

如果你不深究就会认为它返回的就是对象的内存地址,我们可以继续看看它的实现,但是因为这里是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也许和内存地址有关系,但不是直接代表内存地址的,具体需要看虚拟机版本和设置。

4|0equals和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方法必须产生不同的结果。但是给不相等的对象产生不同的整数散列值,是有可能提高散列表(hashtable)的性能。

从理论上来说如果重写了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方法,这里将某个字符串newString("s")作为Key然后put一个值,但是再根据newString("s")去Get的时候却得到null的结果,这是难以让人接受的。

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

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

public static native int identityHashCode(Object x);

5|0总结

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

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

以上就是关于千锋扣丁学堂Java培训之深入理解HashCode方法的全部内容了,想要学好Java开发小编给大家推荐口碑良好的扣丁学堂,扣丁学堂有专业老师制定的Java学习路线图辅助学员学习,此外还有与时俱进的Java课程体系和Java视频教程供大家学习,想要学好Java开发技术的小伙伴快快行动吧。扣丁学堂Java技术交流群:850353792。


                          JavaEE/微服务/源码解析/分布式/企业级架构【VIP体验课】


     【关注微信公众号获取更多学习资料】        【扫码进入JavaEE/微服务VIP免费公开课】  



查看更多关于“Java开发资讯”的相关文章>>

标签: Java培训 Java视频教程 Java多线程 Java面试题 Java学习视频 springBoot项目
微信
微博
15311698296

全国免费咨询热线

邮箱:codingke@1000phone.com

官方群:148715490

北京千锋互联科技有限公司版权所有   北京市海淀区宝盛北里西区28号天丰利商场4层
京ICP备12003911号-6   Copyright © 2013 - 2019
返回顶部 返回顶部