尽管有Java7种Objects.equals()方法的帮助,equals()方法仍然经常被写出冗余和混乱的范儿。本文将演示如何把equals()方法写得精炼到肉眼即可检查。
当你写一个类的时候,它自动继承Object
类。如果你不重写equals()
方法,你将默认使用Object.euqals()
方法。它默认比较内存地址,所以只有当你比较 完全相同 的两个对象时,你才能得到true
返回值。这种方案是“最有鉴别能力的”。
// DefaultComparison.java
class DefaultComparison {
private int i, j, k;
public DefaultComparison(int i, int j, int k) {
this.i = i;
this.j = j;
this.k = k;
}
public static void main(String[] args) {
DefaultComparison
a = new DefaultComparison(1, 2, 3),
b = new DefaultComparison(1, 2, 3);
System.out.println(a == a);
System.out.println(a == b);
}
}
/* Output:
true
false
*/
通常你想放开这种限制。典型的,如果两个对象具有相同类型,所有字段具有相同值,就可以认为这两个对象对等,但有时候你不想在equals()
里比较某些字段。这是类设计流程的一部分。
一个恰当的equals()
方法必须满足5个条件:
- 自反的:对任意
x
,x.equals(x)
应该返回true
- 对称的:对任意
x
和y
,x.equals(y)
返回true
当且仅当y.equals(x)
返回true
- 传递的:对任意
x
,y
和z
,如果x.equals(y)
返回true
且y.equals(z)
返回true
,那么x.equals(z)
应当返回true
- 一致的:对任意
x
和y
,只要用于比较的对象的信息没有更改,无论调用多少次x.equals(y)
,都一致的返回true
或者一致的返回false
- 对任意非
null
的x
,x.equals(null)
都返回false
下面是一些测试满足上述条件并判断你要比较的对象(测试里叫做rval
)是否与当前对象对等:
- 如果
rval
是null
,不对等 - 如果
rval
是this
(你在用自己比较自己),对等 - 如果
rval
不是相同的类或其子类,不对等 - 如果以上测试全通过,你必须决定
rval
中哪些字段是重要的(并一致的),然后比较它们
Java7引入了Objects
类来帮助这个流程,我们可以用它来写一个更好的equals()
方法
下面的例子比较不同版本的Equality
类。为防止重复代码,我们使用工厂方法
来构建用例。这个EqualityFactory
接口只简单的定义了一个make()
方法来生成Equality
对象,所以不同的EqualityFactory
可以产生不同的Equality
子类:
// EqualityFactory.java
import java.util.*;
interface EqualityFactory {
Equality make(int i, String s, double d);
}
现在我们将定义Equality
,它包含3个字段(我们认为在比较时它们全部重要)和euqals()
方法满足上述的四项测试。构造器会输出类名,这样我们在测试时可以确保类型的正确:
// Equality.java
import java.util.*;
public class Equality {
protected int i;
protected String s;
protected double d;
public Equality(int i, String s, double d) {
this.i = i;
this.s = s;
this.d = d;
System.out.println("made 'Equality'");
}
@Override
public boolean equals(Object rval) {
if(rval == null)
return false;
if(rval == this)
return true;
if(!(rval instanceof Equality))
return false;
Equality other = (Equality)rval;
if(!Objects.equals(i, other.i))
return false;
if(!Objects.equals(s, other.s))
return false;
if(!Objects.equals(d, other.d))
return false;
return true;
}
public void
test(String descr, String expected, Object rval) {
System.out.format("-- Testing %s --%n" +
"%s instanceof Equality: %s%n" +
"Expected %s, got %s%n",
descr, descr, rval instanceof Equality,
expected, equals(rval));
}
public static void testAll(EqualityFactory eqf) {
Equality
e = eqf.make(1, "Monty", 3.14),
eq = eqf.make(1, "Monty", 3.14),
neq = eqf.make(99, "Bob", 1.618);
e.test("null", "false", null);
e.test("same object", "true", e);
e.test("different type", "false", new Integer(99));
e.test("same values", "true", eq);
e.test("different values", "false", neq);
}
public static void main(String[] args) {
testAll( (i, s, d) -> new Equality(i, s, d));
}
}
/* Output:
made 'Equality'
made 'Equality'
made 'Equality'
-- Testing null --
null instanceof Equality: false
Expected false, got false
-- Testing same object --
same object instanceof Equality: true
Expected true, got true
-- Testing different type --
different type instanceof Equality: false
Expected false, got false
-- Testing same values --
same values instanceof Equality: true
Expected true, got true
-- Testing different values --
different values instanceof Equality: true
Expected false, got false
*/
testAll()
方法用我们能想到的所有不同类型的对象来执行比较。它用工厂构造Equality
对象。
在main()
方法里,注意对testAll()
方法调用的简化。因为EqualityFactory
只有单个方法,可以使用兰布达表达式定义make()
方法实现。
上面的equals()
方法臃肿的令人心烦,幸好它可以被简化成一种规范的形式。经研究发现:
- 类型检查
instanceOf
消除了空值null
检查的必要性 - 对
this
的比较是多余的,一个正确实现的equals()
方法对自我比较一定没问题
因为&&
是短路比较,当它首次遇到一个失败时会退出并返回一个false
。所以,通过&&
将这些检查串联起来,我们可以将equals()
方法写得更精炼:
// SuccinctEquality.java
import java.util.*;
public class SuccinctEquality extends Equality {
public SuccinctEquality(int i, String s, double d) {
super(i, s, d);
System.out.println("made 'SuccinctEquality'");
}
@Override
public boolean equals(Object rval) {
return rval instanceof SuccinctEquality &&
Objects.equals(i, ((SuccinctEquality)rval).i) &&
Objects.equals(s, ((SuccinctEquality)rval).s) &&
Objects.equals(d, ((SuccinctEquality)rval).d);
}
public static void main(String[] args) {
Equality.testAll( (i, s, d) ->
new SuccinctEquality(i, s, d));
}
}
/* Output:
made 'Equality'
made 'SuccinctEquality'
made 'Equality'
made 'SuccinctEquality'
made 'Equality'
made 'SuccinctEquality'
-- Testing null --
null instanceof Equality: false
Expected false, got false
-- Testing same object --
same object instanceof Equality: true
Expected true, got true
-- Testing different type --
different type instanceof Equality: false
Expected false, got false
-- Testing same values --
same values instanceof Equality: true
Expected true, got true
-- Testing different values --
different values instanceof Equality: true
Expected false, got false
*/
对每个SuccinctEquality
,基类构造器先于衍生类构造器调用。输出显示我们得到的结果依然正确。你可以看到短路发生在空置null
检测和“不同类型”检测,否则equals()
方法里比较列表下面的测试将在类型转换时抛出异常。
当你用别的类来组装你的类时,Objects.euqals()
就变得耀眼了:
// ComposedEquality.java
import java.util.*;
class Part {
String ss;
double dd;
public Part(String ss, double dd) {
this.ss = ss;
this.dd = dd;
}
@Override
public boolean equals(Object rval) {
return rval instanceof Part &&
Objects.equals(ss, ((Part)rval).ss) &&
Objects.equals(dd, ((Part)rval).dd);
}
}
public class ComposedEquality extends SuccinctEquality {
Part part;
public ComposedEquality(int i, String s, double d) {
super(i, s, d);
part = new Part(s, d);
System.out.println("made 'ComposedEquality'");
}
@Override
public boolean equals(Object rval) {
return rval instanceof ComposedEquality &&
super.equals(rval) &&
Objects.equals(part, ((ComposedEquality)rval).part);
}
public static void main(String[] args) {
Equality.testAll( (i, s, d) ->
new ComposedEquality(i, s, d));
}
}
/* Output:
made 'Equality'
made 'SuccinctEquality'
made 'ComposedEquality'
made 'Equality'
made 'SuccinctEquality'
made 'ComposedEquality'
made 'Equality'
made 'SuccinctEquality'
made 'ComposedEquality'
-- Testing null --
null instanceof Equality: false
Expected false, got false
-- Testing same object --
same object instanceof Equality: true
Expected true, got true
-- Testing different type --
different type instanceof Equality: false
Expected false, got false
-- Testing same values --
same values instanceof Equality: true
Expected true, got true
-- Testing different values --
different values instanceof Equality: true
Expected false, got false
*/
注意对super.equals()
的调用——不必重新发明轮子(并且你并不总是有权限访问基类的所有必要组成部分)。
子类间的比较
继承暗示当两个不同的子类向上塑型时可能变得“对等”。假设你有一个Pet
对象的集合,这个集合天然地接受Pet
的子类:在这个例子里,可以是Dog
和Pig
。每个Pet
有一个name
和size
,还有一个内部唯一标识id
。
我们用Objects
类来规范化的定义equals()
和hashCode()
方法,但我们只在基类Pet
里定义它们,并且它们都不包含id
。从equals()
方法的角度看,这意味着对象是否是Pet
,而不关心它是哪个特定种类的Pet
:
// SubtypeEquality.java
import java.util.*;
enum Size { SMALL, MEDIUM, LARGE }
class Pet {
private static int counter = 0;
private final int id = counter++;
private final String name;
private final Size size;
public Pet(String name, Size size) {
this.name = name;
this.size = size;
}
@Override
public boolean equals(Object rval) {
return rval instanceof Pet &&
// Objects.equals(id, ((Pet)rval).id) && // [1]
Objects.equals(name, ((Pet)rval).name) &&
Objects.equals(size, ((Pet)rval).size);
}
@Override
public int hashCode() {
return Objects.hash(name, size);
// return Objects.hash(name, size, id); // [2]
}
@Override
public String toString() {
return String.format("%s[%d]: %s %s %x",
getClass().getSimpleName(), id,
name, size, hashCode());
}
}
class Dog extends Pet {
public Dog(String name, Size size) {
super(name, size);
}
}
class Pig extends Pet {
public Pig(String name, Size size) {
super(name, size);
}
}
public class SubtypeEquality {
public static void main(String[] args) {
Set<Pet> pets = new HashSet<>();
pets.add(new Dog("Ralph", Size.MEDIUM));
pets.add(new Pig("Ralph", Size.MEDIUM));
pets.forEach(System.out::println);
}
}
/* Output:
Dog[0]: Ralph MEDIUM a752aeee
*/
如果我们只考虑类型,那么它是有意义的——有时——只从它们的基类的立场来看,这正是 Liskov替换原则 的基础。这个代码很好的符合了这个原则,因为衍生类没有额外添加任何不在基类的方法。衍生类只在行为上不同,而不是在接口上(这当然不是通常的情况)。
但我们提供两个有着相同数据的不同的对象并把它们放到一个HashSet<Pet>
,只有一个留存下来。这凸显了equals()
方法不是一个完美的数学概念,而是(至少部分是)一种呆板的方法。在哈希化的数据结构里,hashCode()
和equals()
必须密切相关的一同定义才能恰当的工作。
在上面的例子里,Dog
和Pig
被HashSet
哈希化进了同一个篮子。在这里,HashSet
依赖equals()
方法来区分对象,但equals()
认为这两个对象对等。HashSet
不添加Pig
因为它已经有一个相同对象了。
我们依然可以通过强制区分这两个原本相等的对象来使代码工作。这里,每个Pet
已经有一个唯一id
,所以你可以取消标记[1]
处的注释或者将hashCode()
方法切换成标记[2]
的代码。在规范化方法里,你可以两者都做,在两个方法中引入所有“不变性”的字段(“不变性”使得equals()
和hashCode()
在哈希化的数据结构里存储和返回时不会产生不同的值。我对“不变性”加引号是因为你必须评估是否会发生改变)。
补充说明:在hashCode()
中,如果你只使用一个字段,用Objects.hashCode()
,如果使用多个字段,用Objects.hash()
。
我们也可以通过在子类中遵循标准形式来定义equals()
来解决这个问题(但还是不包含id
):
// SubtypeEquality2.java
import java.util.*;
class Dog2 extends Pet {
public Dog2(String name, Size size) {
super(name, size);
}
@Override
public boolean equals(Object rval) {
return rval instanceof Dog2 &&
super.equals(rval);
}
}
class Pig2 extends Pet {
public Pig2(String name, Size size) {
super(name, size);
}
@Override
public boolean equals(Object rval) {
return rval instanceof Pig2 &&
super.equals(rval);
}
}
public class SubtypeEquality2 {
public static void main(String[] args) {
Set<Pet> pets = new HashSet<>();
pets.add(new Dog2("Ralph", Size.MEDIUM));
pets.add(new Pig2("Ralph", Size.MEDIUM));
pets.forEach(System.out::println);
}
}
/* Output:
Dog2[0]: Ralph MEDIUM a752aeee
Pig2[1]: Ralph MEDIUM a752aeee
*/
注意hashCode()
方法是相同的,但两个对象已经不再对等了,所以都出现在HashSet
里。并且,super.equals()
意味着我们不必访问基类的私有字段。
对此的一种解释是Java通过对hashCode()
和equals()
的定义分离了可替代性。我们依然可以把Dog
和Pig
放进一个Set
里而不管hashCode()
和equals()
如何定义,但对象在哈希化的数据结构里不会表现正确,除非这些方法用哈希化结构在心里定义了。不幸的是,equals()
不只是与hashCode()
方法关联使用。这使得当你试图避免为某些类定义它时事情更复杂了,这就是规范化的价值了。但是,这也使事情进一步复杂了,因为有时你也不需要定义这些方法。