Java的Object.hashCode()的返回值到底是不是对象内存地址?
2015-05-03 13:25
537 查看
刚学Java的时候我也有过这种怀疑,但一直没有验证;最近在OSCHINA上看到有人在回答问题时也这么说,于是萌生了一探究竟的想法——
(顺带回顾一下JNI)
上面这段注释指出了
(PS:所谓契约当然是大家一致达成的,各个JVM厂商都会遵循)
一致性(consistent),在程序的一次执行过程中,对同一个对象必须一致地返回同一个整数。
如果两个对象通过
如果两个对象通过
实际上
而这些运行时库一般都是跟JDK/JRE一起发布的;所以,对于不同的JRE环境,问题的答案未必相同。
考虑到具体JVM厂商实现的
最后我们再找一些开源JRE的
要验证这个问题自然需要一种得到对象内存地址的方法,但Java本身并没有提供类似的方法;这也是我在初学Java时没有验证这个问题的原因。
“内存地址”在Java里得不到,但在C/C++中却很容易得到。于是,我们想到——通过JNI让Java代码调用一段C/C++代码来得到对象内存地址。
这里可能需要考虑的还有一点——用Java什么类型能放得下C/C++的指针?
在64位机器上,C/C++的指针是8字节;32位是4字节。
嗯(⊙_⊙)~ 不管怎样Java的long都是8字节,足矣~
并且已经实现了getNativePointer方法。
那么验证开头提出的问题就变得异常简单了:
用
该命令执行后生成了
接着实现这个
编译为动态库:
可能因为找不到jni.h,报错:
在JDK安装目录下查找jni.h:
知道jni.h路径后,用-I选项加到编译命令上再次编译:
OK, 编译成功,生成了 libnative-utils.so 文件。
首先,编译
分别生成了NativeUtils.class和TestNativeUtils.class
好了,就差临门一脚了——执行class文件:
居然出错了:
必须在调用
又有错误,但已经和刚才不一样了:
这次错误是:没有在
(PS:估计
第一次执行时,
而它是一个native的方法,对应代码(java_lang_System.cc):
很显然,这里的关键在于
这段代码可以看出——ART的
真正计算hashcode的是
从
TODO: OpenJDK上的hashCode具体实现和简要分析
Object.c
这段代码指出了
这里只是一个包装,实际计算hashCode的是
又是假牙,实际计算hashCode的是
这段代码可以看出OpenJDK一共实现了5中不同的计算hash值的方法,通过
这段代码中
我的运行环境:
OS:
Ubuntu 12.04 64bit Desktop |
JDK:
java version “1.7.0_55”
OpenJDK Runtime Environment (IcedTea 2.4.7) (7u55-2.4.7-1ubuntu1~0.12.04.2) \
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)
欢迎评论或email(xusiwei1236@163.com)交流观点,转载注明出处,勿做商用。
java.lang.Object.hashCode()的返回值到底是不是对象内存地址?
(顺带回顾一下JNI)
hashCode契约
说到这个问题,大家的第一反应一定和我一样——去查Object.hashCode的源码,但翻开源码,看到的却是这样的(Oracle JDK 8):
/** * 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}. * <p> * The general contract of {@code hashCode} is: * <ul> * <li>Whenever it is invoked on the same object more than once during * an execution of a Java application, the {@code hashCode} method * must consistently return the same integer, provided no information * used in {@code equals} comparisons on the object is modified. * This integer need not remain consistent from one execution of an * application to another execution of the same application. * <li>If two objects are equal according to the {@code equals(Object)} * method, then calling the {@code hashCode} method on each of * the two objects must produce the same integer result. * <li>It is <em>not</em> required that if two objects are unequal * according to the {@link java.lang.Object#equals(java.lang.Object)} * method, then calling the {@code hashCode} method on each of the * two objects must produce distinct integer results. However, the * programmer should be aware that producing distinct integer results * for unequal objects may improve the performance of hash tables. * </ul> * <p> * 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();
Object.hashCode是一个native方法,看不到源码(Java代码,Oracle的JDK是看不到的,OpenJDK或其他开源JRE是可以找到对应的C/C++代码)。
上面这段注释指出了
Object.hashCode()在JRE(Java Runtime Library)中应该遵循的一些契约(contract):
(PS:所谓契约当然是大家一致达成的,各个JVM厂商都会遵循)
一致性(consistent),在程序的一次执行过程中,对同一个对象必须一致地返回同一个整数。
如果两个对象通过
equals(Object)比较,结果相等,那么对这两个对象分别调用
hashCode方法应该产生相同的整数结果。(PS:这里
equals和
hashCode说的都是
Object类的)
如果两个对象通过
java.lang.Object.equals(java.lang.Ojbect)比较,结果不相等,不必保证对这两个对象分别调用
hashCode也返回两个不相同的整数。
实际上
java.lang包里面的类,都是JRE必须的,属于运行时库(Runtime Library),这也是为什么很多JRE下该类的class文件被打包到rt.jar中的原因(应该是Runtime的简写)。
而这些运行时库一般都是跟JDK/JRE一起发布的;所以,对于不同的JRE环境,问题的答案未必相同。
考虑到具体JVM厂商实现的
Object.hashCode相对较复杂,下面先通过另一思路对开头提出的问题进行探究。
最后我们再找一些开源JRE的
Object.hashCode的具体实现作简要分析。
Java中如何获得对象内存地址?
看不到Object.hashCode的源码,反过来,我们可以得到对象的内存地址和
Object.hashCode比较,也能得出结论。
要验证这个问题自然需要一种得到对象内存地址的方法,但Java本身并没有提供类似的方法;这也是我在初学Java时没有验证这个问题的原因。
“内存地址”在Java里得不到,但在C/C++中却很容易得到。于是,我们想到——通过JNI让Java代码调用一段C/C++代码来得到对象内存地址。
这里可能需要考虑的还有一点——用Java什么类型能放得下C/C++的指针?
在64位机器上,C/C++的指针是8字节;32位是4字节。
嗯(⊙_⊙)~ 不管怎样Java的long都是8字节,足矣~
Think in Java——接口和测试
假设我们已经有了一个NativeUtils.java:class NativeUtils { public native static long getNativePointer(Object o); }
并且已经实现了getNativePointer方法。
那么验证开头提出的问题就变得异常简单了:
class TestNativeUtils { public static void main(String args[]) { Object o = new Object(); long nptr = NativeUtils.getNativePointer(o); long hash = o.hashCode(); System.out.println(String.format("hash: %x, nptr: %x", hash, nptr)); } }
Think in C++——实现native方法
好了说干就干,现在就差那个native的getNativePointer了。
用
javah生成对应的
.h文件:
$ javah NativeUtils
该命令执行后生成了
NativeUtils.h:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class NativeUtils */ #ifndef _Included_NativeUtils #define _Included_NativeUtils #ifdef __cplusplus extern "C" { #endif /* * Class: NativeUtils * Method: getNativePointer * Signature: (Ljava/lang/Object;)J */ JNIEXPORT jlong JNICALL Java_NativeUtils_getNativePointer (JNIEnv *, jclass, jobject); #ifdef __cplusplus } #endif #endif
接着实现这个
Java_NativeUtils_getNativePointer,文件命名为NativeUtils.cc:
#include "NativeUtils.h" JNIEXPORT jlong JNICALL Java_NativeUtils_getNativePointer(JNIEnv *env, jclass clazz, jobject o) { return reinterpret_cast<jlong>(o); }
编译为动态库:
$ g++ -shared -o libnative-utils.so NativeUtils.cc
可能因为找不到jni.h,报错:
NativeUtils.h:2:17: fatal error: jni.h: No such file or directory
在JDK安装目录下查找jni.h:
user@host:/usr/lib/jvm/java-7-openjdk-amd64$ find . -name jni.h ./include/jni.h
知道jni.h路径后,用-I选项加到编译命令上再次编译:
$ g++ -shared -I/usr/lib/jvm/java-7-openjdk-amd64/include/ -o libnative-utils.so NativeUtils.cc
OK, 编译成功,生成了 libnative-utils.so 文件。
Run in shell——在shell环境运行
下面就让TestNativeUtils在shell环境执行起来。
首先,编译
NativeUtils和
TestNativeUtils:
$ javac NativeUtils.java
$ javac TestNativeUtils.java
分别生成了NativeUtils.class和TestNativeUtils.class
好了,就差临门一脚了——执行class文件:
$ java TestNativeUtils
居然出错了:
Exception in thread "main" java.lang.UnsatisfiedLinkError: NativeUtils.getNativePointer(Ljava/lang/Object;)J at NativeUtils.getNativePointer(Native Method) at TestNativeUtils.main(TestNativeUtils.java:5)
加载动态库到我们的程序中
到目前为止,我们的Java代码并没有实现NativeUtils.getNativePointer;所以,会有上面的错误。
必须在调用
NativeUtils.getNativePointer前,将我们编译好的动态库加载上。可以用static代码块,以保证在调用前完成加载;修改后的
NativeUtils:
class NativeUtils { static { System.loadLibrary("native-utils"); } public native static long getNativePointer(Object o); }
让JVM能找到动态库
再次编译、执行:$ javac NativeUtils.java
$ java TestNativeUtils
又有错误,但已经和刚才不一样了:
Exception in thread "main" java.lang.UnsatisfiedLinkError: no native-utils in java.library.path at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1886) at java.lang.Runtime.loadLibrary0(Runtime.java:849) at java.lang.System.loadLibrary(System.java:1088) at NativeUtils.<clinit>(NativeUtils.java:3) at TestNativeUtils.main(TestNativeUtils.java:5)
这次错误是:没有在
java.library.path中找到
native-utils,可以在javac命令-D参数上将当期目录加到
java.library.path上,让其能够找到
native-utils:
$ java -Djava.library.path=. TestNativeUtils
hash: 4f5f1ace, nptr: 7f223a5fb958
All in one —— Makefile
上面的多个命令可以写到一个Makefile里,可以实现“一键执行”:test: runtest all: libnative-utils.so JNI_INCLUDE=/usr/lib/jvm/java-7-openjdk-amd64/include NativeUtils.class: NativeUtils.java javac NativeUtils.java TestNativeUtils.class: TestNativeUtils.java javac TestNativeUtils.java NativeUtils.h: NativeUtils.java javah -jni NativeUtils libnative-utils.so: NativeUtils.cc NativeUtils.h g++ -shared -I${JNI_INCLUDE} -o libnative-utils.so NativeUtils.cc runtest: TestNativeUtils.class libnative-utils.so @echo "run test:" java -Djava.library.path=. TestNativeUtils clean: rm -v *.class *.so *.h
几个JRE的具体实现
说道开源的Java运行环境,首先能想到的自然是OpenJDK和Android,下面分别简要分析。hashCode on Android
Android的Object.hashCode和Oracle JDK的略有不同:
private transient int shadow$_monitor_; public int hashCode() { int lockWord = shadow$_monitor_; final int lockWordMask = 0xC0000000; // Top 2 bits. final int lockWordStateHash = 0x80000000; // Top 2 bits are value 2 (kStateHash). if ((lockWord & lockWordMask) == lockWordStateHash) { return lockWord & ~lockWordMask; // } return System.identityHashCode(this); }
(PS:估计
shadow$_monitor_应该是hash值的一个cache,第一次需要计算一下,以后都不用计算了)
第一次执行时,
shadow$_monitor_的值为0,将会调用
System.identityHashCode:
public static native int identityHashCode(Object anObject);
而它是一个native的方法,对应代码(java_lang_System.cc):
static jint System_identityHashCode(JNIEnv* env, jclass, jobject javaObject) { if (UNLIKELY(javaObject == nullptr)) { return 0; } ScopedFastNativeObjectAccess soa(env); mirror::Object* o = soa.Decode<mirror::Object*>(javaObject); return static_cast<jint>(o->IdentityHashCode()); }
很显然,这里的关键在于
mirror::Object::IdentityHashCode:
int32_t Object::IdentityHashCode() const { mirror::Object* current_this = const_cast<mirror::Object*>(this); while (true) { LockWord lw = current_this->GetLockWord(false); switch (lw.GetState()) { case LockWord::kUnlocked: { // kUnlocked 是 LockWord的默认State值 // Try to compare and swap in a new hash, if we succeed we will return the hash on the next // loop iteration. LockWord hash_word(LockWord::FromHashCode(GenerateIdentityHashCode())); DCHECK_EQ(hash_word.GetState(), LockWord::kHashCode); if (const_cast<Object*>(this)->CasLockWordWeakRelaxed(lw, hash_word)) { return hash_word.GetHashCode(); } break; } case LockWord::kThinLocked: { // Inflate the thin lock to a monitor and stick the hash code inside of the monitor. May // fail spuriously. Thread* self = Thread::Current(); StackHandleScope<1> hs(self); Handle<mirror::Object> h_this(hs.NewHandle(current_this)); Monitor::InflateThinLocked(self, h_this, lw, GenerateIdentityHashCode()); // A GC may have occurred when we switched to kBlocked. current_this = h_this.Get(); break; } case LockWord::kFatLocked: { // Already inflated, return the has stored in the monitor. Monitor* monitor = lw.FatLockMonitor(); DCHECK(monitor != nullptr); return monitor->GetHashCode(); } case LockWord::kHashCode: { // 以后调用 return lw.GetHashCode(); } default: { LOG(FATAL) << "Invalid state during hashcode " << lw.GetState(); break; } } } LOG(FATAL) << "Unreachable"; return 0; }
这段代码可以看出——ART的
Object.hashCode确实是有cache的,对于同一个
Ojbect,第一次调用
Object.hashCode将会执行实际的计算并记入cache,以后直接从cache中取出。
真正计算hashcode的是
GenerateIdentityHashCode:
int32_t Object::GenerateIdentityHashCode() { static AtomicInteger seed(987654321 + std::time(nullptr)); int32_t expected_value, new_value; do { expected_value = static_cast<uint32_t>(seed.LoadRelaxed()); new_value = expected_value * 1103515245 + 12345; } while ((expected_value & LockWord::kHashMask) == 0 || !seed.CompareExchangeWeakRelaxed(expected_value, new_value)); return expected_value & LockWord::kHashMask; }
从
GenerateIdentityHashCode可以看出,ART的
Object.hashCode的返回值和对象的地址并没有直接的关系。
hashCode on OpenJDK
OpenJDK项目首页:openjdk.java.netTODO: OpenJDK上的hashCode具体实现和简要分析
Object.c
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}, }; JNIEXPORT void JNICALL Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls) { (*env)->RegisterNatives(env, cls, methods, sizeof(methods)/sizeof(methods[0])); } JNIEXPORT jclass JNICALL Java_java_lang_Object_getClass(JNIEnv *env, jobject this) { if (this == NULL) { JNU_ThrowNullPointerException(env, NULL); return 0; } else { return (*env)->GetObjectClass(env, this); } }
这段代码指出了
Object.hashCode对应的C函数为
JVM_IHashCode,下面需要找到
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) { // NOTE: many places throughout the JVM do not expect a safepoint // to be taken here, in particular most operations on perm gen // objects. However, we only ever bias Java instances and all of // the call sites of identity_hash that might revoke biases have // been checked to make sure they can handle a safepoint. The // added check of the bias pattern is to avoid useless calls to // thread-local storage. if (obj->mark()->has_bias_pattern()) { // Box and unbox the raw reference just in case we cause a STW safepoint. Handle hobj (Self, obj) ; // Relaxing assertion for bug 6320749. assert (Universe::verify_in_progress() || !SafepointSynchronize::is_at_safepoint(), "biases should not be seen by VM thread here"); BiasedLocking::revoke_and_rebias(hobj, false, JavaThread::current()); obj = hobj() ; assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); } } // hashCode() is a heap mutator ... // Relaxing assertion for bug 6320749. assert (Universe::verify_in_progress() || !SafepointSynchronize::is_at_safepoint(), "invariant") ; assert (Universe::verify_in_progress() || Self->is_Java_thread() , "invariant") ; assert (Universe::verify_in_progress() || ((JavaThread *)Self)->thread_state() != _thread_blocked, "invariant") ; ObjectMonitor* monitor = NULL; markOop temp, test; intptr_t hash; markOop mark = ReadStableMark (obj); // object should remain ineligible for biased locking assert (!mark->has_bias_pattern(), "invariant") ; if (mark->is_neutral()) { hash = mark->hash(); // this is a normal header if (hash) { // if it has hash, just return it return hash; } hash = get_next_hash(Self, obj); // allocate a new hash code temp = mark->copy_set_hash(hash); // merge the hash code into header // use (machine word version) atomic operation to install the hash test = (markOop) Atomic::cmpxchg_ptr(temp, obj->mark_addr(), mark); if (test == mark) { return hash; } // If atomic operation failed, we must inflate the header // into heavy weight monitor. We could add more code here // for fast path, but it does not worth the complexity. } else if (mark->has_monitor()) { monitor = mark->monitor(); temp = monitor->header(); assert (temp->is_neutral(), "invariant") ; hash = temp->hash(); if (hash) { return hash; } // Skip to the following code to reduce code size } else if (Self->is_lock_owned((address)mark->locker())) { temp = mark->displaced_mark_helper(); // this is a lightweight monitor owned assert (temp->is_neutral(), "invariant") ; hash = temp->hash(); // by current thread, check if the displaced if (hash) { // header contains hash code return hash; } // WARNING: // The displaced header is strictly immutable. // It can NOT be changed in ANY cases. So we have // to inflate the header into heavyweight monitor // even the current thread owns the lock. The reason // is the BasicLock (stack slot) will be asynchronously // read by other threads during the inflate() function. // Any change to stack may not propagate to other threads // correctly. } // 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,代码和
ObjectSynchronizer::FastHashCode相邻:
// hashCode() generation : // // Possibilities: // * MD5Digest of {obj,stwRandom} // * CRC32 of {obj,stwRandom} or any linear-feedback shift register function. // * A DES- or AES-style SBox[] mechanism // * One of the Phi-based schemes, such as: // 2654435761 = 2^32 * Phi (golden ratio) // HashCodeValue = ((uintptr_t(obj) >> 3) * 2654435761) ^ GVars.stwRandom ; // * A variation of Marsaglia's shift-xor RNG scheme. // * (obj ^ stwRandom) is appealing, but can result // in undesirable regularity in the hashCode values of adjacent objects // (objects allocated back-to-back, in particular). This could potentially // result in hashtable collisions and reduced hashtable efficiency. // There are simple ways to "diffuse" the middle address bits over the // generated hashCode values: // 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. // 地址基础上hack intptr_t addrBits = 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 = 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; }
这段代码可以看出OpenJDK一共实现了5中不同的计算hash值的方法,通过
这段代码中
hashCode进行切换。其中
hashCode == 4的是直接使用地址的(前面的实验说明OpenJDK默认情况下并没有使用这种方式,或许可以通过运行/编译时参数进行选择)。
结论
前面通过JNI验证已经能够得到很显然的结论,hashCode返回的并不一定是对象的(虚拟)内存地址,具体取决于运行时库和JVM的具体实现。我的运行环境:
OS:
Ubuntu 12.04 64bit Desktop |
JDK:
java version “1.7.0_55”
OpenJDK Runtime Environment (IcedTea 2.4.7) (7u55-2.4.7-1ubuntu1~0.12.04.2) \
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)
欢迎评论或email(xusiwei1236@163.com)交流观点,转载注明出处,勿做商用。
相关文章推荐
- Java的Object.hashCode()的返回值到底是不是对象内存地址?
- Java的Object.hashCode()的返回值到底是不是对象内存地址?
- Java 深入学习(7) —— Object.hashCode()的返回值与对象内存地址的关系
- Java中的对象类型的引用到底是不是指针
- Java中Object对象的hashCode方法和String对象的hashCode
- Java管理对象神奇之Object类的equals和hashcode
- Java语言中Object对象的hashCode()取值的底层算法是怎样实现的?
- Java中的对象类型的引用到底是不是指针
- 【java基础】java中Object对象中的Hashcode方法的作用
- 一个对象toString()方法如果没有被重写,那么默认调用它的父类Object的toString()方法,而Object的toString()方法是打印该对象的hashCode,一般hashCode就是此对象的内存地址
- [Java基础要义] Java语言中Object对象的hashCode()取值的底层算法是怎样实现的?
- 集合框架----Java管理对象神奇之Object类的equals和hashcode
- Java语言中Object对象的hashCode()取值的底层算法是怎样实现的?
- java语言中Object对象的hashCode()取值的底层算法是怎样实现的
- 重写Java Object对象的hashCode和equals方法实现集合元素按内容判重
- 集合框架----Java管理对象神奇之Object类的equals和hashcode
- Java语言中Object对象的hashCode()取值的底层算法是怎样实现的?,object hashcode
- JAVA Object对象(toString、equals、hashCode方法)、String类、StringBuffer、StringBuider、System、Runtime、Date、Mat
- 重写Java Object对象的hashCode和equals方法实现集合元素按内容判重
- java.lang.Object 对象中 hashCode 和 equals 方法详解及其延伸