1. 程式人生 > >jdk1.8原始碼解析一:Object類

jdk1.8原始碼解析一:Object類

  Object 類屬於 java.lang 包,此包下的所有類在使用時無需手動匯入,系統會在程式編譯期間自動匯入。Object 類是所有類的基類,當一個類沒有直接繼承某個類時,預設繼承Object類,也就是說任何類都直接或間接繼承此類,Object 類中能訪問的方法在所有類中都可以呼叫,下面我們會分別介紹Object 類中的所有方法。

1、Object 類的結構圖

   

  Object.class類

/*
 * Copyright (c) 1994, 2012, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 */

package java.lang;

/**
 * Class {@code Object} is the root of the class hierarchy.
 * Every class has {@code Object} as a superclass. All objects,
 * including arrays, implement the methods of this class.
 *
 * @author  unascribed
 * @see     java.lang.Class
 * @since   JDK1.0
 */
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 > 0) {
            timeout++;
        }
        wait(timeout);
    }
    
    public final void wait() throws InterruptedException {
        wait(0);
    }
    
    protected void finalize() throws Throwable { }
}

2、 為什麼java.lang包下的類不需要手動匯入?

  不知道大家注意到沒,我們在使用諸如Date類時,需要手動匯入import java.util.Date,再比如使用File類時,也需要手動匯入import java.io.File。但是我們在使用Object類,String 類,Integer類等不需要手動匯入,而能直接使用,這是為什麼呢?

  這裡先告訴大家一個結論:使用 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)和使用者類路徑下去定位需要匯入的類,而這些目錄進僅僅是給出了類的頂層目錄,編譯器的類檔案定位方法大致可以理解為如下公式: 

1

頂層路徑名 \ 包名 \ 檔名.class = 絕對路徑

  單型別匯入我們知道包名和檔名,所以編譯器可以一次性查詢定位到所要的類檔案。按需型別匯入則比較複雜,編譯器會把包名和檔名進行排列組合,然後對所有的可能性進行類檔案查詢定位。例如:  

package com;
 
import java.io.*;
 
import java.util.*;

如果我們檔案中使用到了 File 類,那麼編譯器會根據如下幾個步驟來進行查詢 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程式碼的編譯速度。所以我們在編碼時最好是使用單型別匯入,這樣不僅能提高編譯速度,也能避免命名衝突。

講清楚Java的兩種導包型別了,我們在回到為什麼可以直接使用 Object 類,看到上面查詢類檔案的第③步,編譯器會自動匯入 java.lang 包,那麼當然我們能直接使用了。至於原因,因為用的多,提前載入了,省資源。

3、類構造器

  我們知道類構造器是建立Java物件的途徑之一,通過new 關鍵字呼叫構造器完成物件的例項化,還能通過構造器對物件進行相應的初始化。一個類必須要有一個構造器的存在,如果沒有顯示宣告,那麼系統會預設創造一個無參構造器,在JDK的Object類原始碼中,是看不到構造器的,系統會自動新增一個無參構造器。我們可以通過:

   Object obj = new Object();構造一個Object類的物件。

4、equals 方法

  通常很多面試題都會問 equals() 方法和 == 運算子的區別,== 運算子用於比較基本型別的值是否相同,或者比較兩個物件的引用是否相等,而 equals 用於比較兩個物件是否相等,這樣說可能比較寬泛,兩個物件如何才是相等的呢?這個標尺該如何定?

  我們可以看看 Object 類中的equals 方法:

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

 可以看到,在 Object 類中,== 運算子和 equals 方法是等價的,都是比較兩個物件的引用是否相等,從另一方面來講,如果兩個物件的引用相等,那麼這兩個物件一定是相等的。對於我們自定義的一個物件,如果不重寫 equals 方法,那麼在比較物件的時候就是呼叫 Object 類的 equals 方法,也就是用 == 運算子比較兩個物件。我們可以看看 String 類中的重寫的 equals 方法:

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

String 是引用型別,比較時不能比較引用是否相等,重點是字串的內容是否相等。所以 String 類定義兩個物件相等的標準是字串內容都相同

  在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 類,然後重寫其equals 方法,比較兩個 Person 物件:

package com.ys.bean;
/**
 * Create by vae
 */
public class Person {
    private String pname;
    private int page;

    public Person(){}

    public Person(String pname,int page){
        this.pname = pname;
        this.page = page;
    }
    public int getPage() {
        return page;
    }

    public void setPage(int page) {
        this.page = page;
    }

    public String getPname() {
        return pname;
    }

