主要介绍 Java 中 Object 类的常见方法,通过阅读源码的方式加深对各种方法的理解。如有错误,欢迎在评论中和我一起讨论交流。

写在前面

为了方便在 IntelliJ IDEA 中查看源码,可以使用以下快捷键:

  • Alt + 7Ctrl + F12 查看类中所有的属性和方法;
  • Ctrl + H 查看类的继承关系;
  • Carl + Alt + Shift + U 查看继承关系图。

通过 IDEA 中的各种不同的 图标,可以很快的看出其所代表的是类、抽象类、枚举、方法、属性、还是接口等等。

总览

java.lang 下的 Object 是类层次体系中的根类,也就是说对于每个类都以 Object 类作为超类或父类。所有的对象(包括数组)都实现了此类中的方法。当我们在引用任何一个未知类型对象的时候,就可以使用 Object 类。

父类引用指向子类对象,称为 上转型。例如 Person p = new Son();

该类为对象提供了一些常见的方法,如下图所示:

image.png

  • private static native void registerNatives();
  • public final native Class<?> getClass();
  • public native int hashCode();
  • public boolean equals(Object obj)
  • protected native Object clone() throws CloneNotSupportedException;
  • public String toString()
  • 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
  • public final void wait() throws InterruptedException
  • protected void finalize() throws Throwable

接下来看一下每个方法到底有什么用。

registerNatives()

该方法在 stackoverflow 上有人回答的很赞。简单地说,作为一个被 private static native 修饰的方法,其在 Object 类加载的时候就会执行。被 native 修饰的方法通常是由 C/C++ 实现的,如果想在 Java 中调用其它语言,就必须按照 JNI 的规则实现。

registerNatives() 方法在向 JVM 注册本地方法的时候很有用,它可以将 Java 中的方法与本地方法对应起来,JVM 在执行字节码时根据这些映射关系来调用 C/C++ 方法,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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]));
}

当调用 java.lang.Object.registerNatives 方法的时候,对应的本地方法为 Java_java_lang_Object_registerNatives()。假如在执行 notifyAll 方法的时候,通过之前指定的映射关系,可以查到对应 JVM 中的方法为 JVM_MonitorNotifyAll。由此可见,通过这种关联关系可以很好的实现 Java 语言与其它语言的“交互”,也就是说,registerNatives 方法可以将指定的本地方法绑定到指定函数。

getClass()

final 修饰的 getClass() 方法不允许被子类重写(Override),它用于返回当前 运行时 对象的 Class 对象,同时也可以获取类的信息。

对于返回当前的 运行时 对象的 Class 对象,可以用如下代码来说明:

1
2
3
4
5
6
7
public class Demo {
    public static void main(String[] args) {
        Number n = 123;
        System.out.println(n.getClass());
        System.out.println(n.getClass().getName());
    }
}

输出结果如下:

1
2
class java.lang.Integer
java.lang.Integer

n 一开始定义的是 Number 类型的,从输出结果可以看出,其在运行时输出的结果是 Integer 类型的。这是由于在 Java 中,对于像 123 这种数值来说默认是 Integer 类型的。其中 getName() 用于返回当前对象的实体名称。

对于获取类的信息,可以用以下代码来说明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Demo {
    public static void main(String[] args) {
        Person p = new Person("Tom", 25);
        Class<? extends Person> pClass = p.getClass();
        System.out.println(pClass);
        System.out.println(pClass.getPackage());
        System.out.println(pClass.getName());
    }
}

class Person{
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

输出结果如下所示:

1
2
3
class _01_BasicClass.Person
package _01_BasicClass
_01_BasicClass.Person

可以看到,通过 p.getClass() 获得了 Class 对象,然后可以通过 pClass.getPackage() 来获取该对象所对应 的信息。

hashCode()

该方法用于返回对象的 哈希值,在 HashMap 类中重写了此方法。

它有如下的几个约定:

