JAVA中Object類方法詳解

 

 

 

 

 

一、引言

Object是java所有類的基類,是整個類繼承結構的頂端,也是最抽象的一個類。大家天天都在使用toString()、equals()、hashCode()、waite()、notify()、getClass()等方法,或許都沒有意識到是Object的方法,也沒有去看Object還有哪些方法以及思考為什麼這些方法要放到Object中。本篇就每個方法具體功能、重寫規則以及自己的一些理解。

二、Object方法詳解

Object中含有: registerNatives()、getClass()、hashCode()、equals()、clone()、toString()、notify()、notifyAll()、wait(long)、wait(long,int)、wait()、finalize()共十二個方法。 這個順序是按照Object類中定義方法的順序列舉的,下面我也會按照這個順序依次進行講解。

1.1、registerNatives()

public class Object {
    private static native void registerNatives();
    static {
        registerNatives();
    }
}

什麼鬼?哈哈哈,我剛看到這方法,一臉懵逼。 從名字上理解,這個方法是註冊native方法(本地方法,由JVM實現,底層是C/C++實現的) 向誰註冊呢?當然是向JVM ,當有程式調用到native方法時,JVM才好去找到這些底層的方法進行調用。

Object中的native方法,並使用registerNatives()向JVM進行註冊。(這屬於JNI的範疇,9龍暫不了解,有興趣的可自行查閱。)

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},
};

為什麼要使用靜態方法,還要放到靜態塊中呢?

上一篇整理了 類載入流程 , 我們知道了在類初始化的時候,會依次從父類到本類的類變數及類初始化塊中的類變數及方法按照定義順序放到< clinit>方法中,這樣可以保證父類的類變數及方法的初始化一定先於子類。 所以當子類調用相應native方法,比如計算hashCode時,一定可以保證能夠調用到JVM的native方法。

1.2、getClass()

public final native ClassgetClass():這是一個public的方法,我們可以直接通過對象調用。

類載入的第一階段類的載入就是將.class文件載入到記憶體,並生成一個java.lang.Class對象的過程。getClass()方法就是獲取這個對象,這是當前類的對象在運行時類的所有資訊的集合。這個方法是反射三種方式之一。

1.2.1、反射三種方式:

  1. 對象的getClass();
  2. 類名.class;
  3. Class.forName();
class extends ObjectTest {
    private void privateTest(String str) {
        System.out.println(str);
    }
    public void say(String str) {
        System.out.println(str);
    }
}
public class ObjectTest {
    public static void main(String[] args) throws Exception {
        ObjectTest  = new ();
        //獲取對象運行的Class對象
        Class<? extends ObjectTest> aClass = .getClass();
        System.out.println(aClass);
        //getDeclaredMethod這個方法可以獲取所有的方法,包括私有方法
        Method privateTest = aClass.getDeclaredMethod("privateTest", String.class);
        //取消java訪問修飾符限制。
        privateTest.setAccessible(true);
        privateTest.invoke(aClass.newInstance(), "private method test");
        //getMethod只能獲取public方法
        Method say = aClass.getMethod("say", String.class);
        say.invoke(aClass.newInstance(), "Hello World");
    }
}
//輸出結果:
//class test.
//private method test
//Hello World

反射主要用來獲取運行時的資訊,可以將java這種靜態語言動態化,可以在編寫程式碼時將一個子對象賦值給父類的一個引用,在運行時通過反射可以或許運行時對象的所有資訊,即多態的體現。對於反射知識還是很多的,這裡就不展開講了。

1.3、hashCode()

public native int hashCode();這是一個public的方法,所以 子類可以重寫 它。這個方法返回當前對象的hashCode值,這個值是一個整數範圍內的(-2^31 ~ 2^31 – 1)數字。

對於hashCode有以下幾點約束

  • 在 Java 應用程式執行期間,在對同一對象多次調用 hashCode 方法時,必須一致地返回相同的整數,前提是將對象進行 equals 比較時所用的資訊沒有被修改;
  • 如果兩個對象 x.equals(y) 方法返回true,則x、y這兩個對象的hashCode必須相等。
  • 如果兩個對象x.equals(y) 方法返回false,則x、y這兩個對象的hashCode可以相等也可以不等。 但是,為不相等的對象生成不同整數結果可以提高哈希表的性能。
  • 默認的hashCode是將記憶體地址轉換為的hash值,重寫過後就是自定義的計算方式;也可以通過System.identityHashCode(Object)來返回原本的hashCode。
