Пункт 9 (равнозначный контракт) из Effective Java: пример верный?

Замечательная книга Блоха "Эффективная Ява" указывает, что если equals не симметрично, то поведение коллекций contains является неопределенным.

В приведенном им примере (воспроизведенном с небольшими изменениями ниже) Блох говорит, что он видит "ложь", но с тем же успехом мог бы видеть истину или исключение.

Вы можете увидеть "истину", если в стандарте не указано, contains(Object o) проверки e.equals(o) или же o.equals(e) для каждого элемента в коллекции, а первый реализован. Тем не менее, 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 ответа

Это раннее утро, поэтому, возможно, я упускаю истинную точку вашего вопроса, этот код потерпит неудачу:

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 x.equals(y) должен возвращать true тогда и только тогда, когда y.equals(x) возвращает true.

Так что, да, пример в книге может противоречить Javadoc API коллекций, но принцип верен. Не следует создавать метод равных, который ведет себя странно, иначе со временем возникнут проблемы.

Изменить 2:

Ключевой момент текста:

В текущей реализации Sun это возвращает false, но это просто артефакт реализации. В другой реализации он может так же легко вернуть true или вызвать исключение во время выполнения. После того, как вы нарушили контракт равных, вы просто не знаете, как поведут себя другие объекты при столкновении с вашим объектом.

Однако, учитывая, что Javadoc говорит то, что говорит, может показаться, что поведение исправлено, а не артефакт реализации.

Если его нет в javadoc или если javadoc не является частью спецификации, он может измениться позже, и код больше не будет работать.

В копии книги, которую я сейчас смотрю (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? скажи, что будет, если мы пройдем new CIString("polish") как второй аргумент?

Смотри, несмотря на прохождение первого equals проверьте, что утверждение не будет выполнено на следующей строке - потому что содержит будет возвращать true для этого объекта.


Аналогичные рассуждения применимы к той части, где Блох упоминает об исключении. На этот раз я сохранил второй параметр как String, но для первого представлял реализацию List, отличную от ArrayList (это законно, не правда ли).

  • Видите ли, реализации List, как правило, разрешено выбрасывать ClassCastException из contains нам просто нужно получить тот, который делает именно это и использовать его для нашего теста. Тот, который приходит на ум, может быть основан на 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
            }
        };
    }
    

Что произойдет, если мы передадим список, как указано выше, и строку "польский" test? list.get(0).equals(s) все равно пройдет проверку, но list.contains(s) сгенерирует исключение ClassCastException из TreeSet.contains ().

Это похоже на случай, который имел в виду Блох, когда он упомянул, что list.contains(s) может выдать исключение - опять же, несмотря на прохождение первого equals проверить

Другие вопросы по тегам