Java对象比较

对于Java的对象比较,聊一下个人的看法。

我们知道Java里面比较对象相等,有2种方式 ,== 或者 equals方法。

equals比较方式比较简单,在这儿不做过多说明,主要对==方式进行说明。


1、基本类型比较

Java的8大基本类型可以直接使用==进行比较,可以认为这儿的基本类型的比较是比较。 例如下面的代码块:

    int i = 1;
    int j = 2;
	System.out.println(i == j);

2、基本类型和包装类型的比较

基本类型和包装类型比较与基本类型之间的比较是类似的,包装类型会自动拆箱为基本类型,然后再进行对比,这儿唯一需要注意的是自动拆箱可能导致的空指针异常(NPE)。

 	Integer i = 1;
    int j = 2;
    //avoid npe
	System.out.println(i == j);

由于byteshortintlong对应的包装类型对于-127 ~ +128有缓存,所以在这范围之内的相应包装类型比较等同于相应基本类型的比较。 而超出范围外的值比较结果就不再正确。

	Integer i = 1000;
    Integer j = 1000;
    //为false
    System.out.println(i == j);

3、对象==比较

我们知道对象之间比较使用==是不对的,哪怕对象的equalshashCode方法结果都是相等的。例如下面的代码:

public class MyObject {
	public static void main(String[] args) {
		MyObject myObject = new MyObject();
        MyObject myObject1 = new MyObject();

        //false
        System.out.println(myObject == myObject1);

        //true
        System.out.println(myObject.equals(myObject1));
	}

	@Override
    public int hashCode() {
        return 1;
    }

    @Override
    public boolean equals(Object obj) {
        return true;
    }
}

这儿有种解释==方式比较的是对象在内存中的位置,比较的两个引用是否指向同一个内存地址,如果是则比较结果是相等的,否则就是false。当然这个说法是不正确的,==比较方式的比较结果其实取决于对象的identityHashCode是否相等,这个identityHashCode在没有覆盖hashCode方法的情况下,它的值就等于hashCode方法返回的值。如下代码所示:

	Object obj = new MyObject();
    //正常情况下hashCode与系统的identityHashCode是相等的
    System.out.println(obj.hashCode());
    System.out.println(System.identityHashCode(obj));

也就是说如果两个对象的identityHashCode相等,那么这两个对象使用==比较结果也是相等的,例如String字符串常量,关于String字符串常量比较不再举例,读者可以自行尝试。

4、 hashCode表示什么?

那么又回到上面另一个问题,hashcode是否就是表示对象在内存里面的地址呢?我们知道,Java虚拟机Jvm自带垃圾回收功能,又由于Java对象创建时会将对象优先分配到Eden区,后续经过垃圾回收移动到Survior区,最后经过一定的次数,对象会放置到老年代(Java内存相关知识不在这儿讲解,读者可自行搜索相关资料阅读)。在前面的步骤,会使对象在内存中不断进行移动。如果hashcode表示内存地址,那么按照此逻辑,在不同时段,同一对象的hashCode方法返回值是不想等的。那真的是这样吗?我们可以写一段代码证明一下:

	@Test
    public void testHashCode() {
        Object obj = new MyObject();
       
        //正常情况下hashCode与系统的identityHashCode是相等的,都表示类型的位置
        System.out.println(obj.hashCode());
        System.out.println(System.identityHashCode(obj));
        //睡30s,便于dump内存
        ThreadUtils.sleep(30, TimeUnit.SECONDS);

        //采用linkedList add的速度会快一点
        List<SoftReference<Object>> list = new LinkedList<>();

        //注册关闭钩子,输出一下对象的hashcode
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(obj.hashCode());
            System.out.println(System.identityHashCode(obj));
            System.out.println(list.size());
        }));


        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            list.add(new SoftReference<>(new Object()));

            if (i == 1000000) {
                System.out.println("循环内sleep一下dump内存");
                ThreadUtils.sleep(100, TimeUnit.SECONDS);
            }
        }

    }

执行方法前先指定JVM参数-ea -Xmx100M -XX:+PrintGCDetails
然后在程序睡眠期间使用指令jmap -dump:format=b,file=test4.bin 81265 将内存dump下来,这儿为了对比,需要在对象创建时dump一次,在经过多次垃圾回收后再dump一次。然后使用jhat test4.bin进行内存分析(这儿就不放图了)

得到结果如下:
运行过程中对象obj的hashcode : 1674896058
运行过程中对象obj的identityHashCode : 1674896058
第一次dump对象obj的位置: 0x7bd595c10
第二次dump对象obj的位置: 0x7beac59c8

可以看到hashcode的值与两个内存地址都不一致,那么基本可以说明对象的hashcode并不表示内存地址。 同时Java对hashCode方法也有声明: 同一个Java应用在运行期间,多次调用同一个对象的hashCode方法,返回值必须是同一个整数。

既然hashcode不表示内存地址,那么hashcode到底表示什么呢? Object类的hashCode方法是一个本地方法。下载好OpenJDK的源码后,使用JNI规范在源码中搜索java_lang_Object.h头文件,得到如下代码块:

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_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

紧接着继续搜索FastHashCode,得到:

intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
  //省略其他代码
  // 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);
    //将hash值进行缓存
    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) {
      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(篇幅原因,省略了注释):

static inline intptr_t get_next_hash(Thread * Self, oop obj) {
  intptr_t value = 0 ;
  if (hashCode == 0) {
     value = os::random() ;
  } else
  if (hashCode == 1) {
     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 {
     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;
}

可以看到hash计算因子取决于线程参数的几个值_hashStateX_hashStateY_hashStateZ_hashStateW

  _hashStateX = os::random() ;
  _hashStateY = 842502087 ;
  _hashStateZ = 0x8767 ;    // (int)(3579807591LL & 0xffff) ;
  _hashStateW = 273326509 ;

所以,对象的hashcode值跟内存地址关系不大。



写在最后


  • Java规范关于hashCodeequals方法的规范是很严格的,具体需要满足哪些条件可以网上搜索相关资料阅读。
  • 本文基于JDK8, OpenJDK源码基于JDK8
  • 本文为了偷懒,对于代码的输出结果和jhat分析结果并没有截图,感兴趣的读者可以根据步骤尝试。
  • 本文仅代表个人观点,如果您有什么看法或有不同意见可以在下方评论指出。
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值