    public void setPname(String pname) {
        this.pname = pname;
    }
    @Override
    public boolean equals(Object obj) {
        if(this == obj){//引用相等那麼兩個物件當然相等
            return true;
        }
        if(obj == null || !(obj instanceof  Person)){//物件為空或者不是Person類的例項
            return false;
        }
        Person otherPerson = (Person)obj;
        if(otherPerson.getPname().equals(this.getPname()) && otherPerson.getPage()==this.getPage()){
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        Person p1 = new Person("Tom",21);
        Person p2 = new Person("Marry",20);
        System.out.println(p1==p2);//false
        System.out.println(p1.equals(p2));//false

        Person p3 = new Person("Tom",21);
        System.out.println(p1.equals(p3));//true
    }

}

通過重寫 equals 方法,我們自定義兩個物件相等的標尺為Person物件的兩個屬性都相等,則物件相等,否則不相等。如果不重寫 equals 方法,那麼始終是呼叫 Object 類的equals 方法,也就是用 == 比較兩個物件在棧記憶體中的引用地址是否相等。

   這時候有個Person 類的子類 Man,也重寫了 equals 方法:

package com.ys.bean;
/**
 * Create by vae
 */
public class Man extends Person{
    private String sex;

    public Man(String pname,int page,String sex){
        super(pname,page);
        this.sex = sex;
    }
    @Override
    public boolean equals(Object obj) {
        if(!super.equals(obj)){
            return false;
        }
        if(obj == null || !(obj instanceof  Man)){//物件為空或者不是Person類的例項
            return false;
        }
        Man man = (Man) obj;
        return sex.equals(man.sex);
    }

    public static void main(String[] args) {
        Person p = new Person("Tom",22);
        Man m = new Man("Tom",22,"男");

        System.out.println(p.equals(m));//true
        System.out.println(m.equals(p));//false
    }
}

通過列印結果我們發現 person.equals(man)得到的結果是 true,而man.equals(person)得到的結果卻是false,這顯然是不正確的。

  Man 是 Person 的子類,person instanceof Man 結果當然是false。這違反了我們上面說的對稱性。

  實際上用 instanceof 關鍵字是做不到對稱性的要求的。這裡推薦做法是用 getClass()方法取代 instanceof 運算子。getClass() 關鍵字也是 Object 類中的一個方法,作用是返回一個物件的執行時類,下面我們會詳細講解。

那麼 Person 類中的 equals 方法為:

public boolean equals(Object obj) {
        if(this == obj){//引用相等那麼兩個物件當然相等
            return true;
        }
        if(obj == null || (getClass() != obj.getClass())){//物件為空或者不是Person類的例項
            return false;
        }
        Person otherPerson = (Person)obj;
        if(otherPerson.getPname().equals(this.getPname()) && otherPerson.getPage()==this.getPage()){
            return true;
        }
        return false;
    }

列印結果 person.equals(man)得到的結果是 false,man.equals(person)得到的結果也是false,滿足對稱性。

  注意:使用 getClass 不是絕對的,要根據情況而定,畢竟定義物件是否相等的標準是由程式設計師自己定義的。而且使用 getClass 不符合多型的定義,比如 AbstractSet 抽象類,它有兩個子類 TreeSet 和 HashSet,他們分別使用不同的演算法實現查詢集合的操作,但無論集合採用哪種方式實現,都需要擁有對兩個集合進行比較的功能,如果使用 getClass 實現equals方法的重寫,那麼就不能在兩個不同子類的物件進行相等的比較。而且集合類比較特殊,其子類是不需要自定義相等的概念的。

  所以什麼時候使用 instanceof 運算子,什麼時候使用 getClass() 有如下建議:

  ①、如果子類能夠擁有自己的相等概念,則對稱性需求將強制採用 getClass 進行檢測。

  ②、如果有超類決定相等的概念,那麼就可以使用 instanceof 進行檢測,這樣可以在不同的子類的物件之間進行相等的比較。

下面給出一個完美的 equals 方法的建議:

  1、顯示引數命名為 otherObject,稍後會將它轉換成另一個叫做 other 的變數。

  2、判斷比較的兩個物件引用是否相等,如果引用相等那麼表示是同一個物件,那麼當然相等

  3、如果 otherObject 為 null,直接返回false,表示不相等

  4、比較 this 和 otherObject 是否是同一個類:如果 equals 的語義在每個子類中有所改變,就使用 getClass 檢測;如果所有的子類都有統一的定義,那麼使用 instanceof 檢測

  5、將 otherObject 轉換成對應的類型別變數

  6、最後對物件的屬性進行比較。使用 == 比較基本型別,使用 equals 比較物件。如果都相等則返回true,否則返回false。注意如果是在子類中定義equals,則要包含 super.equals(other)

