Java Object類詳解

一. Object類介紹

       java.lang.Object類是所有類的父類,每個類都使用 Object 作爲超類。所有對象(包括數組)都實現這個類的方法。在不明確給出超類的情況下,Java會自動把Object作爲要定義類的超類。可以使用類型爲Object的變量指向任意類型的對象。

      Object類提供了多個方法,具體如下:
      1. public Object()
      2. public final native Class<?> getClass()
      3. public native int hashCode()
      4. public boolean equals(Object obj)
      5. public String toString()
      6. protected native Object clone()
      7. public final native void notify()
      8. public final native void notifyAll()
      9. public final native void wait(long timeout)
      10. public final void wait(long timeout, int nanos)
      11.public final void wait()
      12.protected void finalize()

      下面筆者將詳細介紹這些重要的方法。


二. 方法詳細介紹

1. public Object()
      這個方法是Object類的一個默認的構造方法,在構造子類時,都會先調用這個默認構造方法。

2. public final native Class<?> getClass()

      getClass()方法是一個final方法,這就意味着不允許子類重寫,同時這也是一個native方法。

      getClass()返回該對象的運行時類的 java.lang.Class 對象,該對象保存着原對象的類信息,比如原對象的類名叫什麼,類裏有什麼方法,字段等,這些信息可以由Class對象的getName()、getMethods()等方法獲取。該方法返回值爲Class對象,後面的“?”是泛型(“?”則屬於類型通配符的一種),代表着正在運行的類。

      這裏筆者只是簡單介紹Object類getClass()的作用,而Class對象更多的是與反射聯繫在一起,關於反射的內容請查看其它相關的資料。
     
3. public native int hashCode()

      hashCode()方法同樣是一個native方法,該方法返回對象的哈希碼值。hashCode是用來在散列存儲結構中確定對象的存儲地址的,在HashMap、Hashtable等底層實現都有用到,其主要是用於查找的快捷性,因爲在集合查找時,hashcode能大大降低對象比較次數,提高查找效率。

      關於hashCode有幾點常規的約定:
      ① 在 Java 應用程序執行期間,在同一對象上多次調用 hashCode 方法時,必須一致地返回相同的整數,前提是對象上 equals 比較中所用的信息沒有被修改。從某一應用程序的一次執行到同一應用程序的另一次執行,該整數無需保持一致。
      ② 如果根據 equals(Object) 方法判定兩個對象是相等的,那麼在兩個對象中的每個對象上調用 hashCode 方法都必須生成相同的整數結果。
      ③ 如果根據equals方法得到兩個對象不相等,那麼這2個對象的hashCode值不需要必須不相同,即可以相同。但是,不相等的對象的hashCode值不同的話可以提高哈希表的性能。
      ④當equals方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定。
實際上,由於hashCode一般是通過將該對象的內部地址轉換成一個整數來實現的,所以通常情況下,不同的對象產生的哈希碼是不同的。

      接下來看個簡單的例子:

public class User {
	private String name;
	private int sex;
	private String address;

