Object類是Java重所有類的始祖,在Java中每個類都是由它擴展而來的。但是並不需要這樣寫:
public class Employee extends Object
如果沒有明確地指出超類,Object就被認爲是這個類的超類。由於在Java中,每個類都是由Object類擴展而來的,所以,熟悉這個類提供的所有服務十分重要。
可以使用Object類型的變量引用任何類型的對象:
Object obj = new Employee('Harray Hacker', 350000);
當然,Object類型的變量只能用於作爲各種值的通用持有者。要想對其中的內容進行具體的操作,還需要清楚對象的原始類型,並進行相應的類型轉換:
Employee e = (Employee)obj;
在Java中,只有基本類型(primitive types)不是對象,例如,數值、字符和布爾類型的值都不是對象。
所有的數組類型,不管是對象數組還是基本類型的數組都擴展了Object類。
equals方法
Object類中的equals方法用於檢測一個對象是否等於另外一個對象。在Object類中,這個方法將判斷兩個對象是否具有相同的引用。如果兩個對象具有相同的引用,它們一定是相等的。從這點上看,將其作爲默認操作也是合乎情理的。然而,對於多數類來說,這種判斷並沒有什麼意義。例如,採用這種方式比較兩個PrintStream對象是否相等就完全沒有意義。然而,經常需要檢測兩個對象狀態的相等性,如果兩個對象的狀態相等,就認爲這兩個對象是相等的。
例如,如果兩個僱員對象的姓名、薪水、僱傭日期都是一樣的,就認爲他們是相等的。
public class Employee {
public boolean equals(Object otherObject) {
if (this == otherObject) return true;
if (otherObject == null) return false;
if (getClass() != otherObject.getClass()) return false;
Employee other = (Employee)otherObject;
return name.equals(other.name) && salary == other.salary && hireDay.equals(other.hireDay);
}
}
在子類中定義equals方法時,首先調用超類的equals。如果檢測失敗,對象就不可能相等。如果超類中的域都相等,就需要比較子類中的實例域。
public class Manager extends Employee {
public boolean equals(Object otherObject) {
if (!super.equals(otherObject)) return false;
Manager other = (Manager)otherObject;
return bonus == other.bonus;
}
}
相等測試與繼承
如果隱式和顯式的參數不屬於同一個類,equals方法將如何處理呢?這是一個很有爭議的問題。在前面的例子中,如果發現類不匹配,equals方法就返回false。但是,許多程序猿卻喜歡使用instanceof進行檢測:
if (!otherObject instanceof Employee)
這樣做不但沒有解決otherObject是子類的情況,並且還有可能招致一些麻煩。這就是建議不要使用這種處理方式的原因所在。Java語言規範要求equals方法具有下面的特性::
- 自反性:對於任何非空引用x,x.equals(x)應該返回true。
- 對稱性:對於任何引用x和y,當且僅當y.equals(x)返回true,x.equals(y)也應該返回true。
- 傳遞性:對於任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,x.equals(z)也應該返回true。
- 一致性:如果x和y引用的對象沒有發生變化,反覆調用x.equal(y)應該返回同樣的結果。
- 對於任意非空引用x,x.equals(null)應該返回false。
這些規則十分合乎情理,從而避免了類庫實現着在數據結構中定位一個元素時還要考慮調用x.equals(y),還是調用y.equals(x)的問題。
然而,就對稱性來說,當參數不屬於同一個類的時候需要仔細地思考一下,如:
e.equals(m)
這裏的e是一個Employee對象,m是一個Manager對象,並且兩個對象具有相同的姓名、薪水、僱用日期。如果在Employee.equals中用instanceof進行檢測,則返回true,然而這意味着反過來調用:
m.equals(e)
也需要返回true。對稱性不允許這個方法調用返回false,或者拋出異常。
這就使得Manager類受到了束縛。這個類的equals方法必須能夠用自己與任何一個Employee對象進行比較,而不考慮經理擁有的那部分特有信息!猛然間會讓人感覺instanceof測試並不是完美無瑕。
下面給出編寫一個完美的equals方法的建議:
1). 顯式參數命名爲otherObject,稍後需要將它轉換成另一個叫做other的變量。
2).檢測this與otherObject是否引用同一個對象:
if (this == otherObject) return true;
這條語句只是一個優化。實際上,這是一種經常採用的形式。因爲計算這個等式要比一個一個地比較類中的域所付出的代價小很多。
3). 檢測ohterObject是否爲null,如果爲null,返回false。這項檢測是很必要的。
if (otherObject == null) return false;
4).比較this與otherObject是否屬於同一個類。如果equals的語義在每個子類中有所改變,就使用getClass檢測:
if (getCalss() != otherObject.getClass()) return false;
如果所有的子類都擁有統一的語義,就使用instanceof檢測:
if (!(otherObject instanceof ClassName)) return false;
5).將otherObject轉換爲相應的類類型變量:
ClassName other = (ClassName)otherObject;
6).現在開始對所有需要比較的域進行比較。使用==比較基本類型域,使用equals比較對象域。如果所有的域都匹配,就返回true,否則返回false。
7).如果在子類中重新定義equals,就要在其中包含調用super.equals(otherObejct);
hashCode方法
散列碼(hash code)是由對象導出的一個整型值。散列碼是沒有規律的。如果x和y是兩個不同的對象,x.hashCode()與y.hashCode()基本上不會相同。
由於hashCode方法定義在Object類中,因此每個對象都有一個默認的散列碼,其值爲對象的存儲地址。如:
String s = "OK";
StringBuilder sb = new StringBuilder(s);
System.out.println(s.hashCode() + " " + sb.hashCode());
String t = new String("OK");
StringBuilder tb = new StringBuilder(t);
System.out.println(t.hashCode() + " " + tb.hashCode());
結果如下:
請注意,字符串s與t擁有相同的散列碼,這是因爲字符串的散列碼是由內容導出的。而字符串緩衝sb與tb卻有這不同的散列碼,這是因爲在StringBuffer類中沒有定義hashCode方法,它的散列碼是由Object類的默認hashCode方法導出的對象存儲地址。
如果重新定義equals方法,就必須重新定義hashCode方法,以便用戶可以將對象插入到散列表。
hashCode方法應該返回一個整型數值(也可以是負數),併合理地組合實例域的散列碼,以便能夠讓各個不同的對象產生的散列碼更加均勻。如,Employee類的hashCode方法:
public int hashCode() {
return 7 * Objects.hashCode(name) + 11 * Double.hashCode(salary) + 13 * Objects.hashCode(hireDay);
}
還有更好的做法,需要組合多個散列值,可以調用Objects.hash並提供多個參數。這個方法會對各個參數調用Objects.hashCode,並組合這些散列值。可以簡單的寫爲:
public int hashCode() {
return Objects.hash(name, salay, hireDay);
}
equals與hashCode的定義必須一致:如果x.equals(y)返回true,那麼x.hashCode()就必須與y.hashCode()具有相同的值。如:如果用定義的Employee.equals比較僱員的ID,那麼hashCode方法就需要散列ID,兒不是僱員的姓名或存儲地址。
toString方法
在Object中還有一個重要的方法,就是toString方法,它用於返回表示對象值的字符串。下面是一個典型的例子。Point類的toString方法將返回下面這樣的字符串:
java.awt.Point[x=10,y=20]
絕大多數(但不是全部)的toString方法都遵循這樣的格式:類的名字,隨後是一對方括號括起來的域值。如:Employee類中的toString方法:
public String toString() {
return getClass().getName() + "[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay + "]";
}
toString方法也可以提供給子類調用。
當然,設計子類的程序員也應該定義自己的toString方法,並將子類域的描述添加進去。如果超類使用了getClass().getName(),那麼子類只要調用super.toString()就可以了。如:
public class Manager extends Employee {
public String toString() {
return super.toString() + "[bonus=" + bonus + "]";
}
}
隨處可見toString方法的主要原因是:只要對象與一個字符串通過操作符"+"連接起來,Java編譯器就會自動地調用toString方法,以便獲得這個對象的字符串描述。
如果x是任意一個對象,並調用System.out.println(x)
,println方法就會直接地調用x.toString(),並打印輸出得到的字符串。
toString方法是一種非常有用的調試工具。在標準類庫中,許多類都定義了toString方法,以便用戶能夠獲得一些有關對象狀態的必要信息。
實例
Employee.java
package cn.freedompc.equals;
import java.time.LocalDate;
import java.util.Objects;
public class Employee {
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String name, double salary, int year, int month, int day) {
this.name = name;
this.salary = salary;
hireDay = LocalDate.of(year, month, day);
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
public void raiseSalary(double byPersent) {
double raise = salary * byPersent / 100;
salary += raise;
}
public boolean equals(Object otherObject) {
if (this == otherObject) return true;
if (otherObject == null) return false;
if (getClass() != otherObject.getClass()) return false;
Employee other = (Employee)otherObject;
return Objects.deepEquals(name, other.name) && salary == other.salary
&& Objects.equals(hireDay, other.hireDay);
}
public int hashCode() {
return Objects.hash(name, salary, hireDay);
}
public String toString() {
return getClass().getName() + "[name=" + name + ",salary=" + salary + ","
+ "hireDay=" + hireDay + "]";
}
}
Manager.java
package cn.freedompc.equals;
public class Manager extends Employee {
private double bonus;
public Manager(String name, double salary, int year, int month, int day) {
super(name, salary, year, month, day);
bonus = 0;
}
public double getSalary() {
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double bonus) {
this.bonus = bonus;
}
public boolean equals(Object otherObject) {
if (!super.equals(otherObject)) return false;
Manager other = (Manager)otherObject;
return bonus == other.bonus;
}
public int hashCode() {
return super.hashCode() + 17 * new Double(bonus).hashCode();
}
public String toString() {
return super.toString() + "[bonus=" + bonus +"]";
}
}
EqualsTest.java
package cn.freedompc.equals;
public class EqualsTest {
public static void main(String[] args) {
Employee alice1 = new Employee("Alice Adams", 75000, 1987, 12, 15);
Employee alice2 = alice1;
Employee alice3 = new Employee("Alice Adams", 75000, 1987, 12, 15);
Employee bob = new Employee("Bob Brandson", 50000, 1989, 10, 1);
System.out.println("alice1 == alice2:" + (alice1 == alice2));
System.out.println("alice1 == alice3:" + (alice1 == alice3));
System.out.println("alice1.equals(alice3):" + alice1.equals(alice3));
System.out.println("alice1.equals(bob):" + alice1.equals(bob));
System.out.println("bob.toString():" + bob);
Manager carl = new Manager("Cral Cracker", 80000, 1987, 12, 15);
Manager boss = new Manager("Cral Cracker", 80000, 1987, 12, 15);
boss.setBonus(5000);
System.out.println("boss.toString():" + boss);
System.out.println("carl.equals(boss):" + carl.equals(boss));
System.out.println("alice1.hashCode():" + alice1.hashCode());
System.out.println("alice3.hashCode():" + alice3.hashCode());
System.out.println("bob.hashCode():" + bob.hashCode());
System.out.println("carl.hashCode():" + carl.hashCode());
}
}
結果
捐贈
若你感覺讀到這篇文章對你有啓發,能引起你的思考。請不要吝嗇你的錢包,你的任何打賞或者捐贈都是對我莫大的鼓勵。