来自Effective Java的第9项(等于合同):示例是否正确?

时间:2011-09-22 14:07:22

标签: java collections effective-java

Bloch的精彩书籍“Effective Java”指出,如果equals不对称,则集合contains的行为是不确定的。

在他给出的例子中(下面稍作修改),布洛赫说他看到了“假”,但也可能看到了真或异。

如果标准没有指定contains(Object o)是否对集合中的每个项目检查e.equals(o)o.equals(e),并且前者已实施,则可以看到“true”。然而,Collections Javadoc清楚地表明它必须是后者(这是我观察到的)。

所以我看到的唯一可能性是“假”或可能是异常(但String Javadoc似乎排除了后者)。

我理解更广泛的观点,非对称equals可能会导致集合之外的代码出现问题,但我不会在他引用的例子中看到它。

我错过了什么吗?

import java.util.List;
import java.util.ArrayList;

class CIString {
  private final String s;

  public CIString(String s) {
    this.s = s;
  }

  @Override public boolean equals( Object o ) {
    System.out.println("Calling CIString.equals from " + this.s );
    if ( o instanceof CIString) 
      return s.equalsIgnoreCase( ( (CIString) o).s);
    if ( o instanceof String) 
      return s.equalsIgnoreCase( (String) o );
    return false;
  }
  // Always override hashCode when you override equals
  // This is an awful hash function (everything collides -> performance is terrible!)
  // but it is semantically sound.  See Item 10 from Effective Java for more details.
  @Override public int hashCode() { return 42; }
}

public class CIS {
  public static void main(String[] args) {
   CIString a = new CIString("Polish");
   String s = "polish";

   List<CIString> list = new ArrayList<CIString>();
   list.add(a);
   System.out.println("list contains s:" + list.contains(s));
 }
}

2 个答案:

答案 0 :(得分:3)

现在是一大早,所以也许我错过了你问题的真实点,这段代码会失败:

public class CIS 
{
    public static void main(String[] args) 
    {
        CIString a = new CIString("Polish");
        String s = "polish";

        List<String> list = new ArrayList<String>();
        list.add(s);
        System.out.println("list contains a:" + list.contains(a));
    }
}

至少奇怪的是,你的代码找到它并且我的代码没有(从理智的角度来看,不是你的代码写得很清楚: - )

编辑:

public class CIS {
  public static void main(String[] args) {
   CIString a = new CIString("Polish");
   String s = "polish";

   List<CIString> list = new ArrayList<CIString>();
   list.add(a);
   System.out.println("list contains s:" + list.contains(s));

   List<String> list2 = new ArrayList<String>();
   list2.add(s);
   System.out.println("list contains a:" + list2.contains(a));
 }
}

现在代码打印出来:

list contains s:false
Calling CIString.equals from Polish
list contains a:true

哪个仍然没有意义......而且非常脆弱。如果两个对象像a.equals(b)那样相等,那么它们也必须等于b.equal(a),这与你的代码不同。

来自javadoc

  

它是对称的:对于任何非空参考值x和y,   当且仅当y.equals(x)返回时,x.equals(y)才应返回true   真。

所以,是的,本书中的示例可能与集合API的Javadoc相反,但原则是正确的。不应该创建一个行为奇怪或最终会出现问题的等于方法。

编辑2:

案文的重点是:

  

在Sun的当前实现中,它恰好返回false,但是   这只是一个实现工件。在另一个实现中,它   可以很容易地返回true或抛出运行时异常。一旦   你违反了平等合同,你根本就不知道其他如何   对象在面对你的对象时会表现出来。

然而,鉴于Javadoc说它看起来似乎行为被修复而不是实现工件。

如果它不在javadoc中,或者javadoc不是规范的一部分,那么它可能会在以后更改,代码将不再有效。

答案 1 :(得分:1)

在我现在看的那本书(第2版)的副本中,项目编号为8,关于对称要求的整个部分显示得非常差。

您提到的特殊问题似乎是由于使用代码与实现过于接近而造成的,这掩盖了作者试图提出的观点。我的意思是,我看list.contains(s),我通过它看到ArrayList和String,关于返回true或抛出异常的所有推理对我来说都没有意义,真的。

  • 我不得不将“使用代码”从实现中移开,以了解它的实现方式:

    void test(List<CIString> list, Object s) {
        if (list != null && list.size() > 0) {
            if (list.get(0).equals(s)) { // unsymmetric equality in CIString
                assert !list.contains(s); // "usage code": list.contain(s)
            }
        }
    }
    

上面看起来很奇怪,但只要list是我们的ArrayList而s是我们的String,测试就会通过。

现在,如果我们使用其他东西而不是String,会发生什么?比如,如果我们将new CIString("polish")作为第二个参数传递会发生什么?

看看,尽管经过了第一次equals检查,断言在下一行失败了 - 因为contains会为此对象返回true。


类似的推理适用于布洛赫提到异常的部分。这一次,我将第二个参数保留为String,但是对于第一个参数,想象一个除ArrayList之外的List实现(这是合法的不是它)。

  • 你看,List实现通常被允许从contains抛出ClassCastException,我们只需要得到一个完全相同的东西并将它用于我们的测试。想到的可能是基于使用适当的比较器包裹在我们原始列表中的TreeSet。

    List<CIString> wrapperWithCce(List<CIString> original,
            Comparator<CIString> comparator) {
        final TreeSet<CIString> treeSet = new TreeSet<CIString>(comparator);
        treeSet.addAll(original);
        return new ArrayList<CIString>() {
            { addAll(treeSet); }
            @Override
            public boolean contains(Object o) {
                return treeSet.contains(o); // if o is String, will throw CCE
            }
        };
    }
    

如果我们将上面的列表和字符串“polish”传递给test会发生什么? list.get(0).equals(s)仍将通过检查,但list.contains(s)将从TreeSet.contains()抛出ClassCastException。

这似乎就像布洛赫在提到list.contains(s)可能引发异常时所想到的那样 - 再次,尽管经过了第一次equals检查