嚼一嚼Object类中的方法

嚼一嚼Object类中的方法

Java 中的所有类都有一个共同的祖先,那就是 Object 类,现在我们就来看看这个类中有哪些方法。

1、Object 类中方法源码(JDK8)

  • private static native void registerNatives();

  • public final native Class<?> getClass();

  • public native int hashCode();

  • public boolean equals(Object obj) { return (this == obj);}

  • protected native Object clone() throws CloneNotSupportedException;

  •     public String toString() {
            return getClass().getName() + "@" + Integer.toHexString(hashCode());
        }
    
  • 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);
        }
    
  • protected void finalize() throws Throwable { }

2、registerNatives 方法

2.1、本地方法

在很多类中,都会像 Object 类一样,存在如下代码:

    private static native void registerNatives();
    static {
        registerNatives();
    }

native 修饰的方法是本地方法,那么我们先弄清楚何为本地方法。

Java有两种方法:Java方法和本地方法。Java方法是由Java语言编写,编译成字节码,存储在class文件中。本地方法是由其他语言(比如C,C++,或者汇编)编写的,编译成和处理器相关的机器代码。本地方法保存在动态连接库中,格式是各个平台专有的。Java方法是平台无关的,但本地方法却不是。 运行中的Java程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。本地方法是联系 Java 程序和底层主机操作系统的连接方法。

2.2、registerNatives 方法作用

这里的 registerNatives 就是一个本地方法,从方法名可以判断,这个方法是用来注册本地方法的。那么注册哪些本地方法呢,我猜你已经想到,就是 Object 类中的其他 native 方法。

一个 Java 程序要想调用一个本地方法,需要执行两个步骤:第一,通过 System.loadLibrary() 将包含本地方法实现的动态文件加载进内存;第二,当Java程序需要调用本地方法时,虚拟机在加载的动态文件中定位并链接该本地方法,从而得以执行本地方法。

Object 类中的 registerNatives 方法的作用就是,让程序主动将本地方法链接到调用方,当 Java 程序需要调用本地方法时,就不需要虚拟机定位链接,而是可以直接调用。

使用 registerNatives 方法的几点好处:

  • 通过 registerNatives 方法在类被加载的时候就主动将本地方法链接到调用方,比当方法被使用时再由虚拟机来定位和链接更方便有效
  • 如果本地方法在程序运行中更新了,可以通过调用 registerNative 方法进行更新
  • Java 程序需要调用一个本地应用提供的方法时,因为虚拟机只会检索本地动态库,因而虚拟机是无法定位到本地方法实现的,这个时候就只能使用 registerNatives 方法进行主动链接
  • 通过 registerNatives 方法,在定义本地方法实现的时候,可以不遵守JNI命名规范

JNI 命名规范:

  • 前缀:Java_
  • 类的全限定名,用下划线进行分隔
  • 方法名

例如 Java_java_lang_Object_registerNatives.

2.3、registerNatives 方法原理

传统 Java JNI 方式:

  • 编写带有 native 方法的 Java 类
  • 使用 javah 命令生成 .h 头文件
  • 编写代码实现头文件中的方法

上述方式每次都需要通过 javah 依据 Java 类的全类名生成对应的 native 函数全名称,其实可以使用 registerNatives 方法把 C/C++ 中的方法隐射到 Java 中的 native 方法。

贴上 Object 类的中 registerNatives 方法的 C 语言实现:

/**
   第一个参数:hashCode 是java中的方法名称
   第二个参数:()I 是java中方法的签名,可以通过javap -s -p 类名.class 查看
   第三个参数:(void *)&JVM_IHashCode (返回值类型)映射到 native 的方法名称
*/
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]));
}

代码中的 *env 为 JNI 环境,RegisterNatives 方法即是进行动态注册。

3、getClass 方法

返回当前对象的运行时类,即一个 Class.

Class 是用来描述字节码的,是一个描述类的类。

4、hashCode 方法

hashCode 方法是根据一定的规则,将对象相关的信息映射成一个数值,这个数值称为散列值。

hashCode 方法的主要作用是为了配合基于散列的集合一起正常运行,例如 HashSet、HashMap、HashTable等。

贴一下 Object 中 hashCode 的本地方法实现:

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 = 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;
} 

总的来说有 6 种生成 hashCode 的方式 ,JDK6 和 JDK7 中使用方式 1 随机数的形式,hash 值是存在对象头中的,所以多次调用也不会出现不一致的情况。JDK8 使用方式 5,走程序的 else 条件,即使用 Xorshift.

5、equals 方法

判断两个对象是否等价,返回 true 表示等价,返回 false 表示不等。

Object 类中是直接使用 == 关系运算符进行比较,== 运算符比较可分为两种情况:

  • 基本数据类型:比较值
  • 引用数据类型:比较内存中的存放地址

显然对于引用数据类型,Object 中的 equals 方法并不太适用,所以一般会进行重写,下面贴一下 String 中的 equals 方法源码:

    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;
    }

可以看到,String 是先比较内存地址,内存地址不同的情况下,逐个字段进行比较,这样使用 equals 比较的结果更符合逻辑。