  • 在 Java 应用程序执行期间,在不修改当前对象的 equals 方法的时候,则在当前同一对象上多次调用 hashCode 方法会返回相同的整数值。 在不同的程序执行后,对象的哈希值不必保持一致。
  • 如果两个对象通过 equals 方法比较后 相等 的话,那么分别对这两个对象调用 hashCode 方法,则最后产生的结果(哈希值)是相同的。
  • 如果两个对象通过 equals 方法比较后 不相等,那么别对这两个对象调用 hashCode 方法的时候,不需要必须产生不同的结果。但是,为不相等的对象生成不同的哈希值可能会提高哈希表的性能。

通常情况下,不同的对象产生的哈希码是不同的。默认情况下,对象的哈希码是通过将该对象的内部地址转换成一个整数来实现的。

例如下面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Demo {
    public static void main(String[] args) {
        HashMap<Person, String> map = new HashMap<>();
        Person p1 = new Person("tom", 24);
        Person p2 = new Person("tom", 25);
        map.put(p1, "tom");
        map.put(p2, "tom");
        System.out.println(map);
    }
}

class Person{
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int hashCode() {
        return name != null ? name.hashCode() : 0;
    }
}

输出结果如下所示:

1
{_01_BasicClass.Person@1c152=tom, _01_BasicClass.Person@1c152=tom}

从结果可以看到,在仅重写 hashCode 方法的时候,p1p2 都可以加入到 map 中。原因是 map 通过判断 两个对象的哈希值相同 并且 这两个对象是同一个对象或者两个对象相等 的时候才指出是重复的。

所以,如果在重写 hashCode 方法的前提下,又重写了 equals 方法,那就会被认为是相同的对象,则只能添加一个元素了。如下所示:

1
2
3
4
5
6
7
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return name != null ? name.equals(person.name) : person.name == null;
    }

输出结果如下所示:

1
{_01_BasicClass.Person@1c152=tom}

所以在实际开发中,这两个方法要一起重写。

equals()

该方法用来判断 两个对象是否相等。直接使用 == 来判断两个对象的地址值是否相等,也就是判断这两个对象是不是同一个对象。其源码如下所示:

1
2
3
public boolean equals(Object obj) {
    return (this == obj);
}

Object 类中的 equals 方法就是用 == 来做比较的,对于基本数据类型,其比较的是具体的值;对于引用数据类型,比较的则是内存地址。


基本数据类型:

  • byte、short、int、long、float、double、char、boolean

引用数据类型:

  • 类、接口、数组

看下面的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Demo {
    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abc";
        String str3 = new String("aaa");
        String str4 = new String("aaa");

        if (str1 == str2)  // true
            System.out.println("str1 == str2");
        if (str3 == str4)  // false
            System.out.println("str3 == str4");
        if (str1.equals(str2))  // true
            System.out.println("str1.equals(str2)");
        if (str3.equals(str4))  // true
            System.out.println("str3.equals(str4)");
    }
}

输出结果如下所示:

1
2
3
str1 == str2
str1.equals(str2)
str3.equals(str4)

从结果可以看出,对于 String str1 = "abc"; 来说,首先在常量池中查看有没有已经存在的值和相应的对象,由于第一次创建,所以创建 str1 并指向 abc。执行到 String str2 = "abc"; 的时候,由于之前创建过了,所以 str1str2 指向的地址都是同一个对象 abc。因此返回 true,即输出 str1 == str2

对于 String str3 = new String("aaa"); 来说,一共会创建两个字符串对象,一个在堆中,另一个在常量池中(前提是常量池中还没有 aaa)。其创建了一个名为 str3 的引用,然后执行到 String str4 = new String("aaa"); 的时候,又创建了一个 str4 的引用,这两个引用的内容都是 aaa。由于 String 重写了 equals 方法,所以此时的 str3.equals(str4) 比较的是 str3str4 的内容是否相等,此时的内容是相等的,所以返回 true

关键就是看有没有重写 equals 方法。

直接使用双引号声明的 String 对象会直接存储在常量池中。


可以直接使用 intern 方法声明 String 对象,其作用是:

  • 如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;

  • 如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。

如下列代码:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
    String s1 = new String("abc");
    String s2 = s1.intern();
    String s3 = "abc";
    System.out.println(s2); //abc
    System.out.println(s1 == s2); //false,因为一个是堆内存中的 String 对象一个是常量池中的 String 对象
    System.out.println(s2 == s3); //true, s1 和 s2指向常量池中的 abc
}

输出结果如下所示:

1
2
3
abc
false
true

此外,equals 方法在非空对象上还存在一些引用上的特性:

  • 自反性:对于任何非空的引用值 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,如果 x.equals(y) 返回的是 true 或 false,不管重复调用多少次 x.equals(y),其结果还是对应的 true 或 false。
  • 对于任何非空引用值 x,x.equals(null) 应该返回的是 false。也就是说,x.equals( 与x不同类型的对象 ) 返回的是 false。

clone()

该方法是一个 protected native 的方法,用于创建并返回一个对象的拷贝。对于任意的对象 x 有如下说明:

  • 表达式 x.clone() != x 返回的是 true。
  • 表达式 x.clone().getClass() == x.getClass() 返回的是 true。
  • 表达式 x.clone().equls(x) 返回的是 true。

这里涉及到 浅拷贝深拷贝 的问题。如下图所示:

image.png

