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 |
|
單型別匯入我們知道包名和檔名,所以編譯器可以一次性查詢定位到所要的類檔案。按需型別匯入則比較複雜,編譯器會把包名和檔名進行排列組合,然後對所有的可能性進行類檔案查詢定位。例如:
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();
}
看到上面的程式碼,這就明白了吧。靜態程式碼塊就是一個類在初始化過程中必定會執行的內容,所以在類載入的時候是會執行該方法的,通過該方法來註冊本地方法。