第8条:覆盖equals时遵守通用约定

如果不需要覆盖equals方法,那么就无需担心覆盖equals方法导致的错误。

什么时候不需要覆盖equals方法?

1.类的每个实例本质上是唯一的。

例如对于Thread,Object提供的equals实现正好符合。

2.不关心类是否提供了“逻辑相等”的测试功能。

例如Random类提*生随机数的能力,如果覆盖equals,目的该是检查两个Random实例是否产生了相同的随机数列,但实际上这个比较功能是不需要的,所以从Object继承的equals是足够的。

3.超类已经覆盖了euqlas,从超类继承过来的行为对于子类也是合适的。

例如,Set实现都从AbstractSet继承euqlas实现,List实现从AbstractList继承equals实现,Map实现从AbstractMap继承equals实现。

4.类是私有的或者包级私有,可以确定它的equals方法永远不会被调用。

这种时候好的做法应该覆盖equals方法,以防被意外调用:

@Override
public boolean equals(Object o) {
    throw new AssertionError();
}

什么时候应该覆盖equals方法?

 类具有自己特有的“逻辑相等”概念(不同于对象等同),而且超类没有覆盖equals以实现期望行为,这时需要覆盖equals方法。

通常这种类是“值类”,仅仅表示值的类,如Integer,Date,在利用equals方法时比较对象引用时,希望知道它们在逻辑上是否相等(值是否相等),而不是它们是否指向同一个对象。

一种特殊的“值类”,实例受控确保“每个值至多只存在一个对象”的类,如枚举类型,对于这样的类,逻辑等同域对象等同是同样的,因此Object的equals方法就能满足,无需覆盖。

equals有一系列的通用约定,在覆盖equals方法时,必须遵守这些约定,否则在使用jdk提供的映射表,集合等类时会导致奇怪的错误。

1.自反性,对于任何非null的引用值x,x.equals(x)必须返回true。

2.对称性,对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。

3.传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)也必须返回true。

4.一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用信息没有被修改,那么多次调用x.equals(y)就会一致地返回true或一致地返回false。

对于任何非null的引用值x,x.equals(null)必须返回false。

解释约定:

1.自反性,要求对象必须等于自身,假如一个类违背这一点,把该类的实例添加到集合中,该集合的contain方法会告诉你该集合不包含刚刚添加的实例,这种情况一般不会出现。

2.对称性,对于任何两个对象是否相等,必须保持一致,考虑下面一个不区分大小写的字符串的类:

public final class CaseInsensitiveString {
    private 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";
System.out.println(cis.equals(s));
System.out.println(s.equals(cis));

cis.equals(s)返回true

s.equals(cis)返回false

问题在于CaseInsensitiveString类中的equals方法知道String对象,而String类中的equals方法却不知道CaseInsensitiveString,因此违反了对称性。

看看String的equals方法:

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
}

String的equals不知道CaseInsensitiveString是一个不区分大小写的类,只是把它当成一个Object或String。

解决这个问题的方法是把与String互操作的这段代码从equals方法中去掉。

@Override
public boolean equals(Object o) {
    return o instanceof CaseInsensitiveString &&
        ((CaseInsensitiveString) o).s.equalsIngnoreCase(s);
}

这样的CaseInsensitiveString的equals方法返回true必须它比较的对象是CaseInsensitiveString,如果比较对象不是CaseInsensitiveString,比如是String,那么它一定会返回false。

3.传递性,如果一个对象等于第二个对象,并且第二个对象又等于第三个对象,则第一个对象一定等于第三个对象。

考虑子类增加的信息会影响到equals的比较结果。

首先有一个简单的不可变的二维整数型的Point类:

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;
    }

    @Override
    public boolean equals(Object o) {
        if(!(o instanceof ColorPoint))
            return false;
        return super.equals(o) && ((ColorPoint) o).color == color;
    }
    
}

如果直接从Point继承equals,颜色信息就会被忽略掉,所以覆盖equals实现颜色信息比较。

问题在于比较普通点和有色点时,调用普通点的equals去比较有色点,如果x,y相等,那么返回true,调用有色点的equals去比较普通点,总是返回false,不符合的对称性。

修正对称性:

@Override
public boolean equals(Object o) {
    if(!(o instanceof Point))//如果比较对象不是Point或其子类,总是返回false
        return false;
    
    if(!(o instanceof ColorPoint))//如果比较对象是普通点,使用普通点的比较方法
        return o.equals(this);

    return super.equals(o)  && ((ColorPoint)o).color == color;//如果是有色点,用Point的比较方法比较x和y同时比较颜色信息
}

这种方法提供了对称性,但是牺牲了传递性:

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)和p2.equals(p3)都返回true,但p1.equals(p3)则返回false,违反传递性。前面两种比较不考虑颜色,而第三种比较则考虑了颜色。