对于 浅拷贝,它是直接将原先对象的引用直接拷贝给新对象的,通过引用拷贝的方式适用于 引用数据类型。而对于基本数据类型来说,则是进行 值传递 的方式。

对于 深拷贝,基本数据类型的拷贝方式也是通过 值传递 的方式,而对于引用数据类型来说,它是根据原先的对象又创建了一个新对象,然后再将内容复制到新对象。

toString()

该方法返回一个字符串,这个字符串的表示形式是由 当前对象实例的类的名称符号 @ 以及 该对象的无符号十六进制 组成。建议所有子类都重写此方法。源代码如下所示:

1
2
3
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

下面通过一个例子来说明其用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Demo {
    public static void main(String[] args) {
    String str = new String("abc");
    // 这里使用 toString() 是多余的,因为 String 中已经重写了 Object 中的 toString() 方法
    System.out.println(str.toString()); 

    Person p = new Person("tom", 25);
    // 由于在 Person 类中没有重写 toString() 方法,所以这里使用的其实是 Object 中的 toString() 方法
    System.out.println(p.toString()); 
    }

}

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

输出结果如下所示:

1
2
abc
_01_BasicClass.Person@1b6d3586

notify()

该方法用于唤醒在当前对象监视器上等待的 单个 线程。如果所有线程都在此对象上等待,则只会选择唤醒它们当中的一个线程。这种选择是 任意的,并且可以根据实现情况进行选择。一个线程可以通过调用 wait() 方法在对象监视器上等待。

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

notify() 方法只能被作为此对象监视器的所有者的线程来调用。一个线程要想成为对象监视器的所有者,可以使用以下 3 种方法:

  • 执行对象的同步实例方法。
  • 执行对对象进行同步的同步语句体,也就是 synchronized 内置锁。
  • 对于 Class 类型的对象来说,执行同步静态方法。

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

notifyAll()

该方法用于唤醒在当前对象监视器上等待的 所有 线程。

单从字面意义上理解,notify() 方法唤醒单个线程,而 notifyAll() 唤醒所有线程。那这两个方法到底有什么区别呢?

这里参考了其它博客,首先要知道两个概念:

  • 锁池:如果线程 A 已经拥有了某个对象 B 的锁,而其它的线程想要调用这个对象 B 的某个 synchronized 方法或执行 synchronized 块的时候,由于其它线程在进入对象 B 的 synchronized 方法或 synchronized 块之前需要先获得对象 B 所持有的锁的拥有权。但是此时对象 B 的锁目前正在被线程 A 拥有,所以其它线程就调用不了对象 B 的 synchronized 方法或执行 synchronized 块,反而进入了对象 B 的 锁池 中。

  • 等待池:如果线程 A 调用了某个对象 B 的 wait() 方法,则线程 A 就会释放对象 B 的锁,同时线程 A 就会进入对象 B 的 等待池 中。为什么?因为 wait() 方法是在 synchronized 中的,线程 A 在执行 wait() 方法之前其实就已经拥有了对象 B 的锁。此时,如果另外一个线程调用了对象 B 的 notifyAll() 方法,那么处于对象 B 的 等待池 中的线程就会全部进入对象 B 的 锁池 中,准备争夺锁的拥有权。如果另外一个线程调用了 notify() 方法,则仅有一个处于对象 B 的 等待池 中的线程会随机地进入对象 B 的 锁池 中。

所以,当某个线程调用 wait() 方法后,在释放锁的同时进入等待池中,不参与锁的竞争。而当调用 notify() 后,等待池中 仅有一个线程 会进入该对象的锁池中参与锁的竞争,如果竞争成功,则会获得锁,否则继续在锁池中等待下一次竞争。当调用 notifyAll() 后,等待池中的 所有线程 都会进入该对象的锁池中参与锁的竞争。

wait(long timeout)

该方法使当前线程等待,直到另一个线程为此对象调用 notify() 或 notifyAll() 方法或经过了指定的时间(timeout)。

需要注意的是,如果将参数设置为 0 的话,则表示一直处于等待状态。

wait(long timeout, int nanos)

该方法和 wait(long timeout) 是一样的,只不过多了一个参数 nanos。所以,超时时间还应该再加上这一额外的时间。其源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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);
}

wait()

该方法通过查看源码可知:就是让当前线程一直处于等待的状态,即 timeout 置为 0。源码如下所示:

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

finalize()

该方法与 JVM 中垃圾回收机制相关,当垃圾回收器确定不再有对该对象的引用时,由垃圾回收器在对象上调用此方法。

参考

Java™ Platform, Standard Edition 8 API Specification

Java 中的锁池和等待池