5.2 Object:宇宙超类
Object类是最终祖先——Java中每个类都扩展Object。但是,你不必写
public class Employee extends Object
如果没有明确提到超类,那么最终的超类Object被认为是理所当然的。由于Java中的每一个类都扩展Object,因此熟悉Object类提供的服务是很重要的。我们将详细介绍本章中的基本内容;查阅后面的章节或查看在线文档,了解此处未涉及的内容。(只有在处理并发性时才会出现几个Object方法,请参阅第12章。)
5.2.1 Object类型的变量
可以使用Object类型的变量引用任何类型的对象:
Object obj = new Employee("Harry Hacker", 35000);
当然,一个Object类型的变量只能作为任意值的通用容器使用。要对值执行任何特定操作,您需要了解原始类型并应用强制转换:
Employee e = (Employee) obj;
在Java中,只有基元类型(数字、字符和布尔值)的值不是对象。
所有数组类型,不管它们是对象数组还是基元类型数组,都是扩展Object类的类类型。
Employee[] staff = new Employee[10];
obj = staff; // OK
obj = new int[10]; // OK
C++注意
在C++中,没有宇宙根类。但是,每个指针都可以转换为void*指针。
5.2.2 equals方法
Object类中的equals方法测试一个对象是否被视为等同于另一个对象。在对象类中实现的equals方法确定两个对象引用是否相同。如果两个对象相同,那么这是一个相当合理的默认值,它们应该是相等的。对于相当多的类,不需要其他任何东西。例如,比较两个PrintStream对象是否相等没有什么意义。但是,您通常希望实现基于状态的平等测试,在这种测试中,当两个对象具有相同的状态时,它们被认为是相等的。
例如,如果两个员工的姓名、工资和雇用日期相同,那么让我们认为他们是相等的。(在实际的员工数据库中,比较ID更为明智。我们使用这个例子来演示实现equals方法的机制。)
public class Employee
{
. . .
public boolean equals(Object otherObject)
{
// a quick test to see if the objects are identical
if (this == otherObject) return true;
// must return false if the explicit parameter is null
if (otherObject == null) return false;
// if the classes don't match, they can't be equal
if (getClass() != otherObject.getClass())
return false;
// now we know otherObject is a non-null Employee
Employee other = (Employee) otherObject;
// test whether the fields have identical values
return name.equals(other.name)
&& salary == other.salary
&& hireDay.equals(other.hireDay);
}
}
getClass方法返回一个对象的类,我们将在本章后面详细讨论这个方法。在我们的测试中,只有属于同一类的两个对象才能相等。
提示
要防止name或hireDay为空,请使用Objects.equals方法。调用Objects.equals(a, b)如果两个参数都为空则返回true,如果只有一个参数为空则返回false,否则调用a.equals(b)。使用该方法,Employee.equals方法的最后一条语句将变为
return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);
5.2.3 相等测试与继承
如果隐式和显式参数不属于同一类,equals方法应该如何操作?这是一个有争议的领域。在前面的示例中,如果类不完全匹配,equals方法将返回false。但是许多程序员使用一个测试实例来代替:
if (!(otherObject instanceof Employee)) return false;
这使得otherObject可能属于子类。然而,这种方法会给你带来麻烦。这就是原因。Java语言规范要求equals
方法具有以下属性:
- 它是自反的(reflexive):对于任何非空引用x,x.equals(x)都应返回true。
- 它是对称的(symmetric):对于任何引用x和y,当且仅当y.equals(x)返回true时,x.equals(y)应返回true。
- 它是可传递的(transitive):对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)应返回true。
- 它是一致的(consistent):如果x和y引用的对象没有更改,那么对x.equals(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进行比较,而不考虑Manager的具体信息!突然之间,测试的例子看起来不那么吸引人。
一些作者已经记录到getClass测试是错误的,因为它违反了替换原则。一个常见的例子是AbstractSet类中的equals方法,它测试两个集合是否具有相同的元素。AbstractSet类有两个具体的子类TreeSet和HashSet,它们使用不同的算法来定位集合元素。您真的希望能够比较任何两个集合,不管它们是如何实现的。
但是,集合示例相当专业。将AbstractSet.equals声明为final是有意义的,因为没有人应该重新定义set相等的语义。(该方法实际上不是final
的。这允许子类为相等性测试实现更有效的算法。)
在我们看来,有两种不同的情况:
- 如果子类可以有自己的相等概念,那么对称性要求强制您使用getClass测试。
- 如果相等的概念在超类中是固定的,那么可以使用
instanceof
测试并允许不同子类的对象彼此相等。
在使用员工和经理的示例中,我们认为两个对象在具有匹配字段时是相等的。如果我们有两个Manager对象具有相同的名称、薪金和雇用日期,但奖金不同,我们希望它们不同。因此,我们使用getClass测试
但是假设我们使用员工ID进行相等测试。相等的概念对所有的子类都有意义。然后我们可以使用instanceof
测试,我们应该声明Employee.equals为final。
注意
标准Java库包含了150种实现equals方法,其中使用了
instanceof
,调用GetClass
,捕获ClassCastException
,或者什么也不做。查看java.sql.Timestamp类的API文档,在该文档中,实现人员会尴尬地注意到他们已经将自己画在了一个角落里。Timestamp类继承自java.util.Date,其equals方法使用instanceof测试,因此不可能重写equals,使其既对称又准确。
以下是编写完美equals方法的方法:
-
将显式参数命名为otherObject,稍后您将需要将其强制转换为应调用other的另一个变量。
-
测试this是否与otherObject相同:
if (this == otherObject) return true;
这个语句只是一个优化。在实践中,这是一个常见的情况。检查身份要比比较字段便宜得多。
-
测试otherObject是否为空,如果为空则返回false。此测试是必需的。
if (otherObject == null) return false;
-
比较this对象和otherObject的类。如果equals的语义可以在子类中更改,请使用getClass测试:
if (getClass() != otherObject.getClass()) return false;
如果所有子类都具有相同的语义,则可以使用instanceof测试:
if (!(otherObject instanceof ClassName)) return false;
-
将otherObject强制转换为你的类类型的变量:
ClassName other = (ClassName) otherObject
-
现在,根据您的相等概念,比较字段。对于基本类型字段使用==,对于对象字段使用Objects.equals。如果所有字段匹配,则返回“true”,否则返回“false”。
return field1 == other.field1 && Objects.equals(field2, other.field2) && . . .;
如果在子类中重新定义equals,请包括对super.equals(other)的调用。
提示
如果有数组类型的字段,则可以使用静态的Array.equals方法检查相应的数组元素是否相等。
小心
这是实现equals方法时的一个常见错误。你能找出问题所在吗?
public class Employee { public boolean equals(Employee other) { return other != null && getClass() == other.getClass() && Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay); } . . . }
此方法将显式参数类型声明为Employee。因此,它不会重写对象类的equals方法,而是定义一个完全不相关的方法。
您可以通过标记要用@Override重写超类方法的方法来防止此类错误:
@Override public boolean equals(Object other)
该方法报错,因为此方法不重写对象超类中的任何方法。
java.util.Array 1.2
- static boolean equals(xxx[] a, xxx[] b) 5
如果数组的长度相等,且元素在相应位置相等,则返回true。数组的组件类型xxx可以是Object、int、long、short、char、byte、boolean、float或double。
java.util.Objects 7
- static boolean equals(Object a, Object b)
返回true如果a和b都是null,false如果它们中其中一个是null,否则返回a.equals(b)
5.2.4 hashCode方法
hashCode是从一个Object继承的整数。如果x和y是两个不同的对象,那么应该对哈希代码进行打乱。很可能x.hashCode()和y.hashcode()是不同的。表5.1列出了由String类的hashCode方法产生的hashCode的一些示例。
表5.1 hashCode方法的Hash Code结果
String | Hash Code |
---|---|
Hello | 69609650 |
Harry | 69496448 |
Hacker | -2141031506 |
String类使用下面的算法来计算hash code
int hash = 0;
for (int i = 0; i < length(); i++)
hash = 31 * hash + charAt(i);
hashCode方法是在Object类中定义的。因此,每个对象都有一个默认的哈希代码。该哈希代码是从对象的内存地址派生的。考虑这个例子:
var s = "Ok";
var sb = new StringBuilder(s);
System.out.println(s.hashCode() + " " + sb.hashCode());
var t = new String("Ok");
var tb = new StringBuilder(t);
System.out.println(t.hashCode() + " " + tb.hashCode());
表5.2显示了结果
表5.2 String和String Builders的Hash Codes
Object | Hash Code | Object | Hash Code |
---|---|---|---|
s | 2556 | t | 2556 |
sb | 20526976 | tb | 20527144 |
请注意,字符串s和t具有相同的哈希代码,因为对于字符串,哈希代码是从其内容派生的。字符串生成器sb和tb具有不同的哈希代码,因为没有为StringBuilder类定义哈希代码方法,对象类中的默认哈希代码方法从对象的内存地址派生哈希代码。
如果重新定义equals方法,还需要为用户可能插入哈希表的对象重新定义hashCode方法。(我们在第9章讨论哈希表。)
hashCode方法应返回一个整数(可以是负数)。只需将实例字段的哈希代码组合起来,就可以使不同对象的哈希代码广泛分散。
例如,下面是Employee类的hashCode方法:
public class Employee
{
public int hashCode()
{
return 7 * name.hashCode()
+ 11 * new Double(salary).hashCode()
+ 13 * hireDay.hashCode();
}
. . .
}
但是,你可以做得更好。首先,使用空安全方法Objects.hashCode。如果参数为null,则返回0,否则返回对该参数调用哈希代码的结果。另外,使用static Double.hashCode
方法避免创建Double对象:
public int hashCode()
{
return 7 * Objects.hashCode(name)
+ 11 * Double.hashCode(salary)
+ 13 * Objects.hashCode(hireDay);
}
更好的是,当您需要组合多个哈希值时,可以调用Objects.hash及其所有值。它将为每个参数调用Objects.hashCode并组合这些值。那么Employee.hashCode方法就是
public int hashCode()
{
return Objects.hash(name, salary, hireDay);
}
equals和hashCode的定义必须兼容:如果x.equals(y)为true,则x.hashCode()必须返回与y.hashCode()相同的值。例如,如果定义Employee.equals来比较雇员ID,那么hashCode方法需要散列ID,而不是雇员名称或内存地址。
提示
如果有数组类型的字段,可以使用静态
Array.hashCode
方法计算由数组元素的哈希代码组成的哈希代码。
java.lang.Object 1.0
- int hashCode()
返回此对象的哈希代码。哈希代码可以是任何整数、正数或负数。相等的对象需要返回相同的哈希代码。
java.util.Objects 7
- static int hash(Object… objects)
返回从所有提供的对象的哈希代码组合而成的哈希代码。 - static int hashCode(Object a)
如果a为空,则返回0,否则返回a.hashCode()。
java.lang.(Integer|Long|Short|Byte|Double|Float|Character|Boolean) 1.0
- static int hashCode(xxx value) 8
返回给定值的哈希代码。这里,xxx是对应于给定包装器类型的原语类型。
java.util.Arrays 1.2
- static int hashCode(xxx[] a) 5
计算数组A的哈希代码。数组的组件类型xxx可以是Object、int、long、short、char、byte、boolean、float或double。
5.2.5 toString方法
Object中的另一个重要方法是toString方法,它返回一个表示此对象值的字符串。这是一个典型的例子。Point类的toString方法返回如下字符串:
java.awt.Point[x=10,y=20]
大多数(但不是全部)toString方法遵循以下格式:类的名称,然后是括在方括号中的字段值。下面是Employee类的toString方法的实现:
public String toString()
{
return "Employee[name=" + name
+ ",salary=" + salary
+ ",hireDay=" + hireDay
+ "]";
}
实际上,你可以做得更好。不是将类名硬连接到toString方法中,而是调用getClass().getName()以获取具有类名的字符串。
public String toString()
{
return getClass().getName()
+ "[name=" + name
+ ",salary=" + salary
+ ",hireDay=" + hireDay
+ "]";
}
这种toString
方法也适用于子类。
当然,子类程序员应该定义自己的toString方法并添加子类字段。如果超类使用getClass().getName(),那么子类可以简单地调用super.toString()。例如,下面是Manager类的toString方法:
public class Manager extends Employee
{
. . .
public String toString()
{
return super.toString()
+ "[bonus=" + bonus
+ "]";
}
}
现在,Manager对象打印为
Manager[name=. . .,salary=. . .,hireDay=. . .][bonus=. . .]
toString
方法是普遍存在的一个重要原因:每当对象由“+”操作符与字符串连接时,Java编译器自动调用toString方法来获得对象的字符串表示形式。例如:
var p = new Point(10, 20);
String message = "The current position is " + p;
// automatically invokes p.toString()
提示
您可以编写"" + x,而来代替编写x.toString()。此语句将空字符串与x的字符串表示形式连接起来,x的字符串表示形式正好是x.toString()。与toString不同,如果x是基元类型,则此语句还是有效。
如果x是任何对象,并且您调用
System.out.println(x);
然后println方法简单地调用x.toString()并打印结果字符串。
Object类定义toString方法以打印对象的类名和哈希代码。例如,调用
System.out.println(System.out)
生成如下所示的输出:
java.io.PrintStream@2f6684
原因是PrintStream类的实现程序不需要重写toString方法。
小心
令人讨厌的是,数组从对象继承了toString方法,增加了数组类型古怪的打印的格式。例如,
int[] luckyNumbers = { 2, 3, 5, 7, 11, 13 }; String s = "" + luckyNumbers;
生成字符串“[I@1A46E30”。(前缀
[i
表示整数数组。)补救方法是调用静态Array.toString
方法。代码String s = Arrays.toString(luckyNumbers);
生成字符串“[2,3,5,7,11,13]”。
要正确打印多维数组(即数组数组),请使用Array.deepToString。
toString方法是一种很好的日志工具。标准类库中的许多类都定义了toString方法,这样您就可以获得有关对象状态的有用信息。这在记录如下消息时特别有用:
System.out.println("Current position = " + position);
正如我们在第7章中所解释的,更好的解决方案是使用Logger类的对象并调用
Logger.global.info("Current position = " + position);
提示
我们强烈建议您向编写的每个类添加toString方法。您以及其他使用类的程序员将感谢日志支持。
清单5.8中的程序测试类Employee(清单5.9)和Manager(清单5.10)的equals、hashCode和toString方法。
清单5.8 equals/EqualsTest.java
package equals;
/**
* This program demonstrates the equals method.
* @version 1.12 2012-01-26
* @author Cay Horstmann
*/
public class EqualsTest
{
public static void main(String[] args)
{
var alice1 = new Employee("Alice Adams", 75000, 1987, 12, 15);
var alice2 = alice1;
var alice3 = new Employee("Alice Adams", 75000, 1987, 12, 15);
var 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);
var carl = new Manager("Carl Cracker", 80000, 1987, 12, 15);
var boss = new Manager("Carl 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());
}
}
清单5.9 equals/Employee.java
package equals;
import java.time.*;
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 byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
public boolean equals(Object otherObject)
{
// a quick test to see if the objects are identical
if (this == otherObject) return true;
// must return false if the explicit parameter is null
if (otherObject == null) return false;
// if the classes don't match, they can't be equal
if (getClass() != otherObject.getClass()) return false;
// now we know otherObject is a non-null Employee
var other = (Employee) otherObject;
// test whether the fields have identical values
return Objects.equals(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 + "]";
}
}
清单5.10 equals/Manager.java
package 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;
var other = (Manager) otherObject;
// super.equals checked that this and other belong to the same class
return bonus == other.bonus;
}
public int hashCode()
{
return java.util.Objects.hash(super.hashCode(), bonus);
}
public String toString()
{
return super.toString() + "[bonus=" + bonus + "]";
}
}
java.lang.Object 1.0
- Class getClass()
返回包含有关该对象的信息的类对象。正如您将在本章后面看到的,Java对类类中封装的类具有运行时表示形式。 - boolean equals(Object otherObject)
比较两个对象是否相等;如果对象指向同一内存区域,则返回true,否则返回false。您应该在自己的类中重写这个方法。 - String toString()
返回表示此对象值的字符串。您应该在自己的类中重写这个方法。
java.lang.Class 1.0
- String getName()
返回此类的名称。 - Class getSuperclass()
将此类的超类作为类对象返回。