1. 逻辑等价约定
翻阅 Objects 源码,可以看到 Objects 定义了 equals() 方法,用于指示其他对象是否与此对象逻辑相等。equals 方法有如下约定:
自反性: 对于任何非空引用值 x,x.equals (x)应该返回 true。
对称性: 对于任何非空引用值 x 和 y,当且仅当 y.equals (x)返回 true 时,x.equals (y)应该返回 true。
传递性: 对于任何非空引用值 x、 y 和 z,如果 x.equals (y)返回 true,而 y.equals (z)返回 true,那么 x.equals (z)应该返回 true。
一致性: 对于任何非空引用值 x 和 y,只要 equals 的比较操作在对象中使用的信息没有被修改,对 x.equals (y)的多次调用一致地返回 true 或者一致地返回 false。
对于任何非空的引用值 x,x.equals (null)应该返回 false。
2. 继承是如何破坏约定的
如何快速编写 equals 方法:
- 使用==操作符检查“参数是否为这个对象的引用”
- 使用instanceof操作符检查“参数是否为正确的类型”
- 把参数转换为正确的类型
- 对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配
我们首先编写一个 Point 类:
class Point {
private final double x;
private final double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null) {
return false;
}
if(!(o instanceof Point point)) {
return false;
}
return Double.compare(this.x, point.x)==0 && Double.compare(this.y, point.y)==0;
}
@Override
public int hashCode() {
int hashcode = 0;
hashcode = 31 * hashcode + Double.hashCode(x);
hashcode = 31 * hashcode + Double.hashCode(y);
return hashcode;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
'}';
}
}
接下来我们扩展这个类,为一个点增加颜色信息:
enum Color {
BLUE, WHITE, RED, ORANGE, YELLOW, GREEN, PURPLE, BLACK
}
class ColorPoint extends Point {
private final Color color;
public ColorPoint(double x, double y, Color color) {
super(x, y);
this.color = color;
}
@Override
public String toString() {
return "ColorPoint{" +
super.toString() + ", color="
+ color + '}';
}
}
1)违反对称性
我们这样覆写 equals 方法:只有当比较对象为有色点且具有同样的位置和颜色时,才返回 true
@Override
public boolean equals(Object o) {
if(o == this) {
return true;
}
if(!(o instanceof ColorPoint p)) {
return false;
}
return super.equals(o) && this.color==p.color;
}
但是当我们比较有色点和无色点时,发现违反了对称性:
public class Test {
public static void main(String[] args) {
Point p1 = new Point(1,2);
Point p2 = new ColorPoint(1,2,Color.BLUE);
System.out.println(p1.equals(p2)); //true
System.out.println(p2.equals(p1)); //false
}
}
2)牺牲传递性,保全对称性
我们可以考虑这样修改 ColorPoint 类中的 equals 方法:当与无色点比较时,忽略颜色信息
@Override
public boolean equals(Object o) {
if(o == this) {
return true;
}
if(!(o instanceof Point)) {
return false;
}
//if o is a normal point
if(!(o instanceof ColorPoint p)) {
return o.equals(this);
}
//if o is a color point
return super.equals(o) && this.color==p.color;
}
这种方法确实提供了对称性,却牺牲了传递性:
public class Test {
public static void main(String[] args) {
Point p1 = new Point(1,2);
Point p2 = new ColorPoint(1,2,Color.BLUE);
Point p3 = new ColorPoint(1,2,Color.BLACK);
System.out.println(p1.equals(p2)); //true
System.out.println(p1.equals(p3)); //true
System.out.println(p2.equals(p3)); //false
}
}
3)牺牲重要属性,保全约定
我们可以考虑用 getClass 测试代替 instanceof 测试,这样可以扩展可实例化的类和增加新的值组件,同时保留 equals 约定:
// normal point
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || o.getClass() != this.getClass()) {
return false;
}
Point p = (Point) o;
return Double.compare(this.x, p.x)==0 && Double.compare(this.y, p.y)==0;
}
// color point
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || o.getClass() != this.getClass()) {
return false;
}
ColorPoint p = (ColorPoint) o;
return super.equals(p) && p.color==this.color;
}
但是一个有色点仍然是一个点,如果采用了这种方法,它就无法完成任务:
public class Test {
public static void main(String[] args) {
ArrayList<Point> pointArrayList = new ArrayList<>();
pointArrayList.add(new ColorPoint(1,2,Color.BLUE));
Point searchPoint = new Point(1,2);
System.out.println(pointArrayList.contains(searchPoint));
}
}
3. 组合替代继承
class Point implements Comparable<Point>{
private final double x;
private final double y;
private static final Comparator<Point> COMPARATOR =
Comparator.comparingDouble((Point p) -> p.x)
.thenComparingDouble(p -> p.y);
public Point(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null) {
return false;
}
if(!(o instanceof Point p)) {
return false;
}
return Double.compare(this.x, p.x)==0 && Double.compare(this.y, p.y)==0;
}
@Override
public int hashCode() {
int hashcode = 0;
hashcode = 31 * hashcode + Double.hashCode(x);
hashcode = 31 * hashcode + Double.hashCode(y);
return hashcode;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
'}';
}
@Override
public int compareTo(Point p) {
return COMPARATOR.compare(this, p);
}
}
enum Color {
BLUE, WHITE, RED, ORANGE, YELLOW, GREEN, PURPLE, BLACK
}
class ColorPoint implements Comparable<ColorPoint>{
private final Point point;
private final Color color;
public ColorPoint(double x, double y, Color color) {
point = new Point(x, y);
this.color = color;
}
//return a normal point
public Point asPoint() {
return point;
}
@Override
public boolean equals(Object o) {
if(o == this) {
return true;
}
if(!(o instanceof ColorPoint p)) {
return false;
}
return p.point.equals(this.point) && this.color==p.color;
}
@Override
public int hashCode() {
return 31 * point.hashCode() + color.hashCode();
}
@Override
public String toString() {
return "ColorPoint{" +
point + ", color="
+ color + '}';
}
@Override
public int compareTo(ColorPoint p) {
return this.point.compareTo(p.asPoint());
}
}