对于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);
由于byte
、short
、int
、long
对应的包装类型对于-127 ~ +128
有缓存,所以在这范围之内的相应包装类型比较等同于相应基本类型的比较。 而超出范围外的值比较结果就不再正确。
Integer i = 1000;
Integer j = 1000;
//为false
System.out.println(i == j);
3、对象==比较
我们知道对象之间比较使用==
是不对的,哪怕对象的equals
和hashCode
方法结果都是相等的。例如下面的代码:
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规范关于
hashCode
和equals
方法的规范是很严格的,具体需要满足哪些条件可以网上搜索相关资料阅读。 - 本文基于JDK8, OpenJDK源码基于JDK8
- 本文为了偷懒,对于代码的输出结果和jhat分析结果并没有截图,感兴趣的读者可以根据步骤尝试。
- 本文仅代表个人观点,如果您有什么看法或有不同意见可以在下方评论指出。