在equals方法中用getClass测试代替instanceof测试,可以扩展可实例化的类和增加新的组件,同时保留equals约定:

@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;
}

只有当对象具有相同的实现时,才能使对象等同,这样的话,p1.equals(p2),p2.equals(p3)和p1.equals(p3)都返回false,符合传递性

下面编写一个方法,检验某个整值点是否在单位圆中:

private static final Set<Point> unitCircle;
static {
    unitCircle = new HashSet<Point>();
    unitCircle.add(new Point(1, 0));
    unitCircle.add(new Point(0, 1));
    unitCircle.add(new Point(-1, 0));
    unitCircle.add(new Point(0, -1));
}

public static boolean  onUnitCircle(Point p) {
    return unitCircle.contains(p);
}

但是假设通过某种不添加值组件的方式扩展Point,例如让构造器记录创建了多少个实例:

public class CounterPoint extends Point {
    private static final AtomicInteger counter = new AtomicInteger();
    
    public CounterPoint(int x, int y) {
        super(x, y);
        counter.incrementAndGet();
    }
    
    public int numberofCreated() {
        return counter.get();
    }
}

根据里氏替换原则,一个类型的重要属性也将适用于它的子类型,但是,如果将CounterPoint实例传给onUnitCircle方法,如果Point类使用了基于getClass的equals方法,无论CounterPoint的x和y值是什么,onUnitCircle都会返回false,但是如果在Point上使用基于instanceof的equals方法,当遇到CounterPoint时,相同的OnUnitCircle方法就会工作得很好。

所以没有一种方法可以满足既扩展不可实例化的类,又增加值组件。根据复合优先于继承原则,不再让ColorPoint扩展Point,而是在ColorPoint中加入一个私有的Point域,以及一个公共视图方法,此方法返回一个与该有色点处于相同位置的普通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 NullPointException();
        point = new Point(x, y);
        this.color = color;
    }

    public Point asPoint() {
        return point;
    }

    @Override
    public boolean equals(Object o) {
        if(!(o instanceof ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }
}

注意,可以在一个抽象类的子类中增加新的值组件,而不会违反equals约定,只要不可能直接创建超类的实例,前面的种种问题都不会发生。

4.一致性,如果两个对象相等,那么它们必须始终保持相等,除非它们有一个对象被修改了。可变对象在不同时候可以与不同的对象相等,而不可变对象则不能,相等的对象永远相等,不想等的对象永远不相等。

无论类是否可变,都不要使equals依赖于不可靠的资源。如java.net.URL的equals方法依赖于URL中主机IP地址的比较,而将一个主机名转成IP可能需要访问网络,而网络的资源是不确定的,所以无法保证产生相同结果。

显示地通过一个null测试来实现对于任何非null的引用值x,x.equals(null)必须返回false是不必要的:

@Override public boolean equals(Object o) {
    if(o == null)
        return false;
}

因为为了测试等同性,equals方法必须先把参数转换成适当的类型,以便可以调用它的访问方法,或者访问它的域,在进行转换之前,equals必须使用instanceof操作符来检查其参数是否为正确的类型。如果比较对象是null,在instanceof的类型检查测试就不可能通过。

实现高质量equals方法的诀窍:

1.使用==操作符检查”参数是否为这个对象的引用“,如果比较操作代价很大,就值的这么做。

2.使用instanceof操作符检查”参数是否为正确的类型“,一般来说正确的类型指equals方法所在的类,某些情况下,是指该类所实现的某个接口,如果类实现的接口改进了equals约定,允许在实现了该接口的类之间进行比较,就使用接口。集合接口如Set,List,Map具有这样的特性。

3.把参数转换成正确的类型。

4.对于类中每个”关键域“,检查参数中的域是否与该对象中对应的域相匹配,域的比较顺序可能会影响性能,应该最先比较最可能不一致的域,或者开销低的域,不属于对象逻辑状态的域一般不比较,如果”冗余域“代表了整个对象的综合描述,同时比较冗余域的开销比比较所有关键域的开销小,那么比较冗余域可以节省比较失败时去比较实际数据所需要的开销。

5.当覆盖了equals方法后,测试是否符合equals的通用约定。

6.覆盖equals时总要覆盖hashCode。

7.不要企图让equals方法过于智能,过度地寻求各种等价关系,容易造成麻烦,如File类不应该把指向同一文件的符号链接当作相同的对象来看待。

8.不要将equals声明中的Object对象替换为其他类型,这会造成没有覆盖,而是重载,只要两个方法返回同样结果,那么这样是可以接受的,但与增加的复杂性相比,不值得。

@Override注解可以防止本想覆盖而错写成重载的方法,如果你的目的是覆盖,就使用该注解,这样在你出错的时候,能提示你你写的方法并不是一个覆盖的方法。