读书笔记 仅供参考
不覆盖 equals 方法
许多覆盖方式会导致错误,并且后果十分严重,最容易避免错误的方法就是不覆盖 equals 方法。每个类的实例都只与自身相等。
- 类的每个实例本质上都是唯一的
- 不关心类是否提供了“逻辑相等”的功能
- 超类已经覆盖了 equals,从超类继承过来的行为对子类也是合适的(例如 List 从 AbstractList 继承 equals)
- 如果类是私有的或包级私有,可以覆盖equals 方法,确保它的 equals 方法不会被意外调用
@Override
public boolean equals(Object o){
throw new AssertionError();
}
何时覆盖 equals
如果类具有自己特有的“逻辑相等”概念,而且超类还没有覆盖 equals 。
这种类属于值类,例如 Integer 或 Data,只是想知道他们在逻辑上相等,并不像知道他们是否指向同一个对象。(如果是实例受控的值类,可以不覆盖 equals,因为每个值至多存在一个对象)
覆盖 equals 的通用规定
如果违反了这些规定,程序会表现不正常,甚至崩溃。
自反性
对于任何非 null 的引用值 x,x.equals(x) 必须返回 true。
对称性
对于任何非 null 的引用值 x 和 y,并且仅当 y.equals(x) 返回 true 时,x.equals(y) 必须返回 true。
错误的例子
//实现不区分大小写的字符串
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
if(s == null) {
throw new NullPointerException();
}
this.s = s;
}
//企图与普通的字符串进行互操作
@Override
public boolean equals(Object o) {
if(o instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
}
if(o instanceof String) {
return s.equalsIgnoreCase((String) o);
}
return false;
}
}
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
cis.equals(s);//返回 true
//String 并不知道如何比较
s.equals(cis);//返回 false
解决方法:只比较 CaseInsensitiveString
传递性
对于任何非 null 的引用值 x,y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,x.equals(z) 必须返回 true。
在考虑子类的情况下,这条约定很容易违背。
超类
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof Point))
return false;
Point p = (Point) o;
return p.x ==x && p.y == y;
}
}
子类
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
//如果不提供 equals 方法,颜色信息就忽略掉了
@Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
}
在比较普通点和有色点,以及相反的情形时,会得到不同的结果,违反了对称性。
Point p = new Point(1, 2);
ColorPoint cp =new ColorPoint(1, 2, Color.RED);
p.equals(cp);//返回 true
cp.equals(p);//返回 false
改善版本
@Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint))
return false;
//是一个普通点,就忽略掉颜色信息
if(!(o instanceof ColorPoint))
return o.equals(this);
//是一个彩色点,就全比较
return super.equals(o) && ((ColorPoint) o).color == color;
}
这种方法提供了对称性,缺失了传递性
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
p1.equals(p2);//返回 true
p2.equals(p3);//返回 true
p1.equals(p3);//返回 false
我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留 equals 约定
听说:在 equals 方法中用 getClass 代替 instanceof ,可以满足上面的要求
//Point 类
@Override
public boolean equals(Object o) {
if(o == null || o.getClass() != getClass()) {
return false;
}
Point p = (Point) o;
return p.x == x && p.y ==y;
}
但是这样违反了 里氏替换原则:一个类型的任何重要属性也将适用于它的子类型。当遇到类似 HashSet 的集合时,就无法将超类和子类都放进去了。
比较好的方法
采用复合,在 ColorPoint 中加入 Point 属性。
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
if(color ==null) {
throw new NullPointerException();
}
point = new Point(x, y);
this.color = color;
}
public Point asPoint() {
return point;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
ps:抽象类的子类可以增加新的值组件,而不会违反 equals 约定。
一致性
对于任何非 null 的引用值 x 和 y,只要 equals 的比较操作在所用的信息没有被改变,多次调用 x.equals(y) 就会一直返回同一个结果
非空性
对于任何非 null 的引用值 x,x.equals(null) 必须返回 false
实现 equals 方法的诀窍
诀窍一
使用 == 操作符检查 “参数是否是这个对象的引用”
诀窍二
使用 instanceof 操作符 检查 “参数是否为正确的类型”
诀窍三
把参数转换成正确的类型。
在转化之前进行 instanceof ,所以肯定会成功
诀窍四
对于该类中的每个“关键域”,检查是否相匹配
float : Float.compare()
double:Double.compare()
如果某些引用域包含 null 合法:
(field == null? o.field == null : field.equals(o.field))
如果通常是相同的对象引用:
(field == o.field || (filed != null && field.equals(o.field)))
提高性能:
先比较最有可能不一致的域
诀窍五
当 equals 完成后,要问自己:是否对称,传递,一致?
告诫
- 覆盖 equals 时总要覆盖 hashCode
- 不要企图让 equals 方法过于智能
- 不要将 equals 声明的 Object 对象转换为其他的类型