【Java面試題】之Object類中方法詳解

JacobGo發表於2017-10-25

之前看到有人分享的面經,面試官先問Object中有什麼方法,然後再要求解釋每一次方法的作用。


先看看Object中有什麼方法



Object類是Java中所有類的基類。位於java.lang包中,一共有13個方法


方法一 Object() 即Object的構造方法

大部分情況下,Java中通過形如 new A(args..)形式建立一個屬於該型別的物件。

其中A即是類名,A(args..)即此類定義中相對應的建構函式。通過此種形式建立的物件都是通過類中的建構函式完成。為體現此特性,Java中規定:在類定義過程中,對於未定義建構函式的類,預設會有一個無引數的建構函式,作為所有類的基類,Object類自然要反映出此特性,在原始碼中,未給出Object類建構函式定義,但實際上,此建構函式是存在的。

當然,並不是所有的類都是通過此種方式去構建,也自然的,並不是所有的類建構函式都是public。


方法二 registerNatives

通常情況下,為了使JVM發現您的本機功能,他們被一定的方式命名。例如,對於java.lang.Object.registerNatives,對應的C函式命名為Java_java_lang_Object_registerNatives。通過使用registerNatives(或者更確切地說,JNI函式RegisterNatives),您可以命名任何你想要你的C函式。

這裡是相關的C程式碼(來自OpenJDK6):

static JNINativeMethod methods[] = {


{“hashCode”, “()I”, (void *)&JVM_IHashCode},


{“wait”, “(J)V”, (void *)&JVM_MonitorWait},


{“notify”, “()V”, (void *)&JVM_MonitorNotify},


{“notifyAll”, “()V”, (void *)&JVM_MonitorNotifyAll},


{“clone”, “()Ljava/lang/Object;”, (void *)&JVM_Clone},


};


JNIEXPORT void JNICALL


Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)


{


(*env)->RegisterNatives(env, cls,methods, sizeof(methods)/sizeof(methods[0]));


}



(請注意,Object.getClass不在列表上;它仍然被稱作Java_java_lang_Object_getClass的“標準”的名稱。)對於列出的功能,相關的C函式如在該表中,這比寫一堆轉發功能更加得心應手。


如果你是在C程式中嵌入Java並且想要這個程式本身內的連結到這個函式,註冊本地函式也是有用的,因為這些通常不會通過標準方法查詢機制被發現。註冊本地函式也可以用來“重新繫結”一個本地方法到另一個C函式(這會有用如果你的程式支援動態載入和解除安裝模組)。



方法三 clone

注意:clone返回的物件為淺拷貝

clone()方法同樣是一個被宣告為native的方法,因此,我們知道了clone()方法並不是Java的原生方法,具體的實現是有C/C++完成的。clone英文翻譯為"克隆",其目的是建立並返回此物件的一個副本。形象點理解,這有一輛科魯茲,你看著不錯,想要個一模一樣的。你呼叫此方法即可像變魔術一樣變出一輛一模一樣的科魯茲出來。配置一樣,長相一樣。但從此刻起,原來的那輛科魯茲如果進行了新的裝飾,與你克隆出來的這輛科魯茲沒有任何關係了。你克隆出來的物件變不變完全在於你對克隆出來的科魯茲有沒有進行過什麼操作了。Java術語表述為:clone函式返回的是一個引用,指向的是新的clone出來的物件,此物件與原物件分別佔用不同的堆空間。

明白了clone的含義後,接下來看看如果呼叫clone()函式物件進行此克隆操作。

首先看一下下面的這個例子:


public class ObjectTest {

    public static void main(String[] args) {

        Object o1 = new Object();
        // The method clone() from the type Object is not visible
        Object clone = o1.clone();
    }

}

例子很簡單,在main()方法中,new一個Oject物件後,想直接呼叫此物件的clone方法克隆一個物件,但是出現錯誤提示:"The method clone() from the type Object is not visible"

why? 根據提示,第一反應是ObjectTest類中定義的Oject物件無法訪問其clone()方法。回到Object類中clone()方法的定義,可以看到其被宣告為protected,估計問題就在這上面了,protected修飾的屬性或方法表示:在同一個包內或者不同包的子類可以訪問。顯然,Object類與ObjectTest類在不同的包中,但是ObjectTest繼承自Object,是Object類的子類,於是,現在卻出現子類中通過Object引用不能訪問protected方法,原因在於對"不同包中的子類可以訪問"沒有正確理解。

