记录Object源码学习,牵扯到native方法具体实现的,暂时不去深究,简单说说方法意义。
上图来自Object源码解析
Object类,所有类的根类,位于java.lang。
在Java中,native关键字用于标识一个方法是由非Java语言实现的,也就是说这个方法的实现是由其他语言(如C或C++)编写的,Object中的大部分方法都是native方法。
这些native方法的具体实现都是由JVM实现的,因为可能这些功能无法通过Java语言本身来实现,例如访问底层操作系统或硬件资源。
在Java的设计中,先有的是JVM的功能,然后根据JVM的功能设计了Object类中的native方法的声明。
Java的设计者首先定义了Java语言的规范和虚拟机的功能,其中包括了一些与底层操作系统和硬件交互的功能。为了实现这些功能,Java语言引入了native关键字,用于声明本地方法,即由其他语言实现的方法。
然后,根据JVM的功能需求,Java的设计者在Object类中声明了一些native方法,这些方法提供了与底层操作系统和硬件交互的能力。这些native方法的具体实现是由JVM提供的,JVM会根据操作系统和硬件平台的不同,在JVM的本地代码库中实现这些方法。
1.registerNatives()方法
private static native void registerNatives();
static {
registerNatives();
}
static{}是一个静态代码块,它在类加载的时候执行,且只会执行一次。静态代码块用于在类加载时进行一些初始化操作,比如初始化静态变量或调用静态方法。
这个函数的作用是将Object的native方法注册到JVM中,通过注册本地方法,Object类中的native方法与JVM中的本地代码库建立了联系,使得Java程序能够通过调用Java类的方法来间接调用底层的本地方法。
2.getClass()方法
public final native Class<?> getClass();
这个函数返回一个对象的运行时类,即对象所属的类的Class对象。它是Object类中的一个成员方法,因此所有的Java对象都可以调用该方法。
通过调用getClass()方法,可以获取一个对象的实际类型信息,包括类的名称、包的名称、父类、接口等。该方法返回的是一个Class对象,可以通过该对象进行一些反射操作,比如获取类的构造函数、方法、字段等信息。
示例代码:
public class Main {
public static void main(String[] args) {
String str = "Hello World";
Class<?> clazz = str.getClass();
/*<?>通配符,表示一个具体的未知类,Class 是一个特殊的类,用于表示类的元数据信息*/
System.out.println(clazz.getName()); // 输出:java.lang.String
System.out.println(clazz.getSimpleName()); // 输出:String
}
}
在上面的示例中,通过调用str对象的getClass()方法,获取了String类的Class对象。然后可以通过Class对象的getName()方法获取类的全限定名,通过getSimpleName()方法获取类的简单名称。
3.hashCode()方法
public native int hashCode();
hashCode()
方法是 Object 类中的一个成员方法,它返回对象的哈希码(hash code)。
哈希码是一个整数值,用于快速确定对象在哈希表中的位置。哈希表是一种常用的数据结构,用于实现集合类(如 HashSet)和映射类(如 HashMap)。在哈希表中,对象的哈希码用于确定对象在内部数组中的索引位置,从而可以快速地进行查找、插入和删除操作。
哈希码的计算通常是基于对象的内容,因此对于相同内容的对象,它们的哈希码应该是相同的。但是,由于哈希码是一个整数值,它的范围有限,因此不同的对象可能会产生相同的哈希码,这种情况称为哈希冲突。
在 Java 中,hashCode()
方法的默认实现是根据对象的内存地址计算哈希码。但是,根据需要,我们可以重写 hashCode()
方法,根据对象的内容计算哈希码,以提高哈希表的性能和减少哈希冲突的概率。
重写 hashCode()
方法时,需要遵循以下规则:
- 如果两个对象通过
equals()
方法相等,那么它们的哈希码必须相等。 - 如果两个对象的哈希码相等,它们不一定相等。
示例代码:
public class Person {
private String name;
private int age;
// 构造函数、getter和setter方法省略
@Override
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
result = 31 * result + age;
return result;
}
}
在上面的示例中,重写了 hashCode()
方法,根据对象的 name
和 age
属性计算哈希码。通过这种方式,相同内容的对象将具有相同的哈希码,从而可以更好地利用哈希表的性能优势。
所以说,在后面的类方法实现中,很多时候视情况而定,重写hashCode()方法适应具体情况。
4.equals(Object obj)方法
public boolean equals(Object obj) {
return (this == obj);
}
非native方法。
这里很明显能看出是比较两个对象是否相同,但是Object这里比较的是地址,后面很多时候不一定是地址,也有可能是键值等数据比较,再根据情况进行重写即可。
5.clone()方法
protected native Object clone() throws CloneNotSupportedException;
该方法用于创建并返回当前对象的一个副本(即克隆对象)。
克隆是指创建一个与原始对象具有相同状态的新对象。通过克隆,可以在不使用构造函数的情况下创建一个对象的副本,从而避免了重新分配内存和初始化对象的开销。
在 Java 中,通过实现 Cloneable
接口,可以标记一个类为可克隆的,没有定义任何方法。如果一个类没有实现 Cloneable
接口但调用了 clone()
方法,会抛出CloneNotSupportedException
异常。
需要注意的是,clone()
方法创建的是浅拷贝(shallow copy)。如果需要创建深拷贝(deep copy),即复制对象及其内容,需要自行实现深拷贝的逻辑。
示例代码:
public class Person implements Cloneable {
private String name;
private int age;
// 构造函数、getter和setter方法省略
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); //调用父类Object的clone方法
}
}
在上面的示例中,Person
类实现了 Cloneable
接口,并重写了 clone()
方法。通过调用 super.clone()
,可以创建并返回当前对象的一个副本。需要注意的是,clone()
方法的返回类型是 Object
,因此需要进行类型转换。
使用 clone()
方法时,可以通过以下方式进行克隆:
Person person1 = new Person("Alice", 20);
Person person2 = (Person) person1.clone();
通过 clone()
方法创建的克隆对象和原始对象是独立的,修改其中一个对象的属性不会影响另一个对象。
6.toString()方法
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
非native方法。
toString()方法返回的字符串表示了该对象的类名和哈希码。Integer.toHexString(hashCode())接受的是一个十进制数,这里也就是哈希码,这个函数功能是将十进制转为十六进制字符串表示。
7.notify()方法
public final native void notify();
用于唤醒在该对象上等待的单个线程。如果有多个线程等待该对象,则只会唤醒其中一个线程,并且是由线程调度器决定唤醒哪个线程。
该方法的作用和意义在于实现线程间的通信。当一个线程在某个对象上调用了该对象的wait()方法后,它会进入该对象的等待集,并且释放对象上的锁。此时,其他线程可以调用该对象的notify()方法来唤醒等待集中的一个线程,使其重新竞争对象上的锁。
通过使用wait()和notify()方法,可以实现线程间的协作和同步。例如,一个线程在生产者-消费者模型中生产了一个数据项后,可以调用该数据项对象的notify()方法,通知消费者线程可以开始消费了。
需要注意的是,notify()方法只能在同步代码块或同步方法中调用,因为它需要获取对象上的锁来唤醒等待的线程。如果在非同步的上下文中调用notify()方法,会抛出IllegalMonitorStateException异常。
下面就是一个生产者消费者模型的简单实现通信。
public class HelloWorld {
public static void main(String[] args) {
final Data data = new Data();
// 创建生产者线程
Thread producerThread = new Thread(() -> {
synchronized (data) { //同步块内
// 生产数据
data.setData("Hello World");
// 通知消费者线程可以开始消费
data.notify();
}
});
// 创建消费者线程
Thread consumerThread = new Thread(() -> {
synchronized (data) {
try {
// 等待生产者线程生产数据
data.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 消费数据
System.out.println("Consumed data: " + data.getData());
}
});
// 启动线程
producerThread.start();
consumerThread.start();
}
static class Data {
private String data;
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
}
在这段代码中,生产者线程在生产数据后确实会通过data.notify()
方法通知消费者线程可以开始消费。然而,由于线程的调度机制是由操作系统控制的,我们无法保证生产者线程在发送通知后立即让出CPU执行时间片,而消费者线程立即获得CPU执行时间片并开始执行。
因此,在某些情况下,生产者线程可能会在发送通知后继续执行一段时间,而消费者线程可能需要等待一段时间才能获得CPU执行时间片并开始执行。这就导致了只有一个"生产数据"的输出,而"Consumed data: Hello World"的输出可能会稍后才出现。
这种情况下,我们无法准确预测生产者和消费者线程的执行顺序。在某些情况下,消费者线程可能会在生产者线程发送通知之前开始执行,这样就会导致输出顺序为"Consumed data: Hello World"先于"生产数据"。
8.notifyAll()方法
public final native void notifyAll();
很明显,它用于唤醒正在等待该对象锁的所有线程。
当一个线程调用了某个对象的notifyAll()方法后,该对象上等待的所有线程都会被唤醒,然后它们会开始竞争这个对象的锁。
被唤醒的线程会从等待状态转变为可运行状态,但并不表示它们会立即获得锁。只有当持有该对象锁的线程释放锁后,唤醒的线程才能有机会获取到锁并执行。
notifyAll()方法和notify()方法一样,只能在同步代码块或同步方法中调用,否则会抛出IllegalMonitorStateException异常。
9.wait方法
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);
}
这里实现了三个wait函数的重载,第三个wait()很简单,就是调用第一个wait函数,传参为0。
第一个wait(),用于使当前线程进入等待状态,直到其他线程调用该对象的notify()方法或notifyAll()方法,或者指定的等待时间到期。
当一个线程调用了某个对象的wait(long timeout)方法后,它会释放该对象的锁,并进入等待状态。在等待状态下,该线程不会参与到锁的竞争,也不会执行任何代码,直到以下三种情况之一发生:
- 其他线程调用了该对象的notify()方法或notifyAll()方法,唤醒了等待的线程。
- 指定的等待时间到期,即等待超时。
- 当前线程被中断。
如果等待时间大于0,则表示在指定的等待时间内等待,如果等待时间小于等于0,则表示一直等待下去,直到被唤醒。所以可以想到,第三个wait()函数的目的就是使进程一直等待下去。
wait(long timeout)方法只能在同步代码块或同步方法中调用,否则会抛出IllegalMonitorStateException异常。另外,该方法会抛出InterruptedException异常,当线程在等待状态中被中断时会抛出该异常。
wait()方法的应用举例已经在notify中说明,不再过多解释。
另外说明,timeout代表的等待时间是毫秒级的。
第二个wait的nanos代表的是额外的纳秒时间,看代码很容易理解,先对timeout和nanos进行数据的合理化判断。这里不同于第一个wait()的是,当timeout小于0是直接抛出异常而不是一直等待。
主要讲一下为什么要对timeout加1:
在Java的线程调度中,时间片是以毫秒为单位进行分配的。线程调度器会按照一定的策略在不同的线程之间切换执行。如果一个线程在等待的过程中发生了上下文切换,即切换到其他线程执行,然后又切换回来,那么实际等待的时间就会少于timeout毫秒。
为了避免这种情况,可以将timeout参数加1,以确保至少等待timeout + 1毫秒的时间。这样即使发生了上下文切换,也能够保证等待的时间不会少于timeout毫秒。
10.finalize()方法
protected void finalize() throws Throwable { }
这里仅仅只是一个空函数,可供子类针对自己的情况进行重写。它是Java中的垃圾回收机制的一部分。当一个对象不再被引用时,垃圾回收器会在适当的时候调用finalize()方法来释放对象占用的资源。