  下面我們給出 Person 類中完整的 equals 方法的書寫:

@Override
    public boolean equals(Object otherObject) {
        //1、判斷比較的兩個物件引用是否相等,如果引用相等那麼表示是同一個物件,那麼當然相等
        if(this == otherObject){
            return true;
        }
        //2、如果 otherObject 為 null,直接返回false,表示不相等
        if(otherObject == null ){//物件為空或者不是Person類的例項
            return false;
        }
        //3、比較 this 和 otherObject 是否是同一個類(注意下面兩個只能使用一種)
        //3.1:如果 equals 的語義在每個子類中所有改變,就使用 getClass 檢測
        if(this.getClass() != otherObject.getClass()){
            return false;
        }
        //3.2:如果所有的子類都有統一的定義,那麼使用 instanceof 檢測
        if(!(otherObject instanceof Person)){
            return false;
        }

        //4、將 otherObject 轉換成對應的類型別變數
        Person other = (Person) otherObject;

        //5、最後對物件的屬性進行比較。使用 == 比較基本型別,使用 equals 比較物件。如果都相等則返回true,否則返回false
        //   使用 Objects 工具類的 equals 方法防止比較的兩個物件有一個為 null而報錯,因為 null.equals() 是會拋異常的
        return Objects.equals(this.pname,other.pname) && this.page == other.page;

        //6、注意如果是在子類中定義equals,則要包含 super.equals(other)
        //return super.equals(other) && Objects.equals(this.pname,other.pname) && this.page == other.page;

    }

請注意,無論何時重寫此方法,通常都必須重寫hashCode方法,以維護hashCode方法的一般約定,該方法宣告相等物件必須具有相同的雜湊程式碼。hashCode 也是 Object 類中的方法,後面會詳細講解。

5、getClass 方法 

  上面我們在介紹 equals 方法時,介紹如果 equals 的語義在每個子類中有所改變,那麼使用 getClass 檢測,為什麼這樣說呢?

  getClass()在 Object 類中如下,作用是返回物件的執行時類。

public final native Class<?> getClass();

  這裡我們要知道用 native 修飾的方法我們不用考慮,由作業系統幫我們實現,該方法的作用是返回一個物件的執行時類,通過這個類物件我們可以獲取該執行時類的相關屬性和方法。也就是Java中的反射,各種通用的框架都是利用反射來實現的,這裡我們不做詳細的描述。

  這裡詳細的介紹 getClass 方法返回的是一個物件的執行時類物件,這該怎麼理解呢?Java中還有一種這樣的用法,通過 類名.class 獲取這個類的類物件 ,這兩種用法有什麼區別呢?

父類:Parent.class

public class Parent {}

子類:Son.class

public class Son extends Parent{}

測試:

@Test
public void testClass(){
    Parent p = new Son();
    System.out.println(p.getClass());
    System.out.println(Parent.class);
}

列印結果:

  

  結論:class 是一個類的屬性,能獲取該類編譯時的類物件,而 getClass() 是一個類的方法,它是獲取該類執行時的類物件。

  還有一個需要大家注意的是,雖然Object類中getClass() 方法宣告是:public final native Class<?> getClass();返回的是一個 Class<?>,但是如下是能通過編譯的:

Class<? extends String> c = "".getClass();

也就是說型別為T的變數getClass方法的返回值型別其實是Class<? extends T>而非getClass方法宣告中的Class<?>。

6、hashCode 方法

  hashCode 在 Object 類中定義如下:

public native int hashCode();

這也是一個用 native 宣告的本地方法,作用是返回物件的雜湊碼,是 int 型別的數值。

  那麼這個方法存在的意義是什麼呢?

  我們知道在Java 中有幾種集合類,比如 List,Set,還有 Map,List集合一般是存放的元素是有序可重複的,Set 存放的元素則是無序不可重複的,而 Map 集合存放的是鍵值對。

  前面我們說過判斷一個元素是否相等可以通過 equals 方法,沒增加一個元素,那麼我們就通過 equals 方法判斷集合中的每一個元素是否重複,但是如果集合中有10000個元素了,但我們新加入一個元素時,那就需要進行10000次equals方法的呼叫,這顯然效率很低。

  於是,Java 的集合設計者就採用了 雜湊表 來實現。關於雜湊表的資料結構我有過介紹。雜湊演算法也稱為雜湊演算法,是將資料依特定演算法產生的結果直接指定到一個地址上。這個結果就是由 hashCode 方法產生。這樣一來,當集合要新增新的元素時,先呼叫這個元素的 hashCode 方法,就一下子能定位到它應該放置的物理位置上。

  ①、如果這個位置上沒有元素,它就可以直接儲存在這個位置上,不用再進行任何比較了;

  ②、如果這個位置上已經有元素了,就呼叫它的equals方法與新元素進行比較,相同的話就不存了;

  ③、不相同的話,也就是發生了Hash key相同導致衝突的情況,那麼就在這個Hash key的地方產生一個連結串列,將所有產生相同HashCode的物件放到這個單鏈表上去,串在一起(很少出現)。這樣一來實際呼叫equals方法的次數就大大降低了,幾乎只需要一兩次。

  