"不同包中的子類可以訪問",是指當兩個類不在同一個包中的時候,繼承自父類的子類內部且主調(呼叫者)為子類的引用時才能訪問父類用protected修飾的成員(屬性/方法)。 在子類內部,主調為父類的引用時並不能訪問此protected修飾的成員。!(super關鍵字除外)

於是,上例改成如下形式,我們發現,可以正常編譯:


public class ObjectTest {

    public static void main(String[] args) {
        ObjectTest ot1 = new ObjectTest();

        try {
            ObjectTest ot2 = (ObjectTest) ot1.clone();
        } catch (CloneNotSupportedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

是的,因為此時的主調已經是子類的引用了。

上述程式碼在執行過程中會丟擲"java.lang.CloneNotSupportedException",表明clone()方法並未正確執行完畢,問題的原因在與Java中的語法規定:

clone()的正確呼叫是需要實現Cloneable介面,如果沒有實現Cloneable介面,並且子類直接呼叫Object類的clone()方法,則會丟擲CloneNotSupportedException異常。

Cloneable介面僅是一個表示介面,介面本身不包含任何方法,用來指示Object.clone()可以合法的被子類引用所呼叫。

於是,上述程式碼改成如下形式,即可正確指定clone()方法以實現克隆。


package com.corn.objectsummary;

public class ObjectTest implements Cloneable {

    public static void main(String[] args) {

        ObjectTest ot1 = new ObjectTest();

        try {
            ObjectTest ot2 = (ObjectTest) ot1.clone();
            System.out.println("ot2:" + ot2);
        } catch (CloneNotSupportedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}


方法4 getClass()

getClass()也是一個native方法,返回的是此Object物件的類物件/執行時類物件Class<?>。效果與Object.class相同。

首先解釋下"類物件"的概念:在Java中,類是是對具有一組相同特徵或行為的例項的抽象並進行描述,物件則是此類所描述的特徵或行為的具體例項。作為概念層次的類,其本身也具有某些共同的特性,如都具有類名稱、由類載入器去載入,都具有包,具有父類,屬性和方法等。於是,Java中有專門定義了一個類,Class,去描述其他類所具有的這些特性,因此,從此角度去看,類本身也都是屬於Class類的物件。為與經常意義上的物件相區分,在此稱之為"類物件"。


方法5 equals

==與equals在Java中經常被使用,大家也都知道==與equals的區別:

==表示的是變數值完成相同(對於基礎型別,地址中儲存的是值,引用型別則儲存指向實際物件的地址);

equals表示的是物件的內容完全相同,此處的內容多指物件的特徵/屬性。

實際上,上面說法是不嚴謹的,更多的只是常見於String類中。首先看一下Object類中關於equals()方法的定義:

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


由此可見,Object原生的equals()方法內部呼叫的正是==,與==具有相同的含義。既然如此,為什麼還要定義此equals()方法?

equlas()方法的正確理解應該是:判斷兩個物件是否相等。那麼判斷物件相等的標尺又是什麼?


如上,在object類中,此標尺即為==。當然,這個標尺不是固定的,其他類中可以按照實際的需要對此標尺含義進行重定義。如String類中則是依據字串內容是否相等來重定義了此標尺含義。如此可以增加類的功能型和實際編碼的靈活性。當然了,如果自定義的類沒有重寫equals()方法來重新定義此標尺,那麼預設的將是其父類的equals(),直到object基類。

如下場景的實際業務需求,對於User bean,由實際的業務需求可知當屬性uid相同時,表示的是同一個User,即兩個User物件相等。則可以重寫equals以重定義User物件相等的標尺。


public class User {

    private int uid;
    private String name;
    private int age;

    public int getUid() {
        return uid;
    }

    public void setUid(int uid) {
        this.uid = uid;
    }

    protected String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null || !(obj instanceof User)) {
            return false;
        }
        if (((User) obj).getUid() == this.getUid()) {
            return true;
        }
        return false;
    }
}


public class ObjectTest implements Cloneable {

    public static void main(String[] args) {
        User u1 = new User();
        u1.setUid(111);
        u1.setName("張三");

        User u2 = new User();
        u2.setUid(111);
        u2.setName("張三丰");

        System.out.println(u1.equals(u2)); //返回true
    }

}


ObjectTest中列印出true,因為User類定義中重寫了equals()方法,這很好理解,很可能張三是一個人小名,張三丰才是其大名,判斷這兩個人是不是同一個人,這時只用判斷uid是否相同即可。

如上重寫equals方法表面上看上去是可以了,實則不然。因為它破壞了Java中的約定:重寫equals()方法必須重寫hasCode()方法


方法6 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()方法。



public class User {

    private int uid;
    private String name;
    private int age;

    public int getUid() {
        return uid;
    }

    public void setUid(int uid) {
        this.uid = uid;
    }

    protected String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null || !(obj instanceof User)) {
            return false;
        }
        if (((User) obj).getUid() == this.getUid()) {
            return true;
        }
        return false;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + this.getUid();
        return result;
    }
}


注:上述hashCode()的重寫中出現了result*31,是因為result*31 = (result<<5) - result。之所以選擇31,是因為左移運算和減運算計算效率遠大於乘法運算。當然,也可以選擇其他數字。


方法7 toString();

toString()方法返回該物件的字串表示。先看一下Object中的具體方法體:

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


toString()方法相信大家都經常用到,即使沒有顯式呼叫,但當我們使用System.out.println(obj)時,其內部也是通過toString()來實現的。

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

如上例中的u1的雜湊碼是638,則對應的16進製為27e,呼叫toString()方法返回的結果為:com.corn.objectsummary.User@27e。


因此:toString()是由物件的型別和其雜湊碼唯一確定,同一型別但不相等的兩個物件分別呼叫toString()方法返回的結果可能相同。


接下來,wait(...) / notify() / notifyAll()方法

(wait和sleep的區別是,wait釋放了鎖,sleep仍然持有鎖)

一說到wait(...) / notify() | notifyAll()幾個方法,首先想到的是執行緒。確實,這幾個方法主要用於java多執行緒之間的協作。先具體看下這幾個方法的主要含義:

wait():呼叫此方法所在的當前執行緒等待,直到在其他執行緒上呼叫此方法的主調(某一物件)的notify()/notifyAll()方法。

wait(long timeout)/wait(long timeout, int nanos):呼叫此方法所在的當前執行緒等待,直到在其他執行緒上呼叫此方法的主調(某一物件)的notisfy()/notisfyAll()方法,或超過指定的超時時間量。

notify()/notifyAll():喚醒在此物件監視器上等待的單個執行緒/所有執行緒。

wait(...) / notify() | notifyAll()一般情況下都是配套使用。下面來看一個簡單的例子:


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後且同步程式碼塊外的程式碼執行時機取決於執行緒排程
被notity喚醒,得以繼續執行
執行緒t執行相加結果45

既然是作用於多執行緒中,為什麼卻是Object這個基類所具有的方法?原因在於理論上任何物件都可以視為執行緒同步中的監聽器,且wait(...)/notify()|notifyAll()方法只能在同步程式碼塊中才能使用。