public class HashCodeTest {
    private int age;
    private String name;
    @Override
    public int hashCode() {
        Object[] a = Stream.of(age, name).toArray();
        int result = 1;
        for (Object element : a) {
            result = 31 * result + (element == null ? 0 : element.hashCode());
        }
        return result;
    }
}

推薦使用Objects.hash(Object… values)方法。相信看源碼的時候,都看到計算hashCode都使用了31作為基礎乘數, 為什麼使用31呢?我比較贊同與理解result * 31 = (result<<5) – result。JVM底層可以自動做優化為位運算,效率很高;還有因為31計算的hashCode衝突較少,利於hash桶位的分布。

1.4、equals()

public boolean equals(Object obj);用於比較當前對象與目標對象是否相等,默認是比較引用是否指向同一對象。為public方法,子類可重寫。

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

為什麼需要重寫equals方法?

因為如果不重寫equals方法,當將自定義對象放到map或者set中時;如果這時兩個對象的hashCode相同,就會調用equals方法進行比較,這個時候會調用Object中默認的equals方法,而默認的equals方法只是比較了兩個對象的引用是否指向了同一個對象,顯然大多數時候都不會指向,這樣就會將重複對象存入map或者set中。這就 破壞了map與set不能存儲重複對象的特性,會造成記憶體溢出 。

重寫equals方法的幾條約定:

  1. 自反性:即x.equals(x)返回true,x不為null;
  2. 對稱性:即x.equals(y)與y.equals(x)的結果相同,x與y不為null;
  3. 傳遞性:即x.equals(y)結果為true, y.equals(z)結果為true,則x.equals(z)結果也必須為true;
  4. 一致性:即x.equals(y)返回true或false,在未更改equals方法使用的參數條件下,多次調用返回的結果也必須一致。x與y不為null。
  5. 如果x不為null, x.equals(null)返回false。

我們根據上述規則來重寫equals方法。

public class EqualsTest{
    private int age;
    private String name;
    //省略get、set、構造函數等
     @Override
    public boolean equals(Object o) {
        //先判斷是否為同一對象
        if (this == o) {
            return true;
        }
        //再判斷目標對象是否是當前類及子類的實例對象
        //注意:instanceof包括了判斷為null的情況,如果o為null,則返回false
        if (!(o instanceof )) {
            return false;
        }
         that = () o;
        return age == that.age &&
                Objects.equals(name, that.name);
    }
     public static void main(String[] args) throws Exception {
         EqualsTest1 equalsTest1 = new EqualsTest1(23, "9龍");
        EqualsTest1 equalsTest12 = new EqualsTest1(23, "9龍");
        EqualsTest1 equalsTest13 = new EqualsTest1(23, "9龍");
        System.out.println("-----------自反性----------");
        System.out.println(equalsTest1.equals(equalsTest1));
        System.out.println("-----------對稱性----------");
        System.out.println(equalsTest12.equals(equalsTest1));
        System.out.println(equalsTest1.equals(equalsTest12));
        System.out.println("-----------傳遞性----------");
        System.out.println(equalsTest1.equals(equalsTest12));
        System.out.println(equalsTest12.equals(equalsTest13));
        System.out.println(equalsTest1.equals(equalsTest13));
        System.out.println("-----------一致性----------");
        System.out.println(equalsTest1.equals(equalsTest12));
        System.out.println(equalsTest1.equals(equalsTest12));
        System.out.println("-----目標對象為null情況----");
        System.out.println(equalsTest1.equals(null));
    }
}
//輸出結果
//-----------自反性----------
//true
//-----------對稱性----------
//true
//true
//-----------傳遞性----------
//true
//true
//true
//-----------一致性----------
//true
//true
//-----目標對象為null情況----
//false

從以上輸出結果驗證了我們的重寫規定是正確的。

注意:instanceof 關鍵字已經幫我們做了目標對象為null返回false,我們就不用再去顯示判斷了。

建議equals及hashCode兩個方法,需要重寫時,兩個都要重寫,一般都是將自定義對象放至Set中,或者Map中的key時,需要重寫這兩個方法。

