JDK1.8源码(一)——java.lang.Object类

一、概述

1、介绍

  Object 类属于 java.lang 包,此包下的所有类在使用时无需手动导入,系统会在程序编译期间自动导入。
  Object 类是所有类的基类,如果一个类没有使用 extends 标识继承另外一个类,那么这个类默认继承Object类。任何类都直接或间接继承此类。

  类结构图:

类结构图

  代码示例:Object类源码

 1 public class Object {
 2 
 3     private static native void registerNatives();
 4     static {
 5         registerNatives();
 6     }
 7 
 8     public final native Class<?> getClass();
 9 
10     public native int hashCode();
11 
12     public boolean equals(Object obj) {
13         return (this == obj);
14     }
15 
16     protected native Object clone() throws CloneNotSupportedException;
17 
18     public String toString() {
19         return getClass().getName() + "@" + Integer.toHexString(hashCode());
20     }
21 
22     public final native void notify();
23 
24     public final native void notifyAll();
25 
26     public final native void wait(long timeout) throws InterruptedException;
27 
28     public final void wait(long timeout, int nanos) throws InterruptedException {
29         if (timeout < 0) {
30             throw new IllegalArgumentException("timeout value is negative");
31         }
32 
33         if (nanos < 0 || nanos > 999999) {
34             throw new IllegalArgumentException(
35                                 "nanosecond timeout value out of range");
36         }
37 
38         if (nanos > 0) {
39             timeout++;
40         }
41 
42         wait(timeout);
43     }
44 
45     public final void wait() throws InterruptedException {
46         wait(0);
47     }
48 
49     protected void finalize() throws Throwable { }
50 }
Object类源码

2、为什么java.lang包下的类不需要手动导入?

  使用 java.lang 包下的所有类,都不需要手动导入。Java中导包有两种方法:
  ①、单类型导入(single-type-import),例如import java.util.Date
  ②、按需类型导入(type-import-on-demand),例如import java.util.*
  单类型导入:需要什么类便导入什么类,这种方式是导入指定的 public 类或者接口。
  按需类型导入:比如import java.util.*,* 并不是导入java.util包下的所有类,而是根据名字按需导入。
  Java编译器会从启动目录(bootstrap),扩展目录(extension)和用户类路径下去定位需要导入的类,而这些目录仅仅给出了类的顶层目录,编译器的类文件定位方法大致可以理解为如下公式:

  顶层路径名 \ 包名 \ 文件名.class = 绝对路径

  单类型导入知道包名和文件名,所以编译器可以一次性定位到所需的类文件。按需类型导入则比较复杂,编译器会把包名和文件名进行排列组合,然后对所有的可能性进行类文件查找定位。例如:

  package com;

  import java.io.*;
  import java.util.*;

  如果文件中使用到了 File 类,那么编译器会按如下几个步骤来进行查找 File 类:

  ①、File  // File类属于无名包,即没有package语句,编译器会首先搜索无名包
  ②、com.File     // File类属于当前包,就是当前编译类的包路径
  ③、java.lang.File  // 由于编译器会自动导入java.lang包,所以也会从该包下查找
  ④、java.io.File
  ⑤、java.util.File
  ......

  注意:编译器找到 java.io.File 类之后并不会停止下一步的寻找,而是把所有的可能性都查找完以确定是否有类导入冲突。
  假设此时的顶层路径有三个,那么编译器就会进行3*5=15次查找。如果查找完成后,编译器发现了两个同名的类,就会报错。要删除你不用的那个类,然后再编译。
  结论:按需类型导入是绝对不会降低Java代码的执行效率的,但会影响Java代码的编译速度。所以,编码时最好使用单类型导入,这样不仅能提高编译速度,也能避免命名冲突。

二、类源码

1、类构造器

  一个类必须要有一个构造器,如果没有显示声明,那么系统会默认创造一个无参构造器,在JDK的Object类源码中,是看不到构造器的,系统会自动添加一个无参构造器。
  可以通过 Object obj = new Object();构造一个Object类的对象。