需要注意的是,当 equals 方法被重写时,通常都要重写 hashCode 方法,使之满足以下规律:

  • equals 为 true ==> hashCode 相同
  • equals 为 false ==> hashCode 不同
  • hashCode 相同 ==> equals 未知
  • hashCode 不同 ==> equals 为 false

6、clone 方法

clone 的意思是复制,顾名思义,这个方法用作对象的复制。

对象复制,或者称之为拷贝,可分为两种,深拷贝和浅拷贝:

  • 深拷贝:假设 B 复制了 A,当修改 A 时,任何情况下 B 不会发生变化,这就是深拷贝
  • 浅拷贝:修改 A 时 B 也会发生变化,这就是浅拷贝

深拷贝与浅拷贝出现的根源在于引用类型,浅拷贝时,被复制的对象的所有变量与原来对象的值相同,但是引用类型的变量,仍然指向原来的对象,即对象中的对象,其实是共享的。深拷贝是一个独立的对象拷贝,从最外层的对象到最里面的引用对象,都复制一遍。所以深拷贝速度较慢且花销较大。

Object 中的 clone 方法是浅拷贝。

7、toString 方法

数据对象的信息,这里需要注意一点,@ 后面的十六进制数字,并不是内存地址,只是 hashCode 的十六进制形式。

8、wait 和 notify 方法

先了解两个概念,锁池和等待池:

  • 锁池:假设线程 A 已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个 synchronized 方法(或者 synchronized 块),由于这些线程在进入对象的 synchronized 方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程 A 拥有,所以这些线程就进入了该对象的锁池中(锁释放后去竞争)
  • 等待池:假设一个线程 A 调用了某个对象的 wait 方法,线程 A 就会释放该对象的锁,进入到该对象的等待池中(不会竞争对象锁)

理解了锁池和等待池,那么 wait 方法的作用也就很清晰了,是将已经获得对象锁的线程立即移到等待池中。

再来说说 notify 的作用:

  • 作用对象:调用 a 对象的 wait 方法后,线程处于 a 对象的等待池中,等待池中的线程不会竞争 a 对象的锁。notify 作用的对象就是这些线程
  • 作用结果:调用 notify 后会有一个线程由等待池进入锁池,参与锁的竞争(随机唤醒)。而 notifyAll 会将对象等待池中的所有对象移到锁池中
  • 作用时机:当调用 notify 或 notifyAll 的线程执行完同步方法或者同步代码块中的所有代码,才会释放这把锁

下面说说需要注意的点:

  • 调用 a.wait() 或者 a.notify() 前必须要先拿到 a 对象的锁,故 wait 和 notify 方法要放在该对象的同步方法或者步代码块中
  • wait 方法会立即释放锁、释放 CPU
  • notify 方法不会立即释放锁,需等 notify 所在线程执行完同步块
  • 未获得该对象锁时调用该对象的 wait 或 notify 方法,会抛出 java.lang.IllegalMonitorStateException 异常
  • yield、sleep 不会释放锁,但会释放 CPU
  • join 释放锁,不会释放CPU
  • wait、sleep、join 都可以通过 interrupt 打断线程暂停状态,使线程立即抛出 InterruptedException

聊聊 Thread 中的 join 方法:

  • 作用:假如在 main 线程调用 t.join(),那么 main 线程阻塞,直到 t 线程运行结束

  • 原理:查看源码,发现 join 方法是同步方法,在内部使用了 wait 方法。当 main 线程调用 t.join() 时,先获取到了 t 线程对象的锁,然后在 join 方法内部执行 wait 方法,导致 main 线程阻塞(因为是 main 线程间接调用了 wait 方法,而不是 t 线程中调用的)。而结束阻塞的方式,是 t.isAlive() 结果返回 false,即线程已经是不可用状态如已经运行结束

9、wait(long timeout) 方法

与 wait 方法的区别是,此方法可以指定一个超时,超时之后将自己唤醒线程。使用 notify 或 notifyAll 可以在超时前唤醒线程。需要注意的是,wait(0) 与 wait() 是等价的。

wait(long timeout,int nanos)这是另一个提供相同功能的方法,唯一的区别是这个可以提供更高的精度。如果 nanos 在 [0,1000000) 之间,那么超时时间为 timeout+1.

10、finalize 方法

  • 当垃圾回收器要回收对象所占内存之前,会先调用该对象的 finalize 方法
  • finalize 方法只会在对象被回收前调用一次,调用具有不确定性,只保证方法会被调用,但是不保证会等待它运行完,原因是如果 finalize 方法运行缓慢或者发生死循环,会导致内存回收系统崩溃
  • finalize 方法是对象逃脱被回收的最后一次机会,并且最多只能用一次
  • 不建议使用 finalize 方法,它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序

11、所有类的父类

我们都知道,Object 是 java 中所有类的父类。
需要注意的是,这里的类,包括普通的 java 类、抽象类以及接口。
可以通过 Object.class.isAssignableFrom(Xxx.class) 进行验证。

参考

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值