一种Java版的规范的`equals()`
来源:互联网 发布:don t starve mac 编辑:程序博客网 时间:2024/04/29 06:26
一种Java版的规范的equals()
原文:A Canonical equals() For Java
尽管有Java7种Objects.equals()方法的帮助,equals()方法仍然经常被写出冗余和混乱的范儿。本文将演示如何把equals()方法写得精炼到肉眼即可检查。
当你写一个类的时候,它自动继承Object
类。如果你不重写equals()
方法,你将默认使用Object.euqals()
方法。它默认比较内存地址,所以只有当你比较 完全相同 的两个对象时,你才能得到true
返回值。这种方案是“最有鉴别能力的”。
// DefaultComparison.javaclass 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:truefalse*/
通常你想放开这种限制。典型的,如果两个对象具有相同类型,所有字段具有相同值,就可以认为这两个对象对等,但有时候你不想在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.javaimport java.util.*;interface EqualityFactory { Equality make(int i, String s, double d);}
现在我们将定义Equality
,它包含3个字段(我们认为在比较时它们全部重要)和euqals()
方法满足上述的四项测试。构造器会输出类名,这样我们在测试时可以确保类型的正确:
// Equality.javaimport 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: falseExpected false, got false-- Testing same object --same object instanceof Equality: trueExpected true, got true-- Testing different type --different type instanceof Equality: falseExpected false, got false-- Testing same values --same values instanceof Equality: trueExpected true, got true-- Testing different values --different values instanceof Equality: trueExpected false, got false*/
testAll()
方法用我们能想到的所有不同类型的对象来执行比较。它用工厂构造Equality
对象。
在main()
方法里,注意对testAll()
方法调用的简化。因为EqualityFactory
只有单个方法,可以使用兰布达表达式定义make()
方法实现。
上面的equals()
方法臃肿的令人心烦,幸好它可以被简化成一种规范的形式。经研究发现:
- 类型检查
instanceOf
消除了空值null
检查的必要性 - 对
this
的比较是多余的,一个正确实现的equals()
方法对自我比较一定没问题
因为&&
是短路比较,当它首次遇到一个失败时会退出并返回一个false
。所以,通过&&
将这些检查串联起来,我们可以将equals()
方法写得更精炼:
// SuccinctEquality.javaimport 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: falseExpected false, got false-- Testing same object --same object instanceof Equality: trueExpected true, got true-- Testing different type --different type instanceof Equality: falseExpected false, got false-- Testing same values --same values instanceof Equality: trueExpected true, got true-- Testing different values --different values instanceof Equality: trueExpected false, got false*/
对每个SuccinctEquality
,基类构造器先于衍生类构造器调用。输出显示我们得到的结果依然正确。你可以看到短路发生在空置null
检测和“不同类型”检测,否则equals()
方法里比较列表下面的测试将在类型转换时抛出异常。
当你用别的类来组装你的类时,Objects.euqals()
就变得耀眼了:
// ComposedEquality.javaimport 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: falseExpected false, got false-- Testing same object --same object instanceof Equality: trueExpected true, got true-- Testing different type --different type instanceof Equality: falseExpected false, got false-- Testing same values --same values instanceof Equality: trueExpected true, got true-- Testing different values --different values instanceof Equality: trueExpected false, got false*/
注意对super.equals()
的调用——不必重新发明轮子(并且你并不总是有权限访问基类的所有必要组成部分)。
子类间的比较
继承暗示当两个不同的子类向上塑型时可能变得“对等”。假设你有一个Pet
对象的集合,这个集合天然地接受Pet
的子类:在这个例子里,可以是Dog
和Pig
。每个Pet
有一个name
和size
,还有一个内部唯一标识id
。
我们用Objects
类来规范化的定义equals()
和hashCode()
方法,但我们只在基类Pet
里定义它们,并且它们都不包含id
。从equals()
方法的角度看,这意味着对象是否是Pet
,而不关心它是哪个特定种类的Pet
:
// SubtypeEquality.javaimport 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.javaimport 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 a752aeeePig2[1]: Ralph MEDIUM a752aeee*/
注意hashCode()
方法是相同的,但两个对象已经不再对等了,所以都出现在HashSet
里。并且,super.equals()
意味着我们不必访问基类的私有字段。
对此的一种解释是Java通过对hashCode()
和equals()
的定义分离了可替代性。我们依然可以把Dog
和Pig
放进一个Set
里而不管hashCode()
和equals()
如何定义,但对象在哈希化的数据结构里不会表现正确,除非这些方法用哈希化结构在心里定义了。不幸的是,equals()
不只是与hashCode()
方法关联使用。这使得当你试图避免为某些类定义它时事情更复杂了,这就是规范化的价值了。但是,这也使事情进一步复杂了,因为有时你也不需要定义这些方法。
- 一种Java版的规范的`equals()`
- Java语言规范要求equals方法应具有的特性
- java语言规范要求equals方法具有下面的特性
- 一种常见的Java编程错误:没有同时定义equals()和hashCode()方法
- java --Arrays的equals
- java 的 equals()方法
- java equals的用法
- java的equals方法
- Effective Java Item8-在覆盖equals(Object类的nonfinal方法)时遵循接口规范
- Java语言规范要求equals方法具有的特性以及实现方式
- XML:规范的一种标准表示法
- IOS 规范设置指针的一种方法
- 深入Java的equals方法
- Java的equals方法使用方法
- JAVA中equals的用法
- 浅析Java的equals方法
- java equals方法的覆盖
- JAVA中equals的用法
- 存储与服务器的连接方式对比(DAS,NAS,SAN)
- View事件分发机制
- ios获取xib布局后的视图frame
- ssm框架整合配置
- Ubuntu15.04中测试安装的opencv2.4.9 时遇到usr/bin/ld: cannot find -lcufft解决方法。
- 一种Java版的规范的`equals()`
- [POJ2249]计算组合数
- Gradle实战:Android多渠道打包方案汇总
- mvc和mvvm档板数据
- QT中子目录调用另一个子目录
- [BZOJ2648]SJY摆棋子(kd-tree)
- Java基础-11总结Eclipse使用,API,Object类
- 硬盘模式兼容性问题不难解决,我们将新电脑的主板设置调整为IDE模式,进入操作系统,修改注册表让操作系统和AHCI匹配:
- BootstrapValidator验证表单注意事项!