• 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

    作者:Craftsman-L

    本博客所有文章仅用于学习、研究和交流目的,版权归作者所有,欢迎非商业性质转载。

    如果本篇博客给您带来帮助,请作者喝杯咖啡吧!点击下面打赏,您的支持是我最大的动力!

  • 相关阅读:
    关于浏览器缓存
    JavaScript基本概念(数组)
    JavaScript基本概念(对象)
    变量作用域
    Javascript基本概念(语句和函数)
    JavaScript基本概念(操作符)
    JavaScript基本概念(变量和数据类型)
    博客园主题美化,修改主题
    一种简易的表达式求值算法
    Go实现的一个命令行HTTP抓包工具
  • 原文地址:https://www.cnblogs.com/originator/p/13894204.html
Copyright © 2020-2023  润新知