美文网首页
08-覆盖equals时请遵守通用约定

08-覆盖equals时请遵守通用约定

作者: GeekGray | 来源:发表于2018-10-06 20:56 被阅读7次

    阅读原文

    08-覆盖equals时请遵守通用约定

    覆盖equals方法看起来似乎很简单,但是有许多覆盖方式会导致错误,并且后果非常严重。

    类的每个实例本质上都是唯一的。对于代表活动实体而不是值的类来说确实如此,例如Thread.Object提供的equals实现对于这些类来说是正确的方式。

    不关心类是否提供了逻辑相等的测试功能。例如,java.util.Random覆盖了equals,以检查两个Random实例是否产生相同的随机数序列,但是设计者并不认为客户端或者期望这样的功能。在这样的情况下,从Object继承得到的equals实现已经足够了。

    超类已经覆盖了equals,从超类继承过来的行为对子类也是合适的。例如,大多数的Set实现都从AbstractSet继承equals实现,List实现从AbstractList继承equals实现,Map实现从AbstractMap继承equals实现。

    类是私有的或是包及私有的,可以确定它的equals方法永远不会被调用。在这种情况下,无疑是应该覆盖equals方法的,以防它被以外调用:

    什么时候应该覆盖Object.equals?

    如果类具有自己的逻辑想的概念,而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖equals方法。这通常属于值类的情形。值类仅仅是一个表示值的类,例如Integer或者Date。程序员在利用equals方法来比较对象的引用时,希望知道它们在逻辑上是否相等,而不是想了解它们是否指向同一个对象。不仅必须覆盖equals方法,而且这样做使得这个类的实例可以被用做映射表(map)的键(key),或者集合(set)的元素,使映射或者集合表现出预期的行为。

    有一种值类不需要覆盖equals方法,即用实例受控确保每个值至多只存在一个对象的类。枚举类型就属于这种类。对于这样的类而言,逻辑相同与对象等同是一回事,因此Object的equals方法等同于逻辑意义上的equals方法。

    在覆盖equals方法时,必须遵守的通用约定

    equals方法实现了等价关系(equivalence relation)

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

    传递性(transitive)。对于任何非null的引用值x和y,当且仅当x.equals(y)返回true时,y.equals(z)也返回true,那么x.equals(z)也必须返回ture。

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

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

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

    自反性

    假如违反了这一条,然后把类的实例添加到集合中,该集合的contains方法将果断地告诉你,该集合不包含你刚刚添加的实例。

    对称性

    任何两个对象对于它们是否相等的问题都必须保持一致。若无意违反此条,例如下面的类,它实现了一个区分大小写的字符串。字符串由toString保持,但在比较操作中被忽略。

    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((String)o);
            }
            return false;
        }
    }
    

    在这个类中,equals方法的意图非常好,它企图与普通字符串对象进行互相操作。假设有一个不区分大小写的字符串和一个普通字符串:

    CaseInsensitiveString cis=new CaseInsensitiveString("Polish");
    String s="polish";
    

    正如所料,cis.equals(s)返回true。问题在于,虽然CaseInsensitiveString类中的equals方法指导普通的字符串对象,但是,String类中的equals方法却并不知道不区分大小写的字符串。因此,s.equals(cis)返回false,显然违反了对称性。假设把不区分大小写的字符串对象放到一个集合中:

    List<CaseInsensitiveString>list=new Arraylist<>();
    list.add(cis);
    

    此时返回的结果是不确定的,或者抛出一个运行时异常。一旦违反了equals约定,当其他对象面对你的对象时,你完全不知道这些对象的行为会怎么样。为了解决这个问题,使它变成一条单独的返回语句:

    @override
    public boolean equals(Object o)
    {
        ((CaseInsenitiveString)o).s.equalsIgnoreCase(s);
    }
    

    传递性

    如果一个对象等于第二个对象并且第二个对象又等于第三对象,则第一个对象一定等于第三对象。同样地,无意识地违反这条规定,考虑子类的情形,它将一个新的值组件添加到了超类中,换句话说,子列增加的信息会影响到equals的比较结果。

    一致性

    如果两个对象相等,它们就必须始终保持相等,除非它们中有一个对象被修改了。换句话说,可变对象在不同的时候可以与不同的对象相等,而不可变对象则不会这样。当你在写一个类的时候,应该考虑它是否应该是不可变的。如果认为是不可变的,就必须保证equals方法满足这样的限制条件:相等的对象永远相等,不相等的对象永远不相等。

    无论类是否不可变的,都不要使equals方法依赖于不可靠的资源。例如,java.URL的equals方法依赖于URL总主机IP地址的比较。将一个主机名转变成IP地址肯需要访问网络,随着时间的推移,不确保会产生相同的结果。这样会导致URL的equals方法违反了equals约定,在实践中有可能引发一些问题。除了极少数的例外情况,equals方法都应该对驻留在内存中的对象执行确定性的计算。

    非空性

    指所有的对象都必须不等于null。

    实现高质量equals方法

    1.使用==操作符检查参数是否为这个对象的引用

    2.使用instanceof操作符检查“参数是否为正确的类型”

    3.把参数转换成正确的类型。因为转换之前进行过instanceof测试,所以确保会成功

    4.对于类中的每个关键域,检查参数中的域和对象中对应的域相匹配。

    如果第二步中的类型是接口,就必须通过接口方法访问参数中的域;如果该类型是类,取决于它们的可访问性

    对于既不是float也不是double类型的基本数据类型域,可以使用==操作符进行比较;对于对象引用域,可以递归地调用equals方法;对于float域,可以使用Float.compare方法,double域同理。如果数组域中的每个元素都很重要,可以使用Arrays.equals

    有些引用域包含合法的null,为了避免可能导致NullPointException异常,则使用如下的习惯用法来比较:

    (field==null ? o.field==null : field.equals(o.field))
    

    如果field和o.field通常是相同的对象引用,那么下面的做法会更快一些:

    (field==o.field || (field!=null && field.equals(o.field)))
    

    相关文章

      网友评论

          本文标题:08-覆盖equals时请遵守通用约定

          本文链接:https://www.haomeiwen.com/subject/ovtkaftx.html