最近在学习 Go 语言,Go 语言中有指针对象,一个指针变量指向了一个值的内存地址。学习过 C 语言的猿友应该都知道指针的概念。Go 语言语法与 C 相近,能够说是类 C 的编程语言,因此 Go 语言中有指针也是很正常的。咱们能够经过将取地址符&
放在一个变量前使用就会获得相应变量的内存地址。java
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 其值就是对象的内存地址。app
可是 HashCode 方法真的是内存地址吗? 回答这个问题前咱们先回顾下一些基础知识。dom
在 Java 中比较两个对象是否相等主要是经过 ==
号,比较的是他们在内存中的存放地址。Object 类是 Java 中的超类,是全部类默认继承的,若是一个类没有重写 Object 的 equals
方法,那么经过equals
方法也能够判断两个对象是否相同,由于它内部就是经过==
来实现的。jvm
//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。ide
这里的相同,是说比较的两个对象是不是同一个对象,即在内存中的地址是否相等。而咱们有时候须要比较两个对象的内容是否相同,即类具备本身特有的“逻辑相等”概念,而不是想了解它们是否指向同一个对象。微服务
例如比较以下两个字符串是否相同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)的发生。
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 都是 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 中对象的地址是会变化的,所以获取实际内存地址的意义不大。
固然以上是博主本人本身的观点,若是猿友有其余不一样的意见或看法也能够留言,你们一块儿共同探讨。
我的公众号:小菜亦牛
欢迎长按下图关注公众号:小菜亦牛!
按期为你奉上分布式,微服务等一线互联网公司相关技术的讲解和分析。