Пункт 9 (равнозначный контракт) из Effective Java: пример верный? - PullRequest
5 голосов
/ 22 сентября 2011

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

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

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

3 голосов
/ 22 сентября 2011

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

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 коллекций, но принцип верен.Не следует создавать метод equals, который ведет себя странно, иначе со временем возникнут проблемы.

Edit 2:

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

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

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

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

1 голос
/ 23 сентября 2011

В копии книги, которую я сейчас смотрю (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
            }
        };
    }
    

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...