Java 是一門典型的面嚮對象語言,提供 extends 關鍵字使子類繼承父類。
public class Student extends Person {
...
}
但是創建 Person 類時不用使用 extends 繼承 Object類。
public class Person extends Object {
...
}
因為,創建的類沒有明確指明繼承關係時,會在編譯時自動繼承 Object 類。可以使用 Object 類型的變量引用任何類型的實例:
Object sample = new Student(1001, "小咖", 20, "男");
也可以將任意的 Object 實例轉換為需要的類型:
Person p = (Person) sample;
上面兩個例子在編譯器中是不會報錯的。因此,Object 類是 Person 類的父類,每個類都是由 Object 擴展而來的。所以,熟悉 Object 類中提供的服務是十分重要的。
Java 中的基本類型不是對象,但也提供了相應的包裝類型,如 int 基本類型對應 Integer 包裝類型。但使用基本類型創建的數組是擴展自 Object 類,實際上是引用。
Object sample = new int[10];
其實,所有的數組類型都是擴展了 Object 類。
下表為 Object 類的通用方法。
| 方法 | 描述 | 異常 |
|---|---|---|
final native Class<?> getClass() |
返回對象的運行時類 | 無 |
native int hashCode() |
返回對象的散列碼 | 無 |
boolean equals(Object obj) |
與其它對象是否相等 | 無 |
native Object clone() |
克隆並返回對象的副本 | CloneNotSupportedException |
String toString() |
返回對象的字符串表示 | 無 |
final native void notify() |
喚醒正在等待對象監聽器上的一個線程 | 無 |
final native void notifyAll() |
喚醒正在等待對象監聽器上的所有線程 | 無 |
final native void wait() |
導致當前線程等待,直到另一個線程調用此對象的notify()或notifyAll() |
InterruptedException |
final native void wait(long timeout) |
導致當前線程等待,直到另一個線程調用此對象的notify()或notifyAll(),或者指定時間已到 |
InterruptedException |
final void wait(long timeout, int nanos) |
導致當前線程等待,直到另一個線程調用此對象的notify()或notifyAll(),或者指定時間已到 |
InterruptedException |
void finalize() |
當GC確定不再有對該對象的引用時,由對象的 GC 調用此方法 | Throwable |
equals()
Object 類提供了 equals() 方法用於檢測對象是否相等。其實現相等性要滿足五個條件:
- 自反性。 對於任何的非空引用都需滿足
x.equals(x) == true。 - 對稱性。 對於任何引用都滿足
x.equals(y) == y.equals(x)。 - 傳遞性。 對於任何引用都滿足
(x.equals(y) && y.equals(z)) == x.equals(z)。 - 一致性。 對於不發生任何變化的引用都滿足
x.equals(y) == x.equals(y),多次調用equals()方法結果不變。 - 與
null的比較。x.equals(null)返回false。對任何不是null的對象調用equals()方法與null比較的結果都為false。
引用類型最好使用 equals() 方法比較;而基本類型使用 == 比較。
其中,子類中定義 equals() 方法時,需先比較超類的 equals()。如果檢測失敗,對象就不可能相等。如果超類中的域都相等,就需要比較子類中的實例域。
下面可以從兩個截然不同的情況看一下這個問題:
- 如果子類能夠擁有自己的相等概念,則對稱性需求將強制採用 getClass 進行檢測。
- 如果由超類決定相等的概念,那麼就可以使用 instanceof 進行檢測,這樣可以在不同子類的對象之間進行相同的比較。
對象中使用 equals() 方法比較需要實現的步驟如下:
- 檢查是否為同一個引用,如果是直接返回
true。 - 檢測傳入的值是否為
null,如果是直接返回false。 - 檢測是否屬於同一個類型。如果不是直接返回
false。 - 將
Object對象類型轉為要比較的類型。 - 比較每個關鍵域是否相等。
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(name, person.name)
&& Objects.equals(sex, person.sex)
&& (age != person.age);
}
如果子類重新定義 equals,就要在其中包含 super.equals(other)。
hasCode()
hashCode() 返回的是整數值,是無規律的散列碼。hashCode() 定義在了 Object 類中,每個類都可以使用 hashCode()方法調用自身散列碼,其值為對象的存儲地址。
Object sample = new Student(1001, "小咖", 20, "男");
System.out.println(sample); // [I@5b464ce8
如果你在創建的類中覆蓋了 equals() 方法,就必須覆蓋 hashCode() 方法 。這是 hashCode() 的通用約定。下面是覆蓋 hashCode() 方法的約定:
- 程序執行期間,對象的
equals()方法中比較的信息不變,同一個對象的hashCode()方法的返回值也不變。兩個程序的執行期間,hashCode()方法返回的值可以不一致。 - 如果兩個對象根據
equals(Object)方法比較相等,那hashCode()方法的返回值也必須相等。 - 如果兩個對象根據
equals(Object)方法比較不相等,那hashCode()方法的返回值最好不相等。如果相等,會在使用Map時造成散列碼衝突。
總結就是兩個對象相等,其散列碼一定相同;但是散列碼相同的兩個對象並不一定相等。因為計算散列碼具有隨機性,兩個值不同的對象可能計算出相同的散列碼。
理想的散列函數是把集合中不相等的實例均勻地分佈到所有可能的 int 之上。但非常困難,只能實現相對接近這種理想的情形。
當計算散列碼時,要將每個域都考慮進去。可以將每個域都當成 R 進制的某一位,然後組成一個 R 進制的整數。
R 一般取奇數 31,偶數會出現乘法溢出,信息會丟失。因為與 2 相乘相當於向左移一位,最左邊的位丟失。並且一個數與 31 相乘可以轉換成移位和減法:31*x == (x<<5)-x,編譯器會自動進行這個優化。
如下是一個如何簡單的計算散列碼的參考:
- 基本類型調用
Type.hashCode(value)來生成。Type類型為基本類型各自的包裝類。 - 如果是引用類型,並且需要覆蓋
equals()方法,equals()方法使用哪些域比較,hashCode()方法也會遞歸地調用這些域的散列碼並計算。 - 如果是數組類型,那數組中的每個值都當做單獨的域來處理。也可以使用
Arrays.hashCode()方法計算。
不要試圖從散列碼計算中排除掉一個對象的關鍵域來提高性能。
下面重寫 Student 類的 hashCode() 方法:
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + sid;
return result;
}
hashCode() 方法返回的散列碼也可以是負數,合理地組合實例域的散列碼,以便能夠讓各個不同的對象產生的散列碼更加均勻。
toString()
默認的 toString() 方法是返回的是 java.lang.Object@511baa65 這種類型是的字符串。
Student sample = new Student(1001, "小咖", 20, "男");
System.out.println(sample); // java.lang.Object@511baa65
上面 System.out.println(sample) 會自動調用 sample.toString() 方法將值輸出到控制枱。但是,提供好的 toString() 實現可以獲取對象狀態的必要信息,也易於調試。因此建議為每個自定義的類覆蓋 toString() 方法。
下面重寫 Student 類的 toString() 方法:
@Override
public String toString() {
return getClass().getName()
+ "{ sid=" + sid
+ ", name=" + super.getName()
+ ", age=" + super.getAge()
+ ", sex=" + super.getSex()
+ " }";
}
如果父類 Person 也重寫類 toString() 方法:
@Override
public String toString() {
return getClass().getName() +
"{" + '\'' +
"name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
'}';
}
並且子類的 toString() 也可以調用:
@Override
public String toString() {
return super.toString() + "{sid = " + sid + "}";
}
// xxx.Student{'name='小咖', age=20, sex='男'}{sid = 1001}
clone()
Object 類提供了 clone() 方法用於克隆實例,但因為是 protected 修飾符所修飾的方法,因此不會顯示地覆蓋 clone() 。實現 Cloneable 接口的類可以覆蓋 clone() 方法提供克隆。如果不實現,會拋出 CloneNotSupportedException 異常。正確的寫法如下所示:
public class Person implements Cloneable {
private String name;
private int age;
private String sex;
private String[] address;
public Person(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
...省略getter與setter...
@Override
protected Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
Java 支持協變返回類型,也就是覆蓋方法的返回類型可以是被覆蓋方法的返回類型的子類。並且在 clone() 方法中調用 super.clone() 方法得到功能完整的克隆對象。
當使用 Person 創建對象並調用 clone() 方法克隆。
Person s1 = new Person("小卡", 22, "男");
s1.setAddress(new String[] {"浙江省", "江蘇省", "湖南省"});
Person s2 = sample.clone();
System.out.println(s1.hashCode()); // 873415566
System.out.println(s2.hashCode()); // 818403870
System.out.println(s1 == s2); // false
System.out.println(s1.getAddress() == s2.getAddress()); // true
從上面的代碼中得出,調用 clone() 方法獲得的對象是個新的對象,但是對象中的引用還是原來的引用,而不是新引用。這次的克隆被稱為 淺拷貝。
使用 clone() 方法與通過構造器創建對象實際上是一樣的,要確保不會傷害到原始的對象,並確保正確地創建被克隆的對象中的約束條件。這次的克隆被稱為 深拷貝。
因此,在 Person 類的內部,address 數組也要遞歸地調用 clone() 方法:
@Override
protected Person clone() throws CloneNotSupportedException {
Person result = (Person) super.clone();
result.address = address.clone();
return result;
}
記住,Cloneable 與引用可變對象的 final 域的正常用法是不兼容的。因此,實現 clone() 方法禁止給 final 賦新值。
上述的拷貝方式比較複雜。可以在類中提供一個拷貝構造器或拷貝工廠來實現克隆的替代功能。
public Person(Person value) {...}
public static Person newInstance(Person value) {...}
finalize()
當 GC 確定不再有對該對象的引用時,GC 會調用對象的 finalize() 方法來清除回收。
protected void finalize() throws Throwable { }
因此,子類可以通過覆蓋此方法處理一些額外的清理工作。 但是,finalize() 方法何時被調用取決於 Java VM,而且不保證 finalize() 方法會被及時地執行 。因此,不要依賴 finalize() 方法來更新重要的持久狀態。
Java VM 會確保一個對象的 finalize() 方法只被調用一次,而且程序中不能直接調用 finalize() 方法。
finalize() 方法通常也不可預測,而且很危險,一般情況下,不必要覆蓋 finalize() 方法。
wait 與 notify
Object 對象提供了 wait() 和 notify() 方法,這兩個方法的使用是相對的:
wait():線程進入等待狀態。notify():喚醒等待該對象的線程。
使用 wait() 方法必須在同步區域內部調用,這個同步區域將對象鎖定在調用 wait() 方法的對象上。下面是使用 wait() 方法的標準模式:
synchronized (obj) {
while (<condition does not hold>) {
obj.wait();
}
}
這時的 obj 對象所在線程會處於等待狀態,需要使用 notify() 方法喚醒 obj 所在線程。
synchronized (obj) {
obj.notify();
}
最好使用 notifyAll() 方法喚醒線程。因為總會產生正確的結果,保證會喚醒所有需要被喚醒的線程。雖然也會喚醒其他線程,但不影響程序的正確性,而且 notifyAll() 方法代替 notify() 方法可以避免來自不相關線程的意外或惡意的等待。
注意:必須使用 synchronized,否則會報 IllegalMonitorStateException 異常。
總結
這裏對 Object 類做了一個簡單的瞭解,知道 Object類是一切類的基類,可以引用一切的引用類型,包括數組類型。且 Object 中提供的方法需要在適合的場景下覆蓋得到最佳的結果,最好始終都覆蓋 toString() 方法。
更多內容請關注公眾號「海人為記」