1.4、clone()

protected native Object clone() throws CloneNotSupportedException;

此方法返回當前對象的一個副本。

這是一個protected方法,提供給子類重寫。但需要實現Cloneable介面,這是一個標記介面,如果沒有實現,當調用object.clone()方法,會拋出CloneNotSupportedException。

public class CloneTest implements Cloneable {
    private int age;
    private String name;
    //省略get、set、構造函數等
    @Override
    protected CloneTest clone() throws CloneNotSupportedException {
        return (CloneTest) super.clone();
    }
    public static void main(String[] args) throws CloneNotSupportedException {
        CloneTest cloneTest = new CloneTest(23, "9龍");
        CloneTest clone = cloneTest.clone();
        System.out.println(clone == cloneTest);
        System.out.println(cloneTest.getAge()==clone.getAge());
        System.out.println(cloneTest.getName()==clone.getName());
    }
}
//輸出結果
//false
//true
//true

從輸出我們看見,clone的對象是一個新的對象;但原對象與clone對象的 String類型 的name卻是同一個引用,這表明,super.clone方法對成員變數如果是引用類型,進行是淺拷貝。

那什麼是淺拷貝?對應的深拷貝?

淺拷貝:拷貝的是引用。

深拷貝:新開闢記憶體空間,進行值拷貝。

那如果我們要進行深拷貝怎麼辦呢?看下面的例子。

class Person implements Cloneable{
    private int age;
    private String name;
     //省略get、set、構造函數等
     @Override
    protected Person clone() throws CloneNotSupportedException {
        Person person = (Person) super.clone();
        //name通過new開闢記憶體空間
        person.name = new String(name);
        return person;
   }
}
public class CloneTest implements Cloneable {
    private int age;
    private String name;
    //增加了person成員變數
    private Person person;
    //省略get、set、構造函數等
    @Override
    protected CloneTest clone() throws CloneNotSupportedException {
        CloneTest clone = (CloneTest) super.clone();
        clone.person = person.clone();
        return clone;
    }
    public static void main(String[] args) throws CloneNotSupportedException {
       CloneTest cloneTest = new CloneTest(23, "9龍");
        Person person = new Person(22, "路飛");
        cloneTest.setPerson(person);
        CloneTest clone = cloneTest.clone();
        System.out.println(clone == cloneTest);
        System.out.println(cloneTest.getAge() == clone.getAge());
        System.out.println(cloneTest.getName() == clone.getName());
        Person clonePerson = clone.getPerson();
        System.out.println(person == clonePerson);
        System.out.println(person.getName() == clonePerson.getName());
    }
}
//輸出結果
//false
//true
//true
//false
//false

可以看到,即使成員變數是引用類型,我們也實現了深拷貝。 如果成員變數是引用類型,想實現深拷貝,則成員變數也要實現Cloneable介面,重寫clone方法。

1.5、toString()

public String toString();這是一個public方法,子類可重寫, 建議所有子類都重寫toString方法,默認的toString方法,只是將當前類的全限定性類名+@+十六進位的hashCode值。

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

我們思考一下為什麼需要toString方法?

我這麼理解的,返回當前對象的字元串表示,可以將其列印方便查看對象的資訊,方便記錄日誌資訊提供調試。

我們可以選擇需要表示的重要資訊重寫到toString方法中。為什麼Object的toString方法只記錄類名跟記憶體地址呢?因為Object沒有其他資訊了,哈哈哈。

1.6、wait()/ wait(long)/ waite(long,int)

這三個方法是用來 執行緒間通訊用 的,作用是 阻塞當前執行緒 ,等待其他執行緒調用notify()/notifyAll()方法將其喚醒。這些方法都是public final的,不可被重寫。

注意:

  1. 此方法只能在當前執行緒獲取到對象的鎖監視器之後才能調用,否則會拋出IllegalMonitorStateException異常。
  2. 調用wait方法,執行緒會將鎖監視器進行釋放;而Thread.sleep,Thread.yield()並不會釋放鎖 。
  3. wait方法會一直阻塞,直到其他執行緒調用當前對象的notify()/notifyAll()方法將其喚醒;而wait(long)是等待給定超時時間內(單位毫秒),如果還沒有調用notify()/nofiyAll()會自動喚醒;waite(long,int)如果第二個參數大於0並且小於999999,則第一個參數+1作為超時時間;
 public final void wait() throws InterruptedException {
        wait(0);
    } 
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);
    }

