• Java


    平时很难遇到需要覆盖equals的情况。


    什么时候不需要覆盖equals?

    • 类的每个实例本质上是唯一的,我们不需要用特殊的逻辑值来表述,Object提供的equals方法正好是正确的。
    • 超类已经覆盖了equals,且从超类继承过来的行为对于子类也是合适的。
    • 当确定该类的equals方法不会被调用时,比如类是私有的。


    如果要问什么时候需要覆盖equals?
    答案正好和之前的问题相反。
    即,类需要一个自己特有的逻辑相等概念,而且超类提供的equals不满足自己的行为。
    (PS:对于枚举而言,逻辑相等和对象相等都是一回事。)


    既然只好覆盖equals,我们就需要遵守一些规定:

    • 自反性 (reflexive):对于任何一个非null的引用值x,x.equals(x)为true。
    • 对称性 (symmetric):对于任何一个非null的引用值x和y,x.equals(y)为true时y.equals(x)为true。
    • 传递性 (transitive):对于任何一个非null的引用值x、y和z,当x.equals(y)为true 且 y.equals(z)为true 则 x.equals(z)为true。
    • 一致性 (consistent):对于任何一个非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)的结果依然一致。
      (PS:对于任何非null的引用值x,x.equals(null)必须返回false。)


    其实这些规定随便拿出一个都是很好理解的。
    难点在于,当我遵守一个规定时有可能违反另一个规定


    自反性就不用说了,很难想想会有人违反这一点。

    关于对称性,下面提供一个反面例子:

    class CaseInsensitiveString {
    
        private final String s;
    
        public CaseInsensitiveString(String s) {
            if (s == null)
                this.s = StringUtils.EMPTY;
            else
                this.s = s;
        }
    
        @Override
        public boolean equals(Object obj) {
            if (obj instanceof CaseInsensitiveString)
                return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
            if (obj instanceof String)
                return s.equalsIgnoreCase((String) obj);
            return false;
        }
    
    }
    

    这个例子显然违反对称性,即x.equals(y)为true 但 y.equals(x)为false。
    不仅是在显示调用时,如果将这种类型作为泛型放到集合之类的地方,会发生难以预料的行为。

    而对于上面这个例子,在equals方法中我就不牵扯其他类型,去掉String实例的判断就可以了。


    关于传递性,即,当x.equals(y)为true 且 y.equals(z)为true 则 x.equals(z)为true。
    这个规定在对类进行扩展时尤其明显。

    比如,我用x,y描述某个Point:

    class Point {
        private final int x;
        private final int y;
    
        public Point(int x, int y) {
            super();
            this.x = x;
            this.y = y;
        }
    
        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof Point))
                return false;
            Point p = (Point) obj;
            return p.x == x && p.y == y;
        }
    
    }
    


    现在我想给Point加点颜色:

    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 obj) {
            if (!(obj instanceof ColorPoint))
                return false;
            return super.equals(obj) && ((ColorPoint) obj).color == color;
        }
    
    }
    

    似乎很自然的提供了ColorPoint的equals方法,但他连对称性的没能满足。

    于是我们加以修改,令其满足对称性:

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


    好了,接下来我们就该考虑传递性了。
    比如我们现在有三个实例,1个Point和2个ColorPoint....
    然后很显然,不满足<当x.equals(y)为true 且 y.equals(z)为true 则 x.equals(z)为true>。
    事实上,我们无法在扩展可实例化类的同时,既增加新的值组件,又保留equals约定。


    于是我索性不用instanceof,改用getClass()。
    这个确实可以解决问题,但很难令人接受。
    如果我有一个子类没有覆盖equals,此时equals的结果永远是false。


    既然如此,我就放弃继承,改用复合(composition)。
    以上面的ColorPoint作为例子,将Point变成ColorPoint的field,而不是去扩展。 代码如下:

    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;
        }
    
        /**
         * Returns the point-view of this color point.
         */
        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);
        }
    
        @Override
        public int hashCode() {
            return point.hashCode() * 33 + color.hashCode();
        }
    }
    


    关于一致性,即如果两者相等则始终相等,除非有一方被修改。
    这一点与其说equals方法,到不如思考写一个类的时候,这个类应该设计成可变还是不可变。
    如果是不可变的,则需要保证一致性。

    考虑到这些规定,以下是重写equals时的一些建议:

    • 第一步使用"=="操作验证是否为同一个引用,以免不必要的比较操作。
    • 使用instanceof检查参数的类型。
    • 检查所有关键的field,对float和double以外的基本类型field直接使用"=="比较。
    • 回过头来重新检查一遍:是否满足自反性、对称性、传递性和一致性。

    任何覆盖了equals方法的类都需要覆盖hashCode方法。 忽视这一条将导致类无法与基于散列的数据结构一起正常工作,比如和HashMap、HashSet和Hashtable。

    下面是hashCode相关规范:

    • 在程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这个对象调用多少次hashCode,起结果必须始终如一地返回同一个证书。
      如果是同一个程序执行多次,每次调用的结果可以不一致。

    • 如果两个对象根据equals方法比较是相等的,那么两个对象的hashCode结果必须相同。

    • 如果两个对象根据equals方法比较是不相等的,那么这两个对象的hashCode不一定返回不同的结果。
      但是,如果不同的对象返回不同的hashCode,则能提高散列表的性能。

    下面的代码是一个反面例子:

    import java.util.HashMap;
    import java.util.Map;
    
    public final class PhoneNumber {
        private final short areaCode;
        private final short prefix;
        private final short lineNumber;
    
        public PhoneNumber(int areaCode, int prefix, int lineNumber) {
            rangeCheck(areaCode, 999, "area code");
            rangeCheck(prefix, 999, "prefix");
            rangeCheck(lineNumber, 9999, "line number");
            this.areaCode = (short) areaCode;
            this.prefix = (short) prefix;
            this.lineNumber = (short) lineNumber;
        }
    
        private static void rangeCheck(int arg, int max, String name) {
            if (arg < 0 || arg > max)
                throw new IllegalArgumentException(name + ": " + arg);
        }
    
        @Override
        public boolean equals(Object o) {
            if (o == this)
                return true;
            if (!(o instanceof PhoneNumber))
                return false;
            PhoneNumber pn = (PhoneNumber) o;
            return pn.lineNumber == lineNumber && pn.prefix == prefix
                    && pn.areaCode == areaCode;
        }
    
        // Broken - no hashCode method!
    
        // A decent hashCode method - Page 48
        // @Override public int hashCode() {
        // int result = 17;
        // result = 31 * result + areaCode;
        // result = 31 * result + prefix;
        // result = 31 * result + lineNumber;
        // return result;
        // }
    
        public static void main(String[] args) {
            Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>();
            m.put(new PhoneNumber(707, 867, 5309), "Jenny");
            System.out.println(m.get(new PhoneNumber(707, 867, 5309)));
        }
    }
    


    通过equals方法比较,两个实例在逻辑上是相等的。
    但由于没有覆盖hashCode方法,两个实例返回的hashCode是不同的。
    在散列表中,如果散列码不匹配,就不必检查两个实例是否相等。

    如果随便提供这样的一个hashCode方法:

    public int hashCode(){
        return 42;
    }
    

    这样会让散列表失去优势,退化为链表。


    最好的hashCode应该是<不同的对象产生不同的散列码>。
    即,散列函数把集合中不同的实例均匀地分布到所有可能的散列值上。


    下面是一种简单的思路(也就是上面例子中注释的部分):

    • 把一个非零常数值放在result变量中。
    • 针对每一个关键的field(假设变量名为f)计算int类型的散列码,不同类型有不同的计算方式。

      • boolean:f?1:0
      • byte,short,char:(int)f
      • long:(int)(f^(f>>>32))
      • float:Float.floatToIntBits(f)
      • double:Double.doubleToIntBits(f)
      • 引用:递归调用hashCode
      • 数组:每个元素作为一个field遵循上述规则
    • 对计算出的散列码值c进行:result = result*31+c;
    • 重复测试。

    注意,这里仅限关键field。 对于那些用其他field值计算出来的field,我们可以将其排除在外。

    如果一个类是不可变的,而且计算散列值的开销比较大,我们可以试着将散列值缓存。

    或者我们也可以试试延迟初始化,在hashCode第一次被调用时进行初始化:

    private volatile int hashCode; // (See Item 71)
    
    @Override public int hashCode() {
        int result = hashCode;
        if (result == 0) {
            result = 17;
            result = 31 * result + areaCode;
            result = 31 * result + prefix;
            result = 31 * result + lineNumber;
            hashCode = result;
        }
        return result;
    }
    

    另外,Josh Bloch在最后加了一段话:

    Many classes in the Java platform libraries, such as String, Integer, and Date, include in their specifications the exact value returned by their hashCode method as a function of the instance value. This is generally not a good idea, as it severely limits your ability to improve the hash function in future releases.

    <可以把它们的hashCode方法返回的确切值规定为该实例的一个函数。> 看了翻译后一头雾水...

    后来在爆栈中看到这么一个回复,记下来作为参考:

    The API docs specify that String.hashCode() is computed by a specific formula. Client code is free to independently compute the hash code using that exact formula and assume it will be the same as that returned by String.hashCode(). This might seem perverse for pure Java code, but does make some sense with JNI. There are probably other cases where it would make sense to take advantage of the extra knowledge that the API specifies.

  • 相关阅读:
    ipandao markdown mathjax版本
    我们是不是太关注于语法而忽略了算法
    SVGG.JS 入门教程
    关于ipandao编辑器手机访问换行问题
    启明星采购系统新版发布
    仿MSDN的帮助系统
    十分钟打造一款在线的数学公式编辑器
    Search Filter Syntax
    从华为养猪说起,聊聊我对中国计算机发展的一个遗憾-为何我们没有开发出自己的编程语言
    从.NET看微软的焦虑
  • 原文地址:https://www.cnblogs.com/kavlez/p/4185547.html
Copyright © 2020-2023  润新知