 從上述例子的輸出結果中可以得出如下結論:

1、wait(...)方法呼叫後當前執行緒將立即阻塞,且適當其所持有的同步程式碼塊中的鎖,直到被喚醒或超時或打斷後且重新獲取到鎖後才能繼續執行;

2、notify()/notifyAll()方法呼叫後,其所線上程不會立即釋放所持有的鎖,直到其所在同步程式碼塊中的程式碼執行完畢,此時釋放鎖,因此,如果其同步程式碼塊後還有程式碼,其執行則依賴於JVM的執行緒排程。

在Java原始碼中,可以看到wait()具體定義如下:


 public final void wait() throws InterruptedException {
        wait(0);
    }


且wait(long timeout, int nanos)方法定義內部實質上也是通過呼叫wait(long timeout)完成。而wait(long timeout)是一個native方法。因此,wait(...)方法本質上都是native方式實現。

notify()/notifyAll()方法也都是native方法。

Java中執行緒具有較多的知識點,是一塊比較大且重要的知識點。後期會有博文專門針對Java多執行緒作出詳細總結。此處不再細述。


方法13 finalize();


finalize方法主要與Java垃圾回收機制有關。首先我們看一下finalized方法在Object中的具體定義:


protected void finalize() throws Throwable { }

我們發現Object類中finalize方法被定義成一個空方法,為什麼要如此定義呢?finalize方法的呼叫時機是怎麼樣的呢?

首先,Object中定義finalize方法表明Java中每一個物件都將具有finalize這種行為,其具體呼叫時機在:JVM準備對此對形象所佔用的記憶體空間進行垃圾回收前,將被呼叫。由此可以看出,此方法並不是由我們主動去呼叫的(雖然可以主動去呼叫,此時與其他自定義方法無異)。




相關文章