2、equals(Object obj)方法

  比较的是对象的引用是否指向同一块内存地址。一般情况下比较两个对象时比较他的值是否一致,所以要进行重写。
  Object.equals方法
  代码示例:案例

 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4         int i = 1, j = 1;
 5         System.out.println(i == j); // true
 6 
 7         Person p1 = new Person("张三", 20);
 8         Person p2 = new Person("张三", 20);
 9         
10         System.out.println(p1 == p2); // false
11         System.out.println(p1.equals(p2)); // false
12     }
13 }
14 
15 class Person {
16     private String name;
17     private int age;
18 
19     public Person(String name, int age) {
20         this.name = name;
21         this.age = age;
22     }
23 }
案例

  如图,对象在栈,堆空间的表示:p1变量存放在栈中,存放的是对象在堆空间的地址(引用)。

  那么,不难理解程序运行的结果:
  ①基本数据类型值是否相等?i == j,true。
  ②两个对象的引用是否相等?p1 == p2,false。显然 p1 = 0x0001,p2 = 0x0002。所以 p1 == p2 为false。
  ③两个对象是否相等?false。
  从源码可以看到,在 Object 类中的 equals 方法与 == 运算符是等价的,也是比较的两个对象的引用是否相等。所以 p1.equals(p2) 为false。

  然而,这两个对象都是("张三",20),如何才能比较两个对象是否相等呢?
  答:需要程序员复写 equals 方法。
  equals 用于比较两个对象是否相等。那么,两个对象如何才认为是相等的呢?这个判定逻辑由程序员自己定义。如下:
  代码示例:Person类的 equals 方法

 1 @Override
 2 public boolean equals(Object o) {
 3     if (this == o) return true; // 引用相同,则一定相等
 4     if (o == null || !(o instanceof Person)) return false;
 5 
 6     Person person = (Person) o;
 7 
 8     // age和name都相同,对象才相等
 9     if (age != person.age) return false;
10     return name.equals(person.name);
11 }
Person类的 equals 方法

  不难看懂,这里复写了Object类中的 equals 方法,定义了我们想要的比较方式来比较对象是否相等。
  这里,定义对象所有的属性相等,则相等。再运行 p1.equals(p2) 为 true。
  当然,也可以写为 age == person.age ,则只要 Person 的年龄相同,则对象相等。读者可自行验证。
  equals() 方法和 == 运算符的区别:

 
equals()
== 运算符
比较类型
对象是否相等(程序员自定义)
①基本类型的值;②对象的引用是否相等

 

  String.equals方法

  源码示例:

 1 // jdk1.8源码
 2 public boolean equals(Object anObject) {
 3     if (this == anObject) {
 4         return true;
 5     }
 6     if (anObject instanceof String) {
 7         String anotherString = (String)anObject;
 8         int n = value.length;
 9         if (n == anotherString.value.length) {
10             char v1[] = value;
11             char v2[] = anotherString.value;
12             int i = 0;
13             while (n-- != 0) {
14                 if (v1[i] != v2[i])
15                     return false;
16                 i++;
17             }
18             return true;
19         }
20     }
21     return false;
22 }
源码示例

  上述源码不难读懂,String类复写了Object类中的 equals 方法,定义了判断字符串相等的方式,是每个字符都相同。
  String 是引用类型,比较时重点在于字符串的内容是否相等。所以不能用 == 运算符,而应该使用 equals 方法。

  equals方法原则

  在Java规范中,对 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,前提是对象中 equals 比较所用的属性没有被修改。
  ⑤对于任何非空引用值 x,x.equals(null) 都应返回 false。

  在上述案例中,定义一个 Person 类的子类 Student,也重写 equals 方法:

 1 class Student extends Person {
 2 
 3     public Student(String name, int age) {
 4         super(name, age);
 5     }
 6 
 7     @Override
 8     public boolean equals(Object o) {
 9         if (this == o) return true;
10         if (o == null || !(o instanceof Student)) return false;
11 
12         if (!super.equals(o)) return false;
13 
14         return super.equals(o);
15     }
16 
17 }
18 
19 public class Main {
20 
21     public static void main(String[] args) {
22         Person p = new Person("Tom", 22);
23         Student s = new Student("Tom", 22);
24 
25         System.out.println(p.equals(s)); // true
26         System.out.println(s.equals(p)); // false
27     }
28 }
子类复写

  结果显然是不正确的,这违反了equals 的对称性。看一下下面代码的结果:

