Java Object 類 深入分析
目錄
2.private static native void registerNatives();
3.protected native Object clone() throws CloneNotSupportedException;
4.public final native Class getClass();
5.public boolean equals(Object obj);
6.public native int hashCode();
8. wait(...) / notify() / notifyAll()
13. protected void finalize();
Object類位於java.lang包中,java.lang包包含著Java最基礎和核心的類,在編譯時會自動匯入。Object類沒有定義屬性,一共有13個方法,具體的方法如下圖:
1.類構造器public Object();
大部分情況下,Java中通過形如 new A(args..)形式建立一個屬於該型別的物件。其中A即是類名,A(args..)即此類定義中相對應的建構函式。通過此種形式建立的物件都是通過類中的建構函式完成。為體現此特性,Java中規定:在類定義過程中,對於未定義建構函式的類,預設會有一個無引數的建構函式,作為所有類的基類,Object類自然要反映出此特性,在原始碼中,未給出Object類建構函式定義,但實際上,此建構函式是存在的。
當然,並不是所有的類都是通過此種方式去構建,也自然的,並不是所有的類建構函式都是public。
2.private static native void registerNatives();
registerNatives函式前面有native關鍵字修飾,Java中,用native關鍵字修飾的函式表明該方法的實現並不是在Java中去完成,而是由C/C++去完成,並被編譯成了.dll,由Java去呼叫。方法的具體實現體在dll檔案中,對於不同平臺,其具體實現應該有所不同。用native修飾,即表示作業系統,需要提供此方法,Java本身需要使用。具體到registerNatives()方法本身,其主要作用是將C/C++中的方法對映到Java中的native方法,實現方法命名的解耦。
既然如此,可能有人會問,registerNatives()修飾符為private,且並沒有執行,作用何以達到?其實,在Java原始碼中,此方法的聲明後有緊接著一段靜態程式碼塊:
private static native void registerNatives();
static {
registerNatives();
}
3.protected native Object clone() throws CloneNotSupportedException;
看,clone()方法又是一個被宣告為native的方法,因此,我們知道了clone()方法並不是Java的原生方法,具體的實現是有C/C++完成的。clone英文翻譯為"克隆",其目的是建立並返回此物件的一個副本。Java術語表述為:clone函式返回的是一個引用,指向的是新的clone出來的物件,此物件與原物件分別佔用不同的堆空間。
clone()的正確呼叫是需要實現Cloneable介面,如果沒有實現Cloneable介面,並且子類直接呼叫Object類的clone()方法,則會丟擲CloneNotSupportedException異常。
4.public final native Class<?> getClass();
getClass()也是一個native方法,返回的是此Object物件的類物件/執行時類物件Class<?>。效果與Object.class相同。
首先解釋下"類物件"的概念:在Java中,類是是對具有一組相同特徵或行為的例項的抽象並進行描述,物件則是此類所描述的特徵或行為的具體例項。作為概念層次的類,其本身也具有某些共同的特性,如都具有類名稱、由類載入器去載入,都具有包,具有父類,屬性和方法等。於是,Java中有專門定義了一個類,Class,去描述其他類所具有的這些特性,因此,從此角度去看,類本身也都是屬於Class類的物件。為與經常意義上的物件相區分,在此稱之為"類物件"。
5.public boolean equals(Object obj);
==與equals在Java中經常被使用,大家也都知道==與equals的區別:
==表示的是變數值完成相同(對於基礎型別,地址中儲存的是值,引用型別則儲存指向實際物件的地址);
equals表示的是物件的內容完全相同,此處的內容多指物件的特徵/屬性。
實際上,上面說法是不嚴謹的,更多的只是常見於String類中。首先看一下Object類中關於equals()方法的定義:
public boolean equals(Object obj) {
return (this == obj);
}
如上,在object類中,此標尺即為==。當然,這個標尺不是固定的,其他類中可以按照實際的需要對此標尺含義進行重定義。如String類中則是依據字串內容是否相等來重定義了此標尺含義。如此可以增加類的功能型和實際編碼的靈活性。當然了,如果自定義的類沒有重寫equals()方法來重新定義此標尺,那麼預設的將是其父類的equals(),直到object基類。
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;
}
6.public native int hashCode();
hashCode()方法返回一個整形數值,表示該物件的雜湊碼值。
hashCode()具有如下約定:
1).在Java應用程式程式執行期間,對於同一物件多次呼叫hashCode()方法時,其返回的雜湊碼是相同的,前提是將物件進行equals比較時所用的標尺資訊未做修改。在Java應用程式的一次執行到另外一次執行,同一物件的hashCode()返回的雜湊碼無須保持一致;
2).如果兩個物件相等(依據:呼叫equals()方法),那麼這兩個物件呼叫hashCode()返回的雜湊碼也必須相等;
3).反之,兩個物件呼叫hasCode()返回的雜湊碼相等,這兩個物件不一定相等。
即嚴格的數學邏輯表示為: 兩個物件相等 <=> equals()相等 => hashCode()相等。因此,重寫equlas()方法必須重寫hashCode()方法,以保證此邏輯嚴格成立,同時可以推理出:hasCode()不相等 => equals()不相等 <=> 兩個物件不相等。
可能有人在此產生疑問:既然比較兩個物件是否相等的唯一條件(也是衝要條件)是equals,那麼為什麼還要弄出一個hashCode(),並且進行如此約定,弄得這麼麻煩?
其實,這主要體現在hashCode()方法的作用上,其主要用於增強雜湊表的效能。
以集合類中,以Set為例,當新加一個物件時,需要判斷現有集合中是否已經存在與此物件相等的物件,如果沒有hashCode()方法,需要將Set進行一次遍歷,並逐一用equals()方法判斷兩個物件是否相等,此種演算法時間複雜度為o(n)。通過藉助於hasCode方法,先計算出即將新加入物件的雜湊碼,然後根據雜湊演算法計算出此物件的位置,直接判斷此位置上是否已有物件即可。(注:Set的底層用的是Map的原理實現)
在此需要糾正一個理解上的誤區:物件的hashCode()返回的不是物件所在的實體記憶體地址。甚至也不一定是物件的邏輯地址,hashCode()相同的兩個物件,不一定相等,換言之,不相等的兩個物件,hashCode()返回的雜湊碼可能相同。
因此,在上述程式碼中,重寫了equals()方法後,需要重寫hashCode()方法。
String類中的hashcode實現(預設值是0):
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;
}
上述hashCode()的重寫中出現了h*31,是因為h*31 = (h<<5) - h。之所以選擇31,是因為左移運算和減運算計算效率遠大於乘法運算。當然,也可以選擇其他數字。
7.public String toString();
Object 類中的實現:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
getClass()返回物件的類物件,getClassName()以String形式返回類物件的名稱(含包名)。Integer.toHexString(hashCode())則是以物件的雜湊碼為實參,以16進位制無符號整數形式返回此雜湊碼的字串表示形式。
toString()是由物件的型別和其雜湊碼唯一確定,同一型別但不相等的兩個物件分別呼叫toString()方法返回的結果可能相同。
8. wait(...) / notify() / notifyAll()
這幾個方法主要用於java多執行緒之間的協作。先具體看下這幾個方法的主要含義:
wait():呼叫此方法所在的當前執行緒等待,直到在其他執行緒上呼叫此方法的主調(某一物件)的notify()/notifyAll()方法。
wait(long timeout)/wait(long timeout, int nanos):呼叫此方法所在的當前執行緒等待,直到在其他執行緒上呼叫此方法的主調(某一物件)的notisfy()/notisfyAll()方法,或超過指定的超時時間量。
notify()/notifyAll():喚醒在此物件監視器上等待的單個執行緒/所有執行緒。
wait(...) / notify() | notifyAll()一般情況下都是配套使用。
看一個簡單的例子:
package com.qqyumidi;
public class ThreadTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
synchronized (r) {
try {
System.out.println("main thread 等待t執行緒執行完");
r.wait();
System.out.println("被notity喚醒,得以繼續執行");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println("main thread 本想等待,但被意外打斷了");
}
System.out.println("執行緒t執行相加結果" + r.getTotal());
}
}
}
class MyRunnable implements Runnable {
private int total;
@Override
public void run() {
// TODO Auto-generated method stub
synchronized (this) {
System.out.println("Thread name is:" + Thread.currentThread().getName());
for (int i = 0; i < 10; i++) {
total += i;
}
notify();
System.out.println("執行notif後同步程式碼塊中依然可以繼續執行直至完畢");
}
System.out.println("執行notif後且同步程式碼塊外的程式碼執行時機取決於執行緒排程");
}
public int getTotal() {
return total;
}
}
輸出結果:
main thread 等待t執行緒執行完
Thread name is:Thread-0
執行notif後同步程式碼塊中依然可以繼續執行直至完畢
執行notif後且同步程式碼塊外的程式碼執行時機取決於執行緒排程 //此行輸出位置有具體的JVM執行緒排程決定,有可能最後執行
被notity喚醒,得以繼續執行
執行緒t執行相加結果45
從上述例子的輸出結果中可以得出如下結論:
1、wait(...)方法呼叫後當前執行緒將立即阻塞,且適當其所持有的同步程式碼塊中的鎖,直到被喚醒或超時或打斷後且重新獲取到鎖後才能繼續執行;
2、notify()/notifyAll()方法呼叫後,其所線上程不會立即釋放所持有的鎖,直到其所在同步程式碼塊中的程式碼執行完畢,此時釋放鎖,因此,如果其同步程式碼塊後還有程式碼,其執行則依賴於JVM的執行緒排程。
在Java原始碼中,可以看到wait()具體定義如下:
wait(long timeout, int nanos)方法定義內部實質上也是通過呼叫wait(long timeout)完成。而wait(long timeout)是一個native方法。因此,wait(...)方法本質上都是native方式實現。
notify()/notifyAll()方法也都是native方法。
Java中執行緒具有較多的知識點,是一塊比較大且重要的知識點。
13. protected void finalize();
Object類中的定義:
protected void finalize() throws Throwable { }
Object中定義finalize方法表明Java中每一個物件都將具有finalize這種行為,其具體呼叫時機在:JVM準備對此對形象所佔用的記憶體空間進行垃圾回收前,將被呼叫。由此可以看出,此方法並不是由我們主動去呼叫的。通常我們知道物件建立完,要做清理操作,當然GC會清理哪些由new分配的記憶體,但是如果不是通過new分配的記憶體清理的話,就需要呼叫finalize方法.
工作原理:一旦垃圾回收器準備釋放物件佔用的記憶體空間,將首先呼叫finalize方法,並且在下一次垃圾回收動作發生,才會真正的回收該物件佔用的記憶體,也就是finalize方法會在垃圾回收器真正回收物件之前呼叫
- finalize方法通常是用於清理一些非本地方法(native),和一些物件安全釋放校驗的操作
- 垃圾回收記憶體是JVM的一個不確定操作,通常會在系統瀕臨記憶體溢位可能會回收,所以你不要指望垃圾回收來控制finalize方法的呼叫