	public User(String name, int sex, String address) {
		super();
		this.name = name;
		this.sex = sex;
		this.address = address;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((address == null) ? 0 : address.hashCode());
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		result = prime * result + sex;
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		User other = (User) obj;
		if (address == null) {
			if (other.address != null)
				return false;
		} else if (!address.equals(other.address))
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		if (sex != other.sex)
			return false;
		return true;
	}
}

      這個例子給出User類,有三個屬性:name、sex和address。使用Eclipse自動生成hashCode()和equals() 方法如上。我們先跳過equals()方法直接看 hashCode()方法實現。從上面的代碼可以看到hashCode()方法首先聲明一個值爲31的整型變量,然後遍歷所有的類屬性,累加計算result=31*result+A,若A是整型數直接相加,若是String類型調用String類的hashCode()方法(其他類型具體實現可自己自動生成hashCode()方法查看,這裏筆者只簡單講下String類型),然後String類的hashCode()實現如下:

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


      上面的代碼其實做的就是計算s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]的值, s[i]是string的第i個字符,n是String的長度。那麼問題來了,爲什麼上面的User類還有這裏的String類都使用31做乘數,而不是用其他數呢?主要有兩個原因:①之所以選擇31,是因爲它是個奇素數。如果乘數是偶數,並且乘法溢出的話,信息就會丟失,因爲與2相乘等價於移位運算。使用素數的好處並不是很明顯,但是習慣上都使用素數來計算散列結果。②31有個很好的特性,就是用移位和減法來代替乘法,可以得到更好的性能:31*i==(i<<5)-i。

      因此,hashCode()方法的實現通常使用31來作乘數,同時方法體裏遍歷所有值來累加得到hashCode的實現能夠導致相同的value就一定有相同的hashCode,不同的value也較大可能得到不同的hashCode,但這不是一定的,不同的value還是有可能有相同的hashCode的。至於爲什麼要使用累加得到hashCode這麼複雜的實現,就需要數學家來解釋了。


4. public boolean equals(Object obj)

      equals()方法用來比較兩個對象是否相等,默認實現是使用“==”直接比較,即比較兩個對象的內存地址是否相等:

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

      equals方法在非空對象引用上有以下特性:
      ①reflexive 自反性。任何非空引用值x,對於x.equals(x)必須返回true。
      ②symmetric 對稱性。任何非空引用值x和y,如果x.equals(y)爲true,那麼y.equals(x)也必須爲true。
      ③transitive 傳遞性。任何非空引用值x、y和z,如果x.equals(y)爲true並且y.equals(z)爲true,那麼x.equals(z)也必定爲true。
      ④consistent 一致性。任何非空引用值x和y,多次調用 x.equals(y) 始終返回 true 或始終返回 false,前提是對象上 equals 比較中所用的信息沒有被修改。

      對於任何非空引用值 x,x.equals(null) 都應返回 false。

      equals()比較容易理解,所以就不多說了。不過需要注意的是,如果重寫了equals()方法,通常有必要重寫hashCode()方法,這一點已經在上面已經講過了。


5. public String toString()

      toString()方法是用來放回一個簡明易懂的“以文本方式表示”的字符串,建議Object所有的子類都重寫這個方法。默認實現如下:

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

      可以看到Object 類的 toString() 方法返回一個字符串,該字符串由類名(對象是該類的一個實例)、at 標記符“@”和此對象哈希碼的無符號十六進制表示組成。


6. protected native Object clone()

      clone()方法創建並返回此對象的一個拷貝。一般情況下,對於任何對象 x,表達式 x.clone() != x 爲true,x.clone().getClass() == x.getClass() 也爲true。

      由於Object 類本身不實現接口 Cloneable,所以在類爲 Object 的對象上調用 clone() 方法將會導致在運行時拋出異常CloneNotSupportedException。同時,如果Object的子類沒有實現接口 Cloneable,調用clone()方法同樣會拋出CloneNotSupportedException,以指示無法克隆某個實例。

      來到這裏可能有人會問,爲什麼需要克隆對象,直接new一個對象不行嗎?答案是因爲克隆的對象可能包含一些已經修改過的屬性,而new出來的對象的屬性都還是初始化時候的值,所以當需要一個新的對象來保存當前對象的“狀態”就靠clone()方法了。那麼我把這個對象的臨時屬性一個一個的賦值給我新new的對象不也行嗎?可以是可以,但是一方面這比較麻煩,另一方面。我們可以看到clone()方法是native方法,毫無疑問這速度快,因爲實在底層實現的。

      要理解好clone()方法,先要知道深度克隆以及淺度克隆。我們知道,如果要實現克隆,需要實現Cloneable接口然後重寫clone()方法。我們調用clone()方法,想的是先在內存中開闢一塊和原始對象一樣的空間,然後原樣拷貝原始對象中的內容。如果我們重寫clone()方法是隻是單單調用super.clone(),對於基本數據類型,這樣的操作是沒有問題的,但對於非基本類型變量,比如依賴其他的類,這樣實現其實clone的對象保存的只是原對象的引用,這導致clone後的非基本類型變量和原始對象中相應的變量指向的是同一個對象。這就是淺度克隆。

