zoukankan      html  css  js  c++  java
  • 第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注解可以防止本想覆盖而错写成重载的方法,如果你的目的是覆盖,就使用该注解,这样在你出错的时候,能提示你你写的方法并不是一个覆盖的方法。

  • 相关阅读:
    hdu 5366 简单递推
    hdu 5365 判断正方形
    hdu 3635 并查集
    hdu 4497 数论
    hdu5419 Victor and Toys
    hdu5426 Rikka with Game
    poj2074 Line of Sight
    hdu5425 Rikka with Tree II
    hdu5424 Rikka with Graph II
    poj1009 Edge Detection
  • 原文地址:https://www.cnblogs.com/13jhzeng/p/5642133.html
Copyright © 2011-2022 走看看