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());
}
}
结果
捐赠
若你感觉读到这篇文章对你有启发,能引起你的思考。请不要吝啬你的钱包,你的任何打赏或者捐赠都是对我莫大的鼓励。