public class User implements Cloneable {
        private String name;
        private int sex;
        private String address;

        public User(String name, int sex, String address) {
                super();
                this.name = name;
                this.sex = sex;
                this.address = address;
        }

        @Override
        protected Object clone() throws CloneNotSupportedException {
                // TODO Auto-generated method stub
                return super.clone();
        }

        public static void main(String[] args) {
                User u1 = new User("Jack", 1, "USA");
                try {
                        User u2 = (User) u1.clone();
                        System.out.println(u1.name == u2.name);
                } catch (CloneNotSupportedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                }
        }

}


      上面的程序輸出是true,證明默認調用super.clone()淺度克隆得到的是原對象的引用。

      其實我們的初衷是對於要克隆的對象中的非基本數據類型的屬性對應的類,也要實現克隆,這樣對於非基本數據類型的屬性,複製的不是一份引用,即新產生的對象和原始對象中的非基本數據類型的屬性指向的不是同一個對象,這就是深度克隆。

      要實現深度克隆,對於有非基本數據類型的屬性的類,在clone時除了實現Cloneable接口調用super.clone()方法,對於非基本數據類型,就需要再在該屬性上調用一次clone()方法:

class Address implements Cloneable {
        private String address;

        public Address(String address) {
                super();
                this.address = address;
        }

        @Override
        protected Object clone() throws CloneNotSupportedException {
                // TODO Auto-generated method stub
                return super.clone();
        }
}

public class User implements Cloneable {
        private String name;
        private int sex;
        private Address address;

        public User(String name, int sex, Address address) {
                super();
                this.name = name;
                this.sex = sex;
                this.address = address;
        }

        @Override
        protected Object clone() throws CloneNotSupportedException {
                User u = (User) super.clone();
                u.address = (Address) this.address.clone();
                return u;
        }

        public static void main(String[] args) {
                Address add = new Address("China");
                User u1 = new User("Jack", 1, add);
                try {
                        User u2 = (User) u1.clone();
                        System.out.println(u1.address == u2.address);
                } catch (CloneNotSupportedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                }
        }
}


      上面的例子中,要實現User類的深度克隆,可以看到User類的clone()方法除了需要調用super.clone(),還需要在非基本數據類型Address上調用clone()方法,當然了,Address也得實現Cloneable接口。這樣才能真正實現深度克隆,上面代碼執行的結果纔會是false。

      clone()方法還有一個需要注意的地方,就是clone()方法是一個protected屬性的方法。爲什麼Object類要將clone()方法定義爲protected,而不是public呢?其實,使用protected來修飾clone()方法,是爲了安全考慮。從上面我們已經瞭解了深度拷貝和淺度拷貝的定義以及區別,噹噹前的類依賴於其他類,即有非基本數據類型的屬性後,Object類無法幫我們實現深度拷貝,將修飾符定義爲protected,這樣想要在其他任何地方調用這個類的clone()方法時,這個類就必須去重寫clone()方法並且把修飾符改爲public,這樣在任何地方都可以調用這個類的clone()方法了。比如下面的例子:

package com;
public class A {             
        
}

package org; 
import com.A; 
public class B {               
        public static void main(String[] args) {               
                A a = new A();                            
                a.clone();               
        } 
}

      如上面的例子,類A是要被克隆的類,類B相當於要使用A的地方,如果類A不重寫clone方法,在B類中是調不到clone()方法的,因爲A和B既不是子父類關係,也不在同一個包下,所以clone()方法對B是不可見的。當類A實現Cloneable接口並且重寫clone()方法後,clone()方法在B類中就可見了,也就是說我們在任何地方都可以克隆A了。

      總而言之,用protected修飾clone()方法,主要是爲了讓子類去重寫它,實現深拷貝,以防在其他任何地方隨意調用後修改了對象的屬性對原來的對象造成影響。如果使用public修飾clone()方法,子類就不用必須重寫clone()方法,這樣就可能會出現問題。


