你知道Object中有哪些方法及其做用嗎?

1、引言2、Object方法詳解1.一、registerNatives()1.二、getClass()1.2.一、反射三種方式:1.三、hashCode()1.四、equals()1.四、clone()1.五、toString()1.六、wait()/ wait(long)/ waite(long,int)1.七、notify()/notifyAll()1.八、finalize()1.8.一、對象在內存中的狀態1.8.二、垃圾回收機制1.8.三、強制垃圾回收3、總結html

1、引言

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

2、Object方法詳解

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

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},
};
複製代碼

爲何要使用靜態方法,還要放到靜態塊中呢?segmentfault

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

1.二、getClass()

public final native Class getClass():這是一個public的方法,咱們能夠直接經過對象調用。網絡

類加載的第一階段類的加載就是將.class文件加載到內存,並生成一個java.lang.Class對象的過程。getClass()方法就是獲取這個對象,這是當前類的對象在運行時類的全部信息的集合。這個方法是反射三種方式之一。app

1.2.一、反射三種方式:
  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這種靜態語言動態化,能夠在編寫代碼時將一個子對象賦值給父類的一個引用,在運行時經過反射能夠或許運行時對象的全部信息,即多態的體現。對於反射知識仍是不少的,這裏就不展開講了。jvm

1.三、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.四、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.四、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.五、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.六、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.七、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.八、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. 可達狀態:有引用指向,這種對象爲可達狀態;
  2. 可恢復狀態:失去引用,這種對象稱爲可恢復狀態;垃圾回收機制開始回收時,回調用可恢復狀態對象的finalize()方法(若是此方法讓此對象從新得到引用,就會變爲可達狀態,不然,會變爲不可大狀態)。
  3. 不可達狀態:完全失去引用,這種狀態稱爲不可達狀態,若是垃圾回收機制這時開始回收,就會將這種狀態的對象回收掉。
1.8.二、垃圾回收機制
  1. 垃圾回收機制只負責回收堆內存種的對象,不會回收任何物理資源(例如數據庫鏈接、網絡IO等資源);
  2. 程序沒法精確控制垃圾回收的運行,垃圾回收只會在合適的時候進行。當對象爲不可達狀態時,系統會在合適的時候回收它的內存。
  3. 在垃圾回收機制回收任何對象以前,總會先調用它的finalize()方法,該方法可能會將對象置爲可達狀態,致使垃圾回收機制取消回收。
1.8.三、強制垃圾回收

上面咱們已經說了,當對象失去引用時,會變爲可恢復狀態,但垃圾回收機制何時運行,何時調用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垃圾回收,但不必定回收,多運行幾回,結果可能都不一致。

3、總結

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

整理不易,以爲能夠,但願你們點贊支持啊。轉載請註明出處。

十分感謝如下連接的分享,參考連接:

registerNatives方法做用

計算hashCode爲何選擇31?

相關文章
相關標籤/搜索