天天看點

Java的Object類九大方法

Java的Object 類的完整路徑是java.lang.Object ,是所有類的父類編譯,當我們建立一個類時,如果沒有明确繼承一個父類,那麼它就會自動繼承 Object,成為 Object 的子類(隐式繼承)。Object類有九大常用方法,分别是getClass()、finalize()、toString()、equals()、hashcode()、wait()、notify()、notifyAll()和clone()。

一、getClass()

首先,getClass()方法用于擷取一個對象的運作時類(Class),進而通過傳回的Class對象擷取Person的相關資訊,比如擷取該類的構造方法、該類有哪些方法、該類有哪些成員變量等資訊。不同VM針對Class做了不同的優化,是以getClass()的實作也并不相同:

這是因為是Java預設的Hotspot虛拟機并沒有開辟單獨的Method Area空間,而是有GC Heap的老生代的Metaspace實作的。而Android采用ART VM,這才造成了這種差異。想深入了解不同VM的實作的運作時資料分區、ClassLoader和Class類要讀很多書,本文不做過多讨論。

二、finalize()

finalize()是Object的protected方法,在發生GC時觸發該方法,大緻流程是當對象變成GC Roots不可達時,GC判斷該對象是否覆寫了finalize()方法,若未覆寫,則直接将其回收。否則,若對象未執行過finalize()方法,将其放入F-Queue隊列,由一低優先級線程執行該隊列中對象的finalize()方法。執行finalize()方法完畢後,GC會再次判斷該對象是否可達,若不可達,則進行回收;否則,對象“複活”。

子類可以override方法以實作(1)防止對象被回收、(2)防止對象不被回收。防止對象被回收隻需讓該對象與GC ROOTS之間存在可達鍊即可。我們重點看看FileInputStream、FileOutputStream、Connection等類怎麼防止使用者忘記釋放資源呢,如下是FileInputStream的部分源碼:

三、toString()

toString()方法傳回該對象的String表示,這也是連每個初級程式員都很熟悉的一個方法了。你仔細讀過Java的源碼,就知道很多類的toString()方法都是精雕細琢的,就像Integer的toString()方法,就針對Android做了一定适配:

在實際開發中,複雜對象的toString()方法用Gson生成JSON來實作。

四、equals()和hashcode()

equals()方法和hashcode()方法,我要放在一起說。

一般來說==比較的是引用是否相同,而equals()則是需要重寫來比較值是否相同。重寫equals()要注意以下幾點注意事項:

(1)對任意x,x.equals(x)一定傳回true

(2)對任意x,y,如果x.equals(y)傳回true,則y.equals(x)也一定傳回true

(3)對任意x,y,z,如果x.equals(y)傳回true,y.equals(z)也傳回true,則x.equals(z)也一定傳回true

(4)對任意x,y,如果對象中用于比較的資訊沒有改變,那麼無論調用多少次x.equals(y),傳回的結果應該保持一緻,要麼一直傳回true,要麼一直傳回false

(5)對任意不是null的x,x.equals(null)一定傳回false,如果兩個對象equals()方法相等則它們的hashCode傳回值一定要相同。我們先看一下String是如何實作equals()的:

我們再看一下String是如何實作hashcode()的:

反之,如果兩個對象的hashCode傳回值相同,它們的equals()方法可以不傳回true。這種情況叫做hash碰撞。HashMap處理hash碰撞的方法叫鍊位址法,除此以外hash碰撞還可以用ArrayMap采用的開放位址法解決,這些不在今天的話題讨論範圍之内,不做贅述。

五、wait()、notify()和notifyAll()

wait()、notify()和notifyAll()三個方法實作了Java的wait-notify機制。

先看wait()方法,wait()方法用來讓持有此對象的螢幕的線程處于阻塞狀态,有參數不同的三個同名方法:

ns是納秒的意思,1s == 1,000,000,000ns,光速是世界上最快的速度,光在1ns時間内僅能傳播0.3m。一般PC的CPU計算一道簡單指令,比如2+3=5的時間為2~4ns。我們一般隻用一個參數的wait()方法或者無參方法就足夠了,第二個參數在現實開發中幾乎用不到。

含參的wait()方法調用後,線程可以在等待時間結束後進入就緒狀态(以下簡稱“喚醒”);無參的wait()方法調用後,則必須等待持有該對象螢幕的線程主動調用notify()或notifyAll()方法後才能被喚醒。差別在于notify()方法喚醒在此對象螢幕上等待的單個線程,如果所有線程都在此對象上等待,則會随機喚醒其中一個線程;而notifyAll()方法則喚醒在此對象螢幕上等待的所有線程。

wait()、notify()和notifyAll()都是final native方法,我們暫時不需要深入了解内部是怎樣實作的,我們隻要知道這就是Java的等待-通知(wait-notify)機制,學習它們的應用場景就好了。

打個比方:

(1)用人機關決定錄用程式員的時間是不确定的,比如可能要面試好多人,需要綜合考慮,不能及時回報

(2)每個程式員面試結束後需要wait()

(3)用人機關綜合考慮之後覺得最合适的程式員,讓HR notify()

(4)最後被選中程式員高高興興去上班了,其他程式員就等着吧

在開發中,wait-notify機制的最廣泛用途就是實作生産者/消費者模型,生産者/消費者模型能解決絕大多數并發問題,通過平衡生産線程和消費線程的工作能力來提高程式的整體處理資料的速度。

使用wait-notify機制的注意事項:

(1)wait()、notify()和notifyAll()必須在synchronized修飾的方法或代碼塊中使用

(2)在while循環裡而不是if語句下使用wait(),確定線上程睡眠前後都檢查wait()觸發的條件(防止虛假喚醒)

(3)wait()方法必須在多線程共享的對象上調用

我們先定義注意(1)和(2)的一個生産者,往隊列裡添加元素:

再定義一個一模一樣的消費者,除了從隊列裡移除元素之外,其他代碼同上

最後編寫符合(3)的測試代碼:

檢視運作結果:

Produce 1604006010

Produce 1312202442

Produce -1478853208

Produce 1460408111

Produce 1802825495

Queue is Full

Consume 1604006010

Consume 1312202442

Consume -1478853208

Consume 1460408111

Consume 1802825495

Queue is Empty

除了以上介紹的用synchronized關鍵字配合Object的wait()/notity()實作,生産者-消費者模型還可以用Lock接口配合Condition的await()、signalAll()實作,此外還可以用BlockingQueue實作,但這些都不在本文的話題讨論範圍之内,就不再贅述了。

六、clone()

Java語言的Object類實作了Cloneable接口,一個對象可以通過調用Clone()方法生成對象。需要注意的是,clone()方法并不是Cloneable接口裡的,而是Object類裡的,Cloneable是一個辨別接口,辨別這個類的對象是可被拷貝的,如果沒有實作Cloneable接口卻調用了clone()方法就會報錯。

對象除了new出來和clone()出來,還可以通過反射和反序列化兩種方式産生,但這兩種方式不在我們今天的話題讨論範圍之内。

所謂原型模式,就是利用clone()生成對象的設計模式。需要提前了解一下深拷貝和淺拷貝的概念。Java中的資料類型分為基本類型和引用類型,在一個方法裡的變量如果是基本類型的話,變量就直接存儲在這個方法的棧幀裡,例如int、long等;而引用類型則在棧幀裡存儲這個變量的指針,指向堆中該實體的位址,例如String、Array等。深拷貝和淺拷貝是隻針對引用資料類型的

比如一個方法有一個基本類型參數和一個引用類型參數,在方法體裡對參數重新指派,會影響傳入的引用類型參數,而不會影響基本類型參數,因為基本類型參數是值傳遞,而引用類型是引用傳遞。需要注意的是,較真來說Java隻有值傳遞,因為Java的引用傳遞傳的是引用類型對象在堆記憶體空間的位址,引用傳遞隻是一種習慣性的說法,這個涉及到JVM和作業系統,不做過多讨論。

先定義一個使用者類:

編寫測試代碼:

Log列印結果如下:

調用前x的值:10

調用後x的值:10

調用前user的值:User{name='唐茜靖, age=18}

調用後user的值:User{name='管晨辰, age=16}

傳遞基本類型的方法(updateValue())流程圖:

Java的Object類九大方法

傳遞引用類型的方法(updateUser())流程圖:

Java的Object類九大方法

但也有例外,比如String類型和<=127的Long類型雖然也是引用類型,卻像基本類型一樣不受影響,這是因為它們會先比較常量池維護的值,這涉及VM的内容,今天不做過多讨論。淺拷貝是在按位(bit)拷貝對象,這個對象有着原始對象屬性值的一份精确拷貝。我們結合應用場景分析一下,還是剛才的User類,我們增加一個存放位址的内部類Address,我們需要使用者資訊可以被其他module查詢,但是不允許它們被其他module修改,新增代碼如下:

我們可以注意到Address還是指向以前的引用,淺拷貝會帶來資料安全方面的隐患,這就到了需要深拷貝的時候了。對于有多層對象的,每個對象都需要實作 Cloneable 并重寫 clone() 方法,才可以實作了對象的串行層層拷貝。就像這樣:

至于徹底深拷貝幾乎是不可能實作的,不但可能存在引用關系非常複雜的情況,也可能存在引用鍊的某一級上引用了一個沒有實作Cloneable接口的第三方對象的情況。

最後總結一下,原型模式的用途之一是保護性拷貝,防止外部對隻讀對象進行修改,剛才我舉的例子就是保護性拷貝。另一個重要用途則是解決建構複雜對象的資源消耗問題,提升建立對象的效率,這是因為clone()方法的原理是在記憶體中拷貝二進制流,比new一個對象的性能好很多,非常适用于需要在循環體内産生大量對象的時候。絕大多數設計模式都是犧牲性能提升開發效率的,原型模式是為數不多的犧牲開發效率提升性能的。我們做一下new和clone的對比:

通過ASM工具檢視bytecode,可以對比出二者對棧資源的消耗:

// access flags 0x1

  public testNew()V

   ……省略……

    MAXSTACK = 4

    MAXLOCALS = 2

  // access flags 0x1

  public testClone()V

    MAXSTACK = 1

此外還需要注意一點,拷貝不會執行構造函數,是以有時候我們需要注意這個潛在的問題。幸好這個問題不是不可避免的,這是Android第五大元件Intent的clone()的實作,沒有用拷貝:

繼續閱讀