  這裡有 A,B,C,D四個物件,分別通過 hashCode 方法產生了三個值,注意 A 和 B 物件呼叫 hashCode 產生的值是相同的,即 A.hashCode() = B.hashCode() = 0x001,發生了雜湊衝突,這時候由於最先是插入了 A,在插入的B的時候,我們發現 B 是要插入到 A 所在的位置,而 A 已經插入了,這時候就通過呼叫 equals 方法判斷 A 和 B 是否相同,如果相同就不插入 B,如果不同則將 B 插入到 A 後面的位置。所以對於 equals 方法和 hashCode 方法有如下要求:

  一、hashCode 要求

  ①、在程式執行時期間,只要物件的(欄位的)變化不會影響equals方法的決策結果,那麼,在這個期間,無論呼叫多少次hashCode,都必須返回同一個雜湊碼。

  ②、通過equals呼叫返回true 的2個物件的hashCode一定一樣。

  ③、通過equasl返回false 的2個物件的雜湊碼不需要不同,也就是他們的hashCode方法的返回值允許出現相同的情況。

  因此我們可以得到如下推論:

  兩個物件相等,其 hashCode 一定相同;

  兩個物件不相等,其 hashCode 有可能相同;

  hashCode 相同的兩個物件,不一定相等;

  hashCode 不相同的兩個物件,一定不相等;

   這四個推論通過上圖可以更好的理解。

  可能會有人疑問,對於不能重複的集合,為什麼不直接通過 hashCode 對於每個元素都產生唯一的值,如果重複就是相同的值,這樣不就不需要呼叫 equals 方法來判斷是否相同了嗎?   實際上對於元素不是很多的情況下,直接通過 hashCode 產生唯一的索引值,通過這個索引值能直接找到元素,而且還能判斷是否相同。比如資料庫儲存的資料,ID 是有序排列的,我們能通過 ID 直接找到某個元素,如果新插入的元素 ID 已經有了,那就表示是重複資料,這是很完美的辦法。但現實是儲存的元素很難有這樣的 ID 關鍵字,也就很難這種實現 hashCode 的唯一演算法,再者就算能實現,但是產生的 hashCode 碼是非常大的,這會大的超過 Java 所能表示的範圍,很佔記憶體空間,所以也是不予考慮的。

  二、hashCode 編寫指導:

  ①、不同物件的hash碼應該儘量不同,避免hash衝突,也就是演算法獲得的元素要儘量均勻分佈。

  ②、hash 值是一個 int 型別,在Java中佔用 4 個位元組,也就是 232 次方,要避免溢位。

  在 JDK 的 Integer類,Float 類,String 類等都重寫了 hashCode 方法,我們自定義物件也可以參考這些類來寫。

  下面是 JDK String 類的hashCode 原始碼:

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

再次提醒大家,對於 Map 集合,我們可以選取Java中的基本型別,還有引用型別 String 作為 key,因為它們都按照規範重寫了 equals 方法和 hashCode 方法。但是如果你用自定義物件作為 key,那麼一定要覆寫 equals 方法和 hashCode 方法,不然會有意想不到的錯誤產生。

7、toString 方法

  該方法在 JDK 的原始碼如下:

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

 getClass().getName()是返回物件的全類名(包含包名),Integer.toHexString(hashCode()) 是以16進位制無符號整數形式返回此雜湊碼的字串表示形式。

  列印某個物件時,預設是呼叫 toString 方法,比如 System.out.println(person),等價於 System.out.println(person.toString())

8、notify()/notifyAll()/wait()

  這是用於多執行緒之間的通訊方法,在後面講解多執行緒會詳細描述,這裡就不做講解了。

9、finalize 方法

protected void finalize() throws Throwable { }

該方法用於垃圾回收,一般由 JVM 自動呼叫,一般不需要程式設計師去手動呼叫該方法。後面再講解 JVM 的時候會詳細展開描述

10、registerNatives 方法 

  該方法在 Object 類中定義如下:

private static native void registerNatives();

這是一個本地方法,在 native 介紹 中我們知道一個類定義了本地方法後,想要呼叫作業系統的實現,必須還要裝載本地庫,但是我們發現在 Object.class 類中具有很多本地方法,但是卻沒有看到本地庫的載入程式碼。而且這是用 private 關鍵字宣告的,在類外面根本調用不了,我們接著往下看關於這個方法的類似原始碼:

    static {
        registerNatives();
    }

   看到上面的程式碼,這就明白了吧。靜態程式碼塊就是一個類在初始化過程中必定會執行的內容,所以在類載入的時候是會執行該方法的,通過該方法來註冊本地方法。