如果在List中存放了一个对象,然后用contains方法判断该对象是否存在,contains方法返回了false,气不气!
时机
- 类具有自己特有的“逻辑相等”概念(不同于对象等同的概念)
- 超类没有覆盖equals以实现期望的行为
值类(value class):仅仅表示一个值的类,例如Integer。
程序员在利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相当,而不在意是否指向同一个对象。
覆盖equal方法:可以满足程序员的要求,也可以把类的实例当作map的key,或者是set的元素。
单例类、枚举类型不需要覆盖,逻辑相同与对象等同是一回事。
约定
自反性reflexive
x != null
, x.equals(x)
:true
对称性symmetric
1 | x!=null,y!=null |
传递性transitive
1 | x!=null,y!=null,z!=null |
一致性consistent
1 | x!=null,y!=null |
x != null
, x.quals(null)
: false
场景
自反性
把类的实例添加到集合中,该集合的contains方法将返回false
对称性
1 | public class CaseInsensitiveString { |
String类中的equals方法不知道不区分大小写的字符串。如果将这两个对象都放在集合中,结果将不可预料,true? false? runtime exception?
需要将equals方法修改为下面的方式
1 |
|
传递性
使用子类时,超类中如果新添加了一个属性,将会影响equals结果
1 | public class Point { |
1 | public class ColorPoint extends Point { |
ColorPoint没有实现equal方法,比较时,颜色信息将被忽略,不是所期望的结果。
如果将ColorPoint中增加equals方法
1 | public class ColorPoint extends Point { |
比较Point和ColorPoint,以及相反的情形时,可能会得到不同的结果。
1 | Point p = new Point(1, 2); |
修改ColorPoint的equals方法,在进行父子类比较时,忽略颜色信息。
1 | public class ColorPoint extends Point { |
ok,对称性有保证了,但是却牺牲了传递性。
1 | public static void main(String[] args) { |
这是面向对象语言中关于等价关系的一个基本问题。无法在扩展可实例化的类的同时,即增加新的属性,同时又保留equals约定。
那尝试用getClass
代替instanceof
?可以扩展可实例化的类和增加新的属性,同时保留equals约定:
1 | public class Point { |
只有当对象具有相同的实现时,才能使对象等同。但是实际情况如何呢?
假设要编写一个方法,检验某个整值点是否处于单位圆中。
1 | private static final Set<Point> unitCircle; |
通过不添加属性的方式扩展Point,判断创建的实例个数。
1 | public class CounterPoint extends Point { |
假设讲CounterPoint的实例传给了onUnitCircle方法。如果Point类使用了基于getClass的equals方法,无论CounterPoint实例的x和y值是什么,onUnitCircle方法都会返回false。因为在onUnitCircle方法所用的HashSet这样的集合,是利用equals方法检验包含条件,没有任何CounterPoint实例与任何Point对应。但是,如果在Point上使用适当的基于instanceof的equals方法,当遇到CounterPoint时,相同的onUnitCircle方法就会工作得很好。
里氏替换原则Liskov subsitution principle:一个类型的任何重要属性也将适用于它的子类型,因此为该类型编写的任何方法,在它的子类型上上也应该同样运行得很好。
虽然没有一种令人满意的办法既可以扩展不可实例化的类,又增加属性,但有一种权宜之计:复合优先于继承。改造ColorPoint:取消扩展,增加Point属性。
1 | public class ColorPoint extends Point { |
在JDK中,有一些类扩展了可实例化的类,并添加了新的属性。例如java.sql.Timestamp
对java.util.Date
进行了扩展,并增加了nanoseconds
属性。Timestamp
的equals
实现确实为了对称性,如果Timestamp和Date对象被用于同一个集合中,或者以其他方式被混合在一起,则会引起不正确的行为。Timestamp类有一个免责声明,告诫程序员不要混合使用Date和Timestamp对象。只要不混合使用,就不会有麻烦,除此之外没有其他的措施可以防止,导致的错误也很难调适。这种行为是错误的,不建议效仿。
用类层次代替标签类
可以在一个抽象类的子类中增加新的值组件,而不会违反equals约定。
1 | public abstract class Shape { |
1 | public class Circle extends Shape { |
1 | public class Rectangle extends Shape { |
只要Shape
类不能被实例化,则上述问题均不可能发生。
一致性
在写一个类的时候,应该仔细考虑它是否是不可变的。如果认为它应该是不可变的,就必须保证equals方法满足:相等的对象永远相等,不想等的对象永远不想等。
无论类是否可变,都不要使equals方法依赖于不可靠的资源
equals方法都应该对驻留在内存中的对象执行确定性的计算。
java.net.URL
的equals
方法依赖于对URL中主机IP地址的比较。但IP是会变化的,会导致一些问题,但因为兼容性的考虑,equal方法无法被改变。
非空性
很难想象什么场景下o.equals(null)
会返回true
,但抛出PNE异常倒是有可能,通常约定equals方法不抛出NPE异常,很多类会进行一个显式的判断:
1 | if(o == null){ |
其实这是没有必要的,在进行判断之前,会将参数转换为适当的类型,以调用它的访问方法或访问其属性。在转换之前,equals方法必须使用instanceof操作符,检查参数是否为正确的类型:
1 | if (!(obj instanceof Rectangle)) { |
如果漏掉这步检查,参数又为错误的类型,则会抛出ClassCastException
异常,这就违反了equals的约定。但是如果instanceof的第一个操作数为null,那么会返回指定的false。
问题
输出是什么?
1 | System.out.println(Float.NaN == Float.NaN); |
1 | false |
根据IEEE 754浮点“单一格式”位布局返回指定浮点值的表示形式,保留非数字(NaN)值。
位31(由掩码0x80000000选择的位)表示浮点数的符号。位30-23(由掩码0x7f800000选择的位)表示指数。
位22-0(由掩码0x007fffff选择的位)表示浮点数的有效数(有时称为尾数)。
如果参数为正无穷大,则结果为0x7f800000。1111111100000000000000000000000
如果参数为负无穷大,则结果为0xff800000。11111111100000000000000000000000
如果参数是NaN,则结果是表示实际NaN值的整数。
与floatToIntBits方法不同,floatToRawIntBits不会将编码NaN的所有位模式折叠为单个“规范”NaN值。
在所有情况下,结果都是一个整数,当赋予intBitsToFloat(int)方法时,它将生成一个与floatToRawIntBits的参数相同的浮点值。
诀窍
- 使用
==
操作符检查”参数是否为这个对象的引用”。 - 使用
instanceof
操作符检查”参数是否为正确类型”。- 一般情况下,正确类型为equals方法所在类
- 如果是接口,允许在实现了该接口的类之间比较,那么久使用接口(比如Set、List)。
1 | //java.util.AbstractList |
- 把参数转换为正确的类型(已经通过instanceof判断)
对于该类中的每个关键属性,检查参数中的属性是否与该对象中对应的属性匹配。
- 非float、非double的基本类型:==
- 对象的引用属性:递归调用equals
- float:Float.compare
- 存在:Float.Nan、-0.0f、+0.0f
- double:Double.compare
- 数组
- 按照上述原则逐个元素比较
- Arrays.equals
- 元素中含
null
- (field == null ? o.field == null : field.equals(o.field))
- 如果两个field是相同的对象引用
- (field == o.field || (field != null && field.equals(o.field)))
- 定制化的类(例如忽略大小写)
- 合理覆盖equals
先比较最有可能不一致的属性,或者是开销最低的属性
- 覆盖equals方法后,通过单元测试检查是否符合对称性、传递性、一致性、自反性和非空性
告诫
- 覆盖equals时总要覆盖hashCode
- 不要企图让equals方法过于智能
- 不要过度的寻求各种等价关系,比如不应该把File类指向同一个文件的符号链接当作相等的对象来看待
- 不要将equals方法的参数类型Object替换为其他类型
- 替换为其他类型就没有重写equals方法而是重载,相当于在Object类型外提供一个强类型的equals方法,除非这两个方法返回结果是一致的。