聊聊Java的Object类

Object源码(JDK8)

搬运源码过来,并将其上注释翻译,我们就能很好的理解这个类了。

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、notify都是和对象锁相关的方法,只有持有对象锁的线程才能调用,否则将抛异常。

wait():将线程放入对象锁的等待链表中,挂起线程,最后释放锁。wait有带超时时间的版本。当发生以下几种情况时,该线程T会被唤醒,并去争抢锁:

  • 某个其他线程为此对象调用了notify方法,而线程T恰好被任意选择为要唤醒的线程。
  • 其他一些线程为此对象调用notifyAll方法。
  • 其他一些线程中断线程T 。(即使被中断也会要去抢锁才能继续执行)
  • 已经过了指定的超时时间。

notify():唤醒在此对象锁上等待的某个线程。有多个线程在等待时,唤醒可能是随机的(由底层实现决定)。唤醒后并不意味着获得锁,线程仍然要公平的去进行锁的争抢。notifyAll()唤醒所有在等待的线程。

*更详细的内容参看《Java并发》

hashCode()

返回对象的哈希码值,支持此方法是为了方便使用哈希表。我们可以看到,这是一个本地方法。

底层原理

如果我们没有重写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 表中时。

重写该方法的规约?

  • 每当在 Java 应用程序同一次执行期间在同一对象上多次调用它时, hashCode方法必须始终返回相同的整数,前提是对象上的equals比较中使用的信息没有被修改。 该整数不需要从应用程序的一次执行到同一应用程序的另一次执行保持一致。
  • 如果根据equals(Object)方法两个对象相等,则对两个对象中的每一个调用hashCode方法必须产生相同的整数结果。
  • 如果两个对象根据不相等equals(Object)方法,然后调用hashCode在各两个对象的方法可能产生相同的结果,因为哈希冲突。 但是,程序员应该使得尽量产生不同的结果,以提高哈希表的性能。

通常的范式

来自Effective Java

  1. 选择一个非零的常数值作为result的初始值,通常选择17,因为17是一个质数

  2. 对于对象中的关键属性(即参与equal计算的属性),计算一个值c,然后将result*31 + c 再赋值给 result

    1. 如果这个属性是基本类型的,可以使用基本类型的包装类型的hashcode方法获得c

      *不管什么类型最后都返回的是一个int值,布尔类型是0或者1,long型则是和hashmap中的hash方法一样,右移32位后异或,浮点型也会做一些处理得到一个int

    2. 如果是一个对象引用,那么可以递归的调用它的hashCode,如果引用的是null,则c=0

    3. 如果是数组,则要把每个元素单独拿出来处理

  3. 最后返回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;
}

hashcode的缓存

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()

重写规约

  • 重写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()

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