(一)Java从头开始源码分析—Object类

既然是从头开始,那就肯定绕不过所有类的根类—Object类,所有创建的类包括抽象类在不指明继承哪个类的时候,都是默认继承Object,它是在java.lang包下的,从JDK1.0开始,源码如下(删了原文注释):

package java.lang;

public class Object {

    private static native void registerNatives();
    static {
        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 >= 500000 || (nanos != 0 && timeout == 0)) {
            timeout++;
        }

        wait(timeout);
    }

    public final void wait() throws InterruptedException {
        wait(0);
    }

    protected void finalize() throws Throwable { }
}

我们可以看到一共有12个方法,意味着所有类可以重写Object类中没有用final修饰符修饰的方法,因为final表示最终的、不可修改的。

在正式介绍这些方法之前,需要先弄清楚一个关键字:native。
我们可以发现有很多方法前都跟上了native关键字,中文翻译是“本地的”。

native 关键字告诉编译器(JVM)调用的是该方法在外部定义,意味着这个方法使用C/C++语言实现的,并且被编译成了dll,由java通过本地接口(Java Native Interface,JNI)去调用,这些函数的实现体在dll中,JDK的源代码中并不包含。对于不同的平台它们也是不同的。这也是java的底层机制,实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。

标识符native可以与所有其它的java标识符连用,但是abstract除外。这是合理的,因为native暗示这些方法是有实现体的,只不过这些实现体是非java的。

更多内容可以去网上查找资料,这里就不再展开,让我们回到源码,从第一个方法开始。

1、registerNatives();

这个方法为了让java虚拟机(JVM)发现本机功能并且注册本地函数,本地方法都保存在动态链接库dll中。

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

2、getClass();

返回这个对象的运行时类,返回的类对象是由所表示类的静态同步方法锁定的对象。

这里涉及到反射,所谓反射,可以理解为在运行时期获取对象类型信息的操作。传统的编程方法要求程序员在编译阶段决定使用的类型,但是在反射的帮助下,编程人员可以动态获取这些信息,从而编写更加具有可移植性的代码。

3、hashCode();

返回该对象的哈希码值。哈希码值是用来在散列存储结构中确定对象的存储地址的,支持此方法是为了提高哈希表(例如 java.util.Hashtable 提供的哈希表)的性能。

hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值,是指JVM虚拟出来的内存地址,不是实际物理内存地址。可以保证不同对象的返回值不同。

hashCode 的常规协定是:

  1. 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
  2. 如果根据 equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
  3. 如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不 要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)

hashcode 通过hash函数计算得到,hashcode就是在hash表中有对应的位置。
每个对象都有hashcode,通过将对象的物理地址转换为一个整数,将整数通过hash计算就可以得到hashcode。

4、equals(Object obj);

指示其他某个对象是否与此对象“相等”。

之前讲到了hashCode方法,那么既然已经有了hashCode,为什么还要一个equals呢?

因为当两个对象的hashCode相同的时候,并不能说明这两个对象就是同一个,相反,当两个对象equals相同的时候,说明这两个对象一定是相同的。

但是,所有对于需要大量并且快速的对比的话如果都用equal()去做显然效率太低,所以解决方式是,每当需要对比的时候,首先用hashCode()去对比,如果hashCode()不一样,则表示这两个对象肯定不相等(也就是不必再用equals()再去对比了),如果hashCode()相同,此时再对比他们的equal(),如果equal()也相同,则表示这两个对象是真的相同了,这样既能大大提高了效率也保证了对比的绝对正确性。

【官方文档中的说明】
equals 方法在非空对象引用上实现相等关系:

  1. 自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
  2. 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
  3. 传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
  4. 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
  5. 对于任何非空引用值 x,x.equals(null) 都应返回 false。

Object 类的 equals 方法实现对象上差别可能性最大的相等关系;即,对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 true(x == y 具有值 true)。

注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

5、clone();

创建并返回此对象的一个副本。“副本”的准确含义可能依赖于对象的类。是一种浅拷贝。
clone方法首先会判对象是否实现了Cloneable接口,若无则抛出CloneNotSupportedException, 最后会调用internalClone。

这里涉及到一个概念:深拷贝和浅拷贝,通俗一点来讲,浅拷贝就是复制了一个对象的引用,真正指向的对象是不变的;而深拷贝则相当于重新开辟了一块内存空间,将原对象中的各种数据信息都复制过来,在此基础上做任何修改都不会对原来的对象产生影响。

6、toString();

【官方文档说明】
返回该对象的字符串表示。通常, toString 方法会返回一个“以文本方式表示”此对象的字符串。结果应是一个简明但易于读懂的信息表达式。建议所有子类都重写此方法。

Object 类的 toString 方法返回一个字符串,该字符串由类名(对象是该类的一个实例)、at 标记符“@”和此对象哈希码的无符号十六进制表示组成。换句话说,该方法返回一个字符串,它的值等于:

getClass().getName() + '@' + Integer.toHexString(hashCode())

7、notify();

这是本地方法且无法被重写。

【官方文档说明】
唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。

直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。

此方法只应由作为此对象监视器的所有者的线程来调用。通过以下三种方法之一,线程可以成为此对象监视器的所有者:

  1. 通过执行此对象的同步实例方法。
  2. 通过执行在此对象上进行同步的 synchronized 语句的正文。
  3. 对于 Class 类型的对象,可以通过执行该类的同步静态方法。

一次只能有一个线程拥有对象的监视器。

notifyAll();

与notify不同的是这个方法唤醒在此对象监视器上等待的所有线程。线程通过调用其中一个 wait 方法,在对象的监视器上等待。

直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。

此方法只应由作为此对象监视器的所有者的线程来调用。

wait(long timeout);

【官方文档说明】
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。

当前线程必须拥有此对象监视器。

此方法导致当前线程(称之为 T)将其自身放置在对象的等待集中,然后放弃此对象上的所有同步要求。出于线程调度目的,在发生以下四种情况之一前,线程 T 被禁用,且处于休眠状态:

  1. 其他某个线程调用此对象的 notify 方法,并且线程 T 碰巧被任选为被唤醒的线程。
  2. 其他某个线程调用此对象的 notifyAll 方法。
  3. 其他某个线程中断线程 T。
  4. 大约已经到达指定的实际时间。但是,如果 timeout 为零,则不考虑实际时间,在获得通知前该线程将一直等待。

然后,从对象的等待集中删除线程 T,并重新进行线程调度。然后,该线程以常规方式与其他线程竞争,以获得在该对象上同步的权利;一旦获得对该对象的控制权,该对象上的所有其同步声明都将被恢复到以前的状态,这就是调用 wait 方法时的情况。然后,线程 T 从 wait 方法的调用中返回。所以,从 wait 方法返回时,该对象和线程 T 的同步状态与调用 wait 方法时的情况完全相同。

如果当前线程在等待之前或在等待时被任何线程中断,则会抛出 InterruptedException。在按上述形式恢复此对象的锁定状态时才会抛出此异常。

注意,由于 wait 方法将当前线程放入了对象的等待集中,所以它只能解除此对象的锁定;可以同步当前线程的任何其他对象在线程等待时仍处于锁定状态。

wait(long timeout, int nanos);

比上一个多了一个参数nanos,意思是当其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。

此方法类似于一个参数的 wait 方法,但它允许更好地控制在放弃之前等待通知的时间量。

当前线程必须拥有此对象监视器。该线程发布对此监视器的所有权,并等待下面两个条件之一发生:

  1. 其他线程通过调用 notify 方法,或 notifyAll 方法通知在此对象的监视器上等待的线程醒来。
  2. timeout 毫秒值与 nanos 毫微秒参数值之和指定的超时时间已用完。

然后,该线程等到重新获得对监视器的所有权后才能继续执行。

wait();

在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。换句话说,此方法的行为就好像它仅执行 wait(0) 调用一样。

finalize();

【官方文档说明】
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。子类重写 finalize 方法,以配置系统资源或执行其他清除。

finalize 的常规协定是:当 Java 虚拟机已确定尚未终止的任何线程无法再通过任何方法访问此对象时,将调用此方法,除非由于准备终止的其他某个对象或类的终结操作执行了某个操作。finalize 方法可以采取任何操作,其中包括再次使此对象对其他线程可用;不过,finalize 的主要目的是在不可撤消地丢弃对象之前执行清除操作。例如,表示输入/输出连接的对象的 finalize 方法可执行显式 I/O 事务,以便在永久丢弃对象之前中断连接。

Object 类的 finalize 方法执行非特殊性操作;它仅执行一些常规返回。Object 的子类可以重写此定义。

Java 编程语言不保证哪个线程将调用某个给定对象的 finalize 方法。但可以保证在调用 finalize 时,调用 finalize 的线程将不会持有任何用户可见的同步锁定。如果 finalize 方法抛出未捕获的异常,那么该异常将被忽略,并且该对象的终结操作将终止。

在启用某个对象的 finalize 方法后,将不会执行进一步操作,直到 Java 虚拟机再次确定尚未终止的任何线程无法再通过任何方法访问此对象,其中包括由准备终止的其他对象或类执行的可能操作,在执行该操作时,对象可能被丢弃。

对于任何给定对象,Java 虚拟机最多只调用一次 finalize 方法。

finalize 方法抛出的任何异常都会导致此对象的终结操作停止,但可以通过其他方法忽略它。

总结

使用wait或者notify()方法一定要在同步代码块中使用,而wait一般要在while循环中使用
wait/notify可以实现生产者消费者模式,其原理是调用wait时将线程放入等待队列,而调用notify时将等待队列中的线程移动到同步队列
wait/notify机制是成对出现的,它们的实现依赖于锁的同步机制

你可能感兴趣的:(Java源码分析)