1 System.out.println(p instanceof Student); // 人是学生吗?fasle
2 System.out.println(s instanceof Person); // 学生是人吗?true

  问题出现在 instanceof 关键字上,实际上用 instanceof 关键字是做不到对称性的要求的。那么,怎么办呢?
  可以用 getClass()方法取代 instanceof 运算符。getClass() 方法也是 Object 类中的一个方法,作用是返回一个对象的运行时类。
  代码示例:修改Person类的 equals 方法

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

  getClass()与 instanceof 的使用界定!
  使用 getClass() 不符合多态的定义,比如 AbstractSet 抽象类,它有两个子类 TreeSet 和 HashSet,他们分别使用不同的算法实现查找集合的操作,但无论集合采用哪种方式实现,都需要拥有对两个集合进行比较的功能,如果使用 getClass() 实现equals方法的重写,那么就不能在两个不同子类的对象进行相等的比较。而且集合类比较特殊,其子类是不需要自定义相等的概念的。
  源码示例:TreeSet 和 HashSet 中 equals 方法都是 AbstractSet定义

 1 public boolean equals(Object o) {
 2     if (o == this)
 3         return true;
 4 
 5     if (!(o instanceof Set))
 6         return false;
 7     Collection<?> c = (Collection<?>) o;
 8     if (c.size() != size())
 9         return false;
10     try {
11         return containsAll(c);
12     } catch (ClassCastException unused)   {
13         return false;
14     } catch (NullPointerException unused) {
15         return false;
16     }
17 }
源码示例

  他两的使用有如下建议:
  ①、如果子类要拥有自己的相等概念,则对称性需求将强制采用 getClass 进行检测。
  ②、如果由父类决定相等的概念,那么就可以使用 instanceof 进行检测,这样可以在不同的子类的对象之间进行相等的比较。
  最后请注意:无论何时重写 equals 方法,通常都必须重写hashCode方法,以维护hashCode方法的一般约定,该方法声明相等对象必须具有相同的哈希值。关于这个,介绍有关集合的源码时,会重点说明!

3、hashCode()方法

  源码示例:

1 public native int hashCode();

  这是一个用 native 修饰的方法,本地方法。作用是返回对象的散列码,是 int 类型的数值。
  关于这个方法,介绍有关集合的源码时,会重点说明!

4、getClass()方法

  源码示例:

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

  这是一个用 native 修饰的方法,本地方法。关于此关键字,详解请参看JVM系列。它是由操作系统实现,该方法的作用是返回一个对象的运行时类对象。
  虽然,此方法返回的是一个 Class<?> ,但是实际是:

1 final Class<? extends String> aClass = "".getClass();

  类型为 T 的变量 getClass 方法,返回值类型其实是Class<? extends T>,而不是方法声明中的Class<?>。
  在官方文档中也有说明:

  https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#getClass--

5、toString()方法

  源码示例:

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

  getClass().getName():返回对象的全类名
  Integer.toHexString(hashCode()):以16进制无符号整数形式返回此哈希码的字符串表示形式。
  打印某个对象时,默认是调用 toString 方法,比如 System.out.println(person),等价于 System.out.println(person.toString())。

6、notify()/notifyAll()/wait() 方法

  用于多线程之间的通信方法,详情请阅读多线程相关知识。

7、finalize 方法

  该方法用于垃圾回收,一般由 JVM 自动调用,不需要程序员去手动调用该方法。详情请阅读 JVM 系列。

8、registerNatives 方法 

  源码示例:

1 private static native void registerNatives();
2 static {
3     registerNatives();
4 }

  这是一个本地方法,一个类定义了本地方法后,想要调用操作系统的实现,必须还要装载本地库。这里是通过静态代码块来完成本地库的载入代码。
  静态代码块是一个类在初始化过程中必定会执行的内容(详细请阅读JVM相关知识),所以在类加载的时候会执行该方法,通过该方法来注册本地方法。
  在官方文档中也有说明:

  https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html

posted @ 2020-10-28 23:20  Craftsman-L  阅读(275)  评论(0编辑  收藏  举报