7. public final native void notify()

      首先看到notify()方法是一個native方法,並且也是final的,不允許子類重寫。

      notify()喚醒在此對象監視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,並在對實現做出決定時發生。線程通過調用其中一個 wait ()方法,在對象的監視器上等待。

      直到當前的線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程。被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競爭。例如,喚醒的線程在作爲鎖定此對象的下一個線程方面沒有可靠的特權或劣勢。

      notify()方法只能被作爲此對象監視器的所有者的線程來調用。一個線程要想成爲對象監視器的所有者,可以使用以下3種方法:①執行對象的同步實例方法;②使用synchronized內置鎖;③對於Class類型的對象,執行同步靜態方法。

      需要注意的是一次只能有一個線程擁有對象的監視器。如果當前線程不是此對象監視器的所有者的話會拋出IllegalMonitorStateException異常。


8. public final native void notifyAll()

      notifyAll()方法跟上面的notify()一樣,不同的是notifyAll()是喚醒在此對象監視器上等待的所有線程。
      同樣的,如果當前線程不是對象監視器的所有者,那麼調用notifyAll同樣會發生IllegalMonitorStateException異常。

9. public final native void wait(long timeout)

      該方法導致當前的線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量。當前的線程必須擁有此對象監視器,否則還是會發生IllegalMonitorStateException異常。

      該線程發佈對此監視器的所有權並等待,直到其他線程通過調用 notify ()方法或 notifyAll() 方法通知在此對象的監視器上等待的線程醒來。然後該線程將等到重新獲得對監視器的所有權後才能繼續執行。

      wait()方法會讓當前線程(比如線程T)將其自身放置在對象的等待集中,並且放棄該對象上的所有同步要求。出於線程調度目的,線程T是不可用並處於休眠狀態,直到發生以下四件事中的任意一件:
      ①其他某個線程調用此對象的notify()方法,並且線程T碰巧被任選爲被喚醒的線程;
      ②其他某個線程調用此對象的notifyAll()方法;
      ③其他某個線程調用Thread.interrupt()方法中斷線程T;
      ④時間到了參數設置的超時時間。如果timeout參數爲0,則不會超時,會一直進行等待。

      可以理解wait()方法相當於放棄了當前線程對對象監視器的所有者(也就是說釋放了對象的鎖)之後,線程T會被等待集中被移除,並且重新進行線程調度。然後,該線程以常規方式與其他線程競爭,以獲得在該對象上同步的權利。一旦獲得對該對象的控制權,該對象上的所有其同步聲明都將被恢復到以前的狀態,這就是調用wait方法時的情況。然後,線程T從wait方法的調用中返回。所以,從wait方法返回時,該對象和線程T的同步狀態與調用wait方法時的情況完全相同。

      在沒有被通知、中斷或超時的情況下,線程還可以喚醒一個所謂的虛假喚醒 (spurious wakeup)。雖然這種情況在實踐中很少發生,但是應用程序必須通過以下方式防止其發生,即對應該導致該線程被提醒的條件進行測試,如果不滿足該條件,則繼續等待。換句話說,等待應總是發生在循環中,如下面的示例:

synchronized (obj) {         
        while (<condition does not hold>)                  
                obj.wait(timeout);              
        ... // Perform action appropriate to condition 
}

      如果當前線程在等待之前或在等待時被任何線程中斷,則會拋出InterruptedException異常。在按上述形式恢復此對象的鎖定狀態時纔會拋出此異常。

