搬运源码过来,并将其上注释翻译,我们就能很好的理解这个类了。
public class Object {
// 注册本地方法,即在虚拟机中对本地方法做链接,是为了类中的本地方法可以被调用
// jdk后面的版本好像没有这段代码了,可能是不需要手动写出来了吧
private static native void registerNatives();
static {
registerNatives();
}
// 获得类信息
public final native Class<?> getClass();
// hashcode方法,必要时重写
public native int hashCode();
// equals方法,默认是判断两个对象是不是物理上就是一个对象,必要时重写
public boolean equals(Object obj) {
return (this == obj);
}
// 克隆方法,默认是浅拷贝,深拷贝需要自己实现
protected native Object clone() throws CloneNotSupportedException;
// toString方法,默认是 “类名@hashcode”
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
//下面是一对用于同步的方法 wait - notify。放到下面单独讲。
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
public final void wait() throws InterruptedException {
wait(0);
}
// 提供了一种回收对象的方法,jdk11后已被废弃
protected void finalize() throws Throwable { }
}
接下来分析一些比较有意思的方法。
wait、notify都是和对象锁相关的方法,只有持有对象锁的线程才能调用,否则将抛异常。
wait()
:将线程放入对象锁的等待链表中,挂起线程,最后释放锁。wait有带超时时间的版本。当发生以下几种情况时,该线程T会被唤醒,并去争抢锁:
notify()
:唤醒在此对象锁上等待的某个线程。有多个线程在等待时,唤醒可能是随机的(由底层实现决定)。唤醒后并不意味着获得锁,线程仍然要公平的去进行锁的争抢。notifyAll()唤醒所有在等待的线程。
*更详细的内容参看《Java并发》
返回对象的哈希码值,支持此方法是为了方便使用哈希表。我们可以看到,这是一个本地方法。
如果我们没有重写hashcode
,那么默认的hash值是怎么计算出来的呢?
不同的JDK生成算法不同,OpenJDK8提供了6种生成hashcode的方式:
0. A randomly generated number. 随机数
1. A function of memory address of the object. 一个和对象内存地址相关的函数
2. A hardcoded 1 (used for sensitivity testing.) 硬编码,用于测试的
3. A sequence. 一个序列
4. The memory address of the object, cast to int. 对象内存地址转成int
5. Thread state combined with xorshift (https://en.wikipedia.org/wiki/Xorshift) 线程状态结合异或移位算法
Openjdk源码中获取hashcode方法
上有一段注释,表明最后一种算法是更好的。
// 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.
对象可能需要存到 hash 表中时。
来自Effective Java
选择一个非零的常数值作为result
的初始值,通常选择17,因为17是一个质数
对于对象中的关键属性(即参与equal
计算的属性),计算一个值c
,然后将result*31 + c
再赋值给 result
:
如果这个属性是基本类型的,可以使用基本类型的包装类型的hashcode
方法获得c
*不管什么类型最后都返回的是一个int
值,布尔类型是0或者1,long
型则是和hashmap
中的hash
方法一样,右移32位后异或,浮点型也会做一些处理得到一个int
值
如果是一个对象引用,那么可以递归的调用它的hashCode
,如果引用的是null
,则c=0
如果是数组,则要把每个元素单独拿出来处理
最后返回result
@Override
public int hashCode() {
int result = 17;
result = 31 * result + Integer.valueOf(mint).hashCode();
//...
result = 31 * result + (mObj == null ? 0 : mObj.hashCode());
return result;
}
jvm将hashcode
缓存在对象头中,尽量减少调用次数。我们看看对象头中的Markword。
MarkWord存放哈希值、gc的分代年龄、偏向锁标记位、锁的量级标记位
|-------------------------------------------------------|
| Mark Word (32 bits) |
|-------------------------------------------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |
|-------------------------------------------------------|
我们需要知道我们可以重写类的hashcode方法,因此hashcode分为两种,一种是没有重写的,这是直接调用native方法得到的,一种是用户重写的。这里只会缓存native的hashcode,而不会缓存用户的hashcode。并且在对象刚创建时,这里是空的,在第一次调用native hashcode方法时才会缓存到这里。所以一旦类的hashcode被重写了,这里将一直为空。
这部分是被对象锁复用的,加锁的时候会发生改变,可以百度下也可以参看我的<Java并发>。有一个细节可以说说,如果开启了偏向锁,那么对象头中存放hashcode
的位置会被偏向锁持有线程id所占据。即没有了hashcode缓存,而此时一旦调用了原生的hashcode方法就会使得重新在对象头写入hash值。这个操作会导致偏向锁的膨胀。
重写equals()
必须同时重写hashCode()
自反性:对于任何非空引用x,x.equals(x)
必须返回true。
对称性:对于任何非空引用x和y,如果且仅当y.equals(x)
返回true时x.equals(y)
必须返回true。
传递性:对于任何非空引用x、y、z,如果 x.equals(y)
返回true,y.equals(z)
返回true,则x.equals(z)
必须返回true。
一致性:对于任何非空引用x和y,如果在equals比较中使用的信息没有修改,则x.equals(y)
的多次调用必须始终返回true或始终返回false。
对于任何非空引用x,x.equals(null)
必须返回false。
@Override
public boolean equals(Object obj) {
if(this == obj){ //地址相等
return true;
}
if(obj == null){ // 传值为null应该直接返回false
return false;
}
if (!(obj instanceof User)){ // 必要时做一个类型判断
return false;
}
User other = (User) obj; // 转换类型
// 对关键属性进行对比
return this.name == other.name && this.number == other.number && this.father.equals(other.father);
}
clone默认是浅克隆,对于对象的属性,我们可以通过序列化/反序列化进行深克隆。通常要用这个方法,我们需要实现Cloneable接口,然后重写clone方法。Java的clone被认为是一个很烂的设计。
研究的时候我发现一个我关于protected的理解误区:
clone方法是被protected修饰的,有意思的是,我们自定义的类无法调用clone方法。这似乎有点奇怪,我一开始认为protected不也是被子类继承了吗,为什么无法调用呢?
public class A{
void a(){
this.clone(); // #1 可以调用
}
}
public class Main {
public static void main(String[] args) {
A a = new A();
a.clone(); // #2 不能调用
}
}
我们来看看protected的定义:protected 的属性和方法可以在本包和子类访问
我们来看上面的#1,A是Object的子类,所以在A类里可以直接调用Object的clone方法。而#2处,虽然Main也是继承于Object,但是不是在Main自己属性方法中调用clone,因此不能调用。
我们来看个更一般的例子。
public class M{
protected void m(){
}
}
public class A extends M{
void a(){
this.m(); // 这是可以调用的
}
}
public class A{
void a(){
M m = new M();
m.m(); // 这要看A和M是不是在一个包下,在一个包下才可以调用
}
}
因此我们在实现Cloneable接口重写clone方法时,还要思考清楚需要用什么修饰符。
原文:https://www.cnblogs.com/cpcpp/p/15212385.html