尽管Object
是一个具体类,但是设计它主要是为了扩展。它所有的非final
方法(equals
、hashCode
、toString
、clone
和finalize
)都有明确的通用约定,这些方法被设计成要被覆盖的。任何一个类,在覆盖这些方法的时候,都有责任遵守这些通用约定;如果不能做到这一点,其他依赖于这些约定的类就无法结合该类一起正常工作。
有许多覆盖equals
方法的方式会导致错误,并且后果非常严重。最容易避免这类问题的办法就是不覆盖equals
方法,在这种情况下,类的每个实例都只与它自身相等。
不需要覆盖equals
方法的情况
类的每个实例本质上都是唯一的。对于代表活动实体而不是值的类来说确实如此,例如Thread
。Object
提供的equals
实现对于这些类来说是正确的行为。
不关心类是否提供“逻辑相等”的测试功能。java.util.Random
可以覆盖equals
,以检查两个Random
实例是否产生相同的随机数序列,但是这样的功能是没有价值的。
超类已经覆盖了equals
,从超类继承而来的行为对于子类也是合适的。Set
实现都从AbstractSet
继承了equals
的实现;List
实现从AbstractList
继承equals
实现;Map
实现从AbstractMap
继承equals
实现。
类是私有的或包级私有的,可以确定它的equals
方法永远不会被调用。必须覆盖equals
方法,以防止它被意外调用:
实例受控的值类不需要覆盖equals
方法,因为实例受控的值类可以确保“每个值至多只存在一个对象”。例如枚举类型。实例受控的值类的实例逻辑相同与对象等同是一回事。
需要覆盖Object.equals
的情况
值类——类具有自己特有的“逻辑相等”的概念(不同于对象等同),而且超类没有覆盖equals
实现期望的行为,这时需要覆盖equals
方法。
值类:仅仅是一个表示值的类,例如Integer
或Date
。
使用equals
比较值对象的引用,是比较它们在逻辑上是否相等,而不是确认它们是否指向同一个对象。
覆盖equals
方法的目的
覆盖equals
方法,要让该类的实例可以做Map
的键,或是Set
的元素,使映射或集合表现出预期的行为。
equals
方法的通用约定
自反性:对于任何非null
的引用值x
,x.equals(x)
必须返回true
。
对称性:x
、y
、z
都是非null
,如果x.equals(y) == true
,y.equals(z) == true
,那么x.equals(z) == true
。
一致性:非null
的x
、y
,只要equals
方法所用的对象属性没有被修改,那么多次调用x.equals(y)
必定返回true
或false
。
非null
的x
,x.equals(null) == false
。
必须严格遵守通用规定
没有那个类是孤立的。
一个类的实例会被频繁地传递给另一个类的实例。很多类,包括所有的集合类,都依赖于传递给它们的对象是否遵守了equals
约定。
通用约定的详解解读——自反性
对象必须等于自身。
通用约定的详解解读——对称性
任何两个对象对于它们是否相等必须保持一致。
这个类企图与普通的字符串对象进行互操作。
cis.equals(s) == true
但s.equals(cis) == false
,这违反了对称性。
把违反了
equals
的对称性的类的实例加入集合中,其行为是不可预测的(取决于是集合调用cis.equals(s)
还是s.equals(cis)
)。一旦违反了
equals
约定,当其他对象面对你的对象时,你完全不知道这些对象的行为会怎么样。
因此建议把企图与String
互操作的代码从equals
方法中去掉:
通用约定的详解解读——传递性
子类增加的信息会影响到equals
的比较结果。
扩展该类:
直接继承Point
的equals
方法会忽略掉颜色信息,这是无法接受的。
问题:
p.equals(cp) == true
而cp.equals(p) == false
。解决办法:
上面的解决方案提供了对称性,却牺牲了传递性。父类的
equals
方法必定适合于子类的实例。
p1.equals(p2)==rue
,p2.equals(p3)==true
,而p1.equals(p3)== false
。我们无法在扩展可实例化的类的时候既增加新的值组件,同时又保留
equals
约定。
使用getClass
测试代替instance
测试:
只有当对象具有相同的实现时,才能使对象等同。
通过在不添加值组件的方式扩展了
Point
:
里氏替换原则:一个类型的任何重要属性也将适用于它的子类型,因此为该类型编写的任何方法,在它的子类型上也应该同样运行得很好。
将CounterPoint
实例传递给onUnitCircle
方法,onUnitCircle
方法将返回false
。
通用约定地详解解读——传递性——子类添加值组件的权宜之计
java.sql.Timestamp
扩展了java.util.Date
,并添加了nanoseconds
域。Timestamp
的equals
实现违反了对称性,因此不可以混用Timestamp
和Date
对象。
java.sql.Timestamp
这种行为是错误的,不值得效仿。
通用约定地详解解读——传递性——抽象类
可以在抽象类的子类中增加新的值组件,不会违反equals
约定。
抽象类Shape
,子类Circle
添加radius
属性,子类Rectangle
添加length
和width
属性,只要不可以直接创建超类的实例,就不会有违反传递性。
通用约定的详解解读——一致性
如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象(或者两个都)被修改了。
不可变类:相等的对象永远相等,不相等的对象永远不相等。
无论类是否可变不可变,都不要使equals
方法依赖于不可靠的资源。如果违反了,想要满足一致性的要求就十分困难了。
java.net.URL
的equals
方法依赖于URL中主机IP地址的比较。主机是可以改变了IP的地址,因此随着时间的推移,equals
不确保会产生相同的结果。
通用约定地详解解读——非空性
所有的对象都必须不等于null
。
通用约定不允许equals
方法抛出空指针异常。
这项测试是不必要的。
instanceof
的第一个操作数是null
,那么,不管第二个操作数是哪种类型,instanceof
操作符都返回false
。
因为把null
传给equals
方法,类型检查就会返回false
,所以不需要单独的null
检查。
如何写出高质量的equals
方法——使用==
操作符检查“参数是否为这个对象的引用”
优化性能
如何写出高质量的equals
方法——使用instanceof
操作符检查“参数是否为正确的类型”
正确的类型是指equals
方法所在的那个类。有些情况下,是指该类所实现的某个接口。如果类实现的接口改进了equals
约定,允许在实现了该接口的类之间进行比较,那么就使用接口,例如集合接口(Set
、List
、Map
、Map.Entry
)。
如何写出高质量的equals
方法——把参数转换成正确的类型
转换之前必须进行instanceof
测试
如何写出高质量的equals
方法——对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配
全部测试通过,则返回true
,否则返回false
。
如果类型是接口,就必须通过接口方法访问该参数中的域;如果该类型是个类,也许能够直接访问参数中的域,这要取决于它们的可访问性。
对于既不是float
也不是double
类型的基本类型域,可以使用==
操作符进行比较;对于对象引用域,可以递归地调用equals
方法;对于float
域,可以使用Float.compare
方法;对于double
域,则使用Double.compare
。对于float
和double
域进行特殊的处理是有必要的,因为存在着Float.NaN
、-0.0f
以及类似的double
常量。
对于数组域,则要把以上这些指导原则应用到每个元素上。如果数组域的每个元素都很重要,就可以使用Arrays.equals
方法。
有些对象引用域为null
是合法的,所以为了避免空指针异常,习惯使用如下的做法:
如果field
和o.field
通常是相同的对象引用,推荐使用如下的做法:
域的比较顺序可能会影响到equals
方法的性能。为了获得最佳的性能,应该最先比较最有可能不一致的域,或者是开销最低的域,最理想的情况下是两个条件同时满足的域。
不需要比较不属于对象逻辑状态的域。
不需要比较冗余的域,冗余域可以由“关键域”计算获得。但是比较冗余域有可能会提高equals
方法的性能。如果冗余域代表了整个对象的综合描述,比较这个域可以节省当比较失败时去比较实际数据所需要的开销。
如何写出高质量的equals
方法——当你编写完了equals
方法,应该问自己三个问题:它是否是对称的、传递的、一致的?
最好编写单元测试进行测试。
自反性和非空性通常会自动满足。
告诫
覆盖equals
时总要覆盖hashCode
。
不要企图让equals
方法过于智能。
File
类不应该试图把指向同一个文件的符号链接当做相等的对象来看待。
不要将equals
声明中的Object
对象替换为其他的类型。
这是重载,不是覆盖。
在原有的equals
方法的基础上,再提供一个“强类型”的equals
方法,只要这两个方法返回同样的结果,那么这是可以接受的。在特定的情况下,也许能够稍微改善性能,但是与增加的复杂度相比,这种做法是不值得的。
推荐覆盖equals
方法的时候加上@Override
注解。