10. public final void wait(long timeout, int nanos) 

      跟wait(long timeout)方法類似,多了一個nanos參數,這個參數表示額外時間(以毫微秒爲單位,範圍是 0-999999)。 所以超時的時間還需要加上nanos毫秒。

      需要注意的是 wait(0, 0)和wait(0)效果是一樣的,即一直等待。

11. public final void wait()

      wait()方法導致當前的線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法。換句話說,此方法的行爲就好像它僅執行 wait(0) 調用一樣。

      一般情況下,wait()方法和notify()方法會一起使用的,wait()方法阻塞當前線程,notify()方法喚醒當前線程。一個使用wait()和notify()方法的生產者消費者例子代碼如下:

public class WaitNotifyTest {

    public static void main(String[] args) {
        Factory factory = new Factory();
        new Thread(new Producer(factory, 5)).start();
        new Thread(new Producer(factory, 5)).start();
        new Thread(new Producer(factory, 20)).start();
        new Thread(new Producer(factory, 30)).start();
        new Thread(new Consumer(factory, 10)).start();
        new Thread(new Consumer(factory, 20)).start();
        new Thread(new Consumer(factory, 5)).start();
        new Thread(new Consumer(factory, 5)).start();
        new Thread(new Consumer(factory, 20)).start();
    }

}

class Factory {

    public static final Integer MAX_NUM = 50;

    private int currentNum = 0;

    public void consume(int num) throws InterruptedException {
        synchronized (this) {
            while(currentNum - num < 0) {
                this.wait();
            }
            currentNum -= num;
            System.out.println("consume " + num + ", left: " + currentNum);
            this.notifyAll();
        }
    }

    public void produce(int num) throws InterruptedException {
        synchronized (this) {
            while(currentNum + num > MAX_NUM) {
                this.wait();
            }
            currentNum += num;
            System.out.println("produce " + num + ", left: " + currentNum);
            this.notifyAll();
        }
    }

}

class Producer implements Runnable {
    private Factory factory;
    private int num;
    public Producer(Factory factory, int num) {
        this.factory = factory;
        this.num = num;
    }
    @Override
    public void run() {
        try {
            factory.produce(num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


class Consumer implements Runnable {
    private Factory factory;
    private int num;
    public Consumer(Factory factory, int num) {
        this.factory = factory;
        this.num = num;
    }
    @Override
    public void run() {
        try {
            factory.consume(num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

12. protected void finalize()

      當垃圾回收器確定不存在對該對象的更多引用時,由對象的垃圾回收器調用此方法。

      finalize()方法是一個protected方法,Object類的默認實現是不進行任何操作。子類需要重寫 finalize ()方法,以配置系統資源或執行其他清除。

      finalize 的常規協定是:當 Java虛擬機已確定尚未終止的任何線程無法再通過任何方法訪問此對象時,將調用此方法,除非由於準備終止的其他某個對象或類的終結操作執行了某個操作。finalize() 方法可以採取任何操作,其中包括再次使此對象對其他線程可用;不過,finalize()的主要目的是在不可撤消地丟棄對象之前執行清除操作。

      finalize ()方法執行非特殊性操作,它僅執行一些常規返回。Object 的子類可以重寫此定義。Java 不保證哪個線程將調用某個給定對象的 finalize 方法,但可以保證在調用 finalize()時,調用 finalize()的線程將不會持有任何用戶可見的同步鎖定。如果 finalize()方法拋出未捕獲的異常,那麼該異常將被忽略,並且該對象的終結操作將終止。

      在啓用某個對象的 finalize() 方法後,將不會執行進一步操作,直到 Java 虛擬機再次確定尚未終止的任何線程無法再通過任何方法訪問此對象,其中包括由準備終止的其他對象或類執行的可能操作,在執行該操作時,對象可能被丟棄。

      對於任何給定對象,Java 虛擬機最多隻調用一次 finalize() 方法。


發佈了69 篇原創文章 · 獲贊 81 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章