1.7、notify()/notifyAll()

前面說了, 如果當前執行緒獲得了當前對象鎖,調用wait方法,將鎖釋放並阻塞;這時另一個執行緒獲取到了此對象鎖,並調用此對象的notify()/notifyAll()方法將之前的執行緒喚醒。 這些方法都是public final的,不可被重寫。

  1. public final native void notify(); 隨機喚醒之前在當前對象上調用wait方法的一個執行緒
  2. public final native void notifyAll(); 喚醒所有之前在當前對象上調用wait方法的執行緒

下面我們使用wait()、notify()展示執行緒間通訊。假設9龍有一個賬戶,只要9龍一發工資,就被女朋友給取走了。

//賬戶
public class Account {
    private String accountNo;
    private double balance;
    private boolean flag = false;
    public Account() {
    }
    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }
    /**
     * 取錢方法
     *
     * @param drawAmount 取款金額
     */
    public synchronized void draw(double drawAmount) {
        try {
            if (!flag) {
                //如果flag為false,表明賬戶還沒有存入錢,取錢方法阻塞
                wait();
            } else {
                //執行取錢操作
                System.out.println(Thread.currentThread().getName() + " 取錢" + drawAmount);
                balance -= drawAmount;
                //標識賬戶已沒錢
                flag = false;
                //喚醒其他執行緒
                notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void deposit(double depositAmount) {
        try {
            if (flag) {
                //如果flag為true,表明賬戶已經存入錢,取錢方法阻塞
                wait();
            } else {
                //存錢操作
                System.out.println(Thread.currentThread().getName() + " 存錢" + depositAmount);
                balance += depositAmount;
                //標識賬戶已存入錢
                flag = true;
                //喚醒其他執行緒
                notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//取錢者
public class DrawThread extends Thread {
    private Account account;
    private double drawAmount;
    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }
    @Override
    public void run() {
        //循環6次取錢
        for (int i = 0; i < 6; i++) {
            account.draw(drawAmount);
        }
    }
}
//存錢者
public class DepositThread extends Thread {
    private Account account;
    private double depositAmount;
    public DepositThread(String name, Account account, double depositAmount) {
        super(name);
        this.account = account;
        this.depositAmount = depositAmount;
    }
    @Override
    public void run() {
        //循環6次存錢操作
        for (int i = 0; i < 6; i++) {
            account.deposit(depositAmount);
        }
    }
}
//測試
public class DrawTest {
    public static void main(String[] args) {
        Account brady = new Account("9龍", 0);
        new DrawThread("女票", brady, 10).start();
        new DepositThread("公司", brady, 10).start();
    }
}
//輸出結果
//公司 存錢10.0
//女票 取錢10.0
//公司 存錢10.0
//女票 取錢10.0
//公司 存錢10.0
//女票 取錢10.0

例子中我們通過一個boolean變數來判斷賬戶是否有錢,當取錢執行緒來判斷如果賬戶沒錢,就會調用wait方法將此執行緒進行阻塞;這時候存錢執行緒判斷到賬戶沒錢, 就會將錢存入賬戶,並且調用notify()方法通知被阻塞的執行緒,並更改標誌;取錢執行緒收到通知後,再次獲取到cpu的調度就可以進行取錢。反覆更改標誌,通過調用wait與notify()進行執行緒間通訊。實際中我們會時候生產者消費者隊列會更簡單。

注意:調用notify()後,阻塞執行緒被喚醒,可以參與鎖的競爭,但可能調用notify()方法的執行緒還要繼續做其他事,鎖並未釋放,所以我們看到的結果是,無論notify()是在方法一開始調用,還是最後調用,阻塞執行緒都要等待當前執行緒結束才能開始。

為什麼wait()/notify()方法要放到Object中呢?

因為每個對象都可以成為鎖監視器對象,所以放到Object中,可以直接使用。

1.8、finalize()

protected void finalize() throws Throwable ;

此方法是在垃圾回收之前,JVM會調用此方法來清理資源。此方法可能會將對象重新置為可達狀態,導致JVM無法進行垃圾回收。

我們知道java相對於C++很大的優勢是程式設計師不用手動管理記憶體,記憶體由jvm管理;如果我們的引用對象在堆中沒有引用指向他們時,當記憶體不足時,JVM會自動將這些對象進行回收釋放記憶體,這就是我們常說的垃圾回收。但垃圾回收沒有講述的這麼簡單。

finalize()方法具有如下4個特點:

  1. 永遠不要主動調用某個對象的finalize()方法,該方法由垃圾回收機制自己調用;
  2. finalize()何時被調用,是否被調用具有不確定性;
  3. 當JVM執行可恢復對象的finalize()可能會將此對象重新變為可達狀態;
  4. 當JVM執行finalize()方法時出現異常,垃圾回收機制不會報告異常,程式繼續執行。
public class FinalizeTest {
    private static FinalizeTest ft = null;
    public void info(){
        System.out.println("測試資源清理得finalize方法");
    }
    public static void main(String[] args) {
        //創建FinalizeTest對象立即進入可恢復狀態
        new FinalizeTest();
        //通知系統進行垃圾回收
        System.gc();
        //強制回收機制調用可恢復對象的finalize()方法
//        Runtime.getRuntime().runFinalization();
        System.runFinalization();
        ft.info();
    }
    @Override
    public void finalize(){
        //讓ft引用到試圖回收的可恢復對象,即可恢復對象重新變成可達
        ft = this;
        throw new RuntimeException("出異常了,你管不管啊");
    }
}
//輸出結果
//測試資源清理得finalize方法

我們看到,finalize()方法將可恢復對象置為了可達對象,並且在finalize中拋出異常,都沒有任何資訊,被忽略了。

1.8.1、對象在記憶體中的狀態

對象在記憶體中存在三種狀態:

  1. 可達狀態 :有引用指向,這種對象為可達狀態;
  2. 可恢復狀態 :失去引用,這種對象稱為可恢復狀態;垃圾回收機制開始回收時,回調用可恢復狀態對象的finalize()方法(如果此方法讓此對象重新獲得引用,就會變為可達狀態,否則,會變為不可大狀態)。
  3. 不可達狀態 :徹底失去引用,這種狀態稱為不可達狀態,如果垃圾回收機制這時開始回收,就會將這種狀態的對象回收掉。

 

 

 

 

 

1.8.2、垃圾回收機制

  1. 垃圾回收機制只負責回收堆記憶體種的對象 ,不會回收任何物理資源(例如資料庫連接、網路IO等資源);
  2. 程式無法精確控制垃圾回收的運行, 垃圾回收只會在合適的時候進行 。當對象為不可達狀態時,系統會在合適的時候回收它的記憶體。
  3. 在垃圾回收機制回收任何對象之前,總會先調用它的finalize()方法 ,該方法可能會將對象置為可達狀態,導致垃圾回收機製取消回收。

1.8.3、強制垃圾回收

上面我們已經說了,當對象失去引用時,會變為可恢復狀態,但垃圾回收機制什麼時候運行,什麼時候調用finalize方法無法知道。雖然垃圾回收機制無法精準控制,但java還是提供了方法可以建議JVM進行垃圾回收,至於是否回收,這取決於虛擬機。但似乎可以看到一些效果。

public class GcTest {
    public static void main(String[] args){
        for(int i=0;i<4;i++){
            //沒有引用指向這些對象,所以為可恢復狀態
            new GcTest();
            //強制JVM進行垃圾回收(這只是建議JVM)
            System.gc();
            //Runtime.getRuntime().gc();
        }
    }
    @Override
    public void finalize(){
        System.out.println("系統正在清理GcTest資源。。。。");
    }
}
//輸出結果
//系統正在清理GcTest資源。。。。
//系統正在清理GcTest資源。。。。

System.gc(),Runtime.getRuntime().gc()兩個方法作用一樣的,都是建議JVM垃圾回收,但不一定回收,多運行幾次,結果可能都不一致。

三、總結

本篇舉例講解了Objec中的所有方法的作用、意義及使用,從java最基礎的類出發,感受java設計之美吧。我是不會高訴大家,這好像面試也會問的【攤手】。

歡迎工作一到五年的Java工程師朋友們加入Java程式設計師開發: 721575865

群內提供免費的Java架構學習資料(裡面有高可用、高並發、高性能及分散式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用”沒有時間「來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

Tags: