23

Добрый день! Недавно попалась весьма интересная задачка на Java. Которой хотел бы поделиться.

Дан следующий класс

public class RobustCheck {
private final char first;
private final char second;

public RobustCheck(char first, char second) {
    this.first = first;
    this.second = second;
}

public boolean equals (RobustCheck b) {
    return this.first == b.first && this.second == b.second;
}

public int hashCode() {
    return 31 * first + second;
}

public static void main(String[] args) {
    Set<RobustCheck> s = new HashSet<RobustCheck>();
    for(int i=0; i<10; i++)
        for(char ch='a'; ch<='z'; ch++) {
            s.add(new RobustCheck(ch, ch));
        }
    System.out.println(s.size());
}

}

И вопросы:

  1. Почему программа возвращает размер 260 вместо 26? Как ее исправить?
  2. Чего не хватает в данной программе, чтобы она указала ошибку на этапе компиляции?
Nofate
  • 34,603
Dex
  • 9,981
  • 3
  • 34
  • 60
  • 1
    Пожалуйста, не отмечайте такие действительно интересные вопросы, как учебное-задание.

    И такой вопросик. Как думаете robustness в данном контексте это ирония ? Может лучше robustless (знаю, что слово малоупотребимое)?

    – avp Oct 06 '11 at 09:17
  • @avp, просто взято из лекций по IT-Security из раздела Robust programming и не придумал я лучшего слова. – Dex Oct 10 '11 at 10:05

4 Answers4

15

Задача на знание краеугольных камней языка Java и недостатков в его дизайне. В данном случае метод RobustCheck#equals перегружает (overloading) метод Object#equals, в результате чего при сравнении объектов RobustCheck используется метод Object#equals, который вместо сравнения на эквивалентность сравнивает на идентичность. Все 260 созданных в программе объектов RobustCheck не являются идентичными, т.к. физически являются отдельными объектами, размещенными в своих собственных областях памяти. Поэтому методы HashSet "считают" их неравными друг другу и добавляют во множество. В то время как логически неравных объектов RobustCheck только 26 штук, все последующие эквивалентны одному из этих 26-ти. Поэтому нам же необходимо перекрыть (overriding) метод Object#equals, чтобы изменить семантику сравнения. Для этого сигнатура метода RobustCheck#equals должна соответствовать сигнатуре перекрываемого Object#equals; перепишем метод так:

public boolean equals (Object b) {
    if (b instanceof RobustCheck)
        return this.first == ((RobustCheck)b).first && this.second == ((RobustCheck)b).second;
    else
        return false;
}

Чтобы такие ошибки (когда вместо перекрытия мы по ошибке используем перегрузку) смог заметить компилятор, сообщим ему наши намерения о том, что метод Object#equals мы перекрываем в производном классе RobustCheck, добавив соответствующую аннотацию:

@Override
public boolean equals (Object b) {
    if (b instanceof RobustCheck)
        return this.first == ((RobustCheck)b).first && this.second == ((RobustCheck)b).second;
    else
        return false;
}

Ну вот теперь все должно работать как надо:

>java RobustCheck
26

К сожалению, это не исправляет недостатки в дизайне Java. Более интересный вопрос, в дополнение к этой задачке, заключается в том, как вообще избежать всей этой "мумба-юмбы" при определении семантики сравнения двух объектов? В случае с Java ответа, к сожалению, нет. Но можно использовать более продуманные языки. К примеру, Scala, которая самостоятельно определяет корректную семантику сравнения на эквивалентность для неизменяемых (immutable) объектов, избавляя программиста от этой чреватой ошибками задачи:

object Main extends App {
    // Неизменяемые объекты моделируются в Scala с помощью специальных
    // "case"-классов.
    case class RobustCheck(first: Char, second: Char)
val s = collection.mutable.HashSet.empty[RobustCheck]

for (i <- 1 to 10)
    for (c <- 'a' to 'z')
        s += RobustCheck(c, c)

println(s.size)

}

Результат ожидаем:

>scala Main
26

Без всяких заморочек.

angry
  • 8,677
  • 18
  • 74
  • 182
  • @avp, очень рад, что ответ помог вам понять что-то новое :) @Dex, выкладывайте, уверен, такие задачки полезны для многих изучающих Java. – Nikolay Artamonov Oct 06 '11 at 18:34
8

Занятно. Правильный ответ подразумевает изменение типа параметра и добавление собачки? :)

UPD. Ах да, еще каст дополнительно будет нужен.

--- spoiler alert ---

В общем, результат в 26 предполагается из-за того, что Set должен содержать только уникальные значения. Для этого переопределяется метод equals(), который, по задумке, должен был бы возвращать true для объектов, у которых одинаковые поля first и second. Фишка же в том, что equals() на самом деле не переопределен - оригинальная сигнатура выглядит как equals(Object o) (1). А Object#equals() выдает true только для идентичных объектов (this == obj) - вот и выходит 260. И программист заметил бы это, если бы использовал аннотацию @Override перед своим методом equals() (2).

P.S. Подобные маневры становятся особенно хорошо заметны, когда готовишься к сдаче SCJP :)

yozh
  • 3,976
  • 15
  • 17
  • А вы попробуйте, да еще бы пару пояснений:) – Dex Oct 05 '11 at 15:44
  • Имхо, правиольный ответ подразмевает только добавление аннотации. Все остальное - это уже следствие. – a_gura Oct 05 '11 at 16:04
  • Нет, в данном случае аннотация не поможет, так как сигнатуры разные. – Valeriy Karchov Oct 05 '11 at 16:07
  • @yozh, вот, собственно, я на этом попался, когда первый раз увидел сей код и задачу, не имея компилятора под рукой. Пришлось подтягивать знания в области. Думаю сделать задачу общей, как считаете? – Dex Oct 05 '11 at 16:14
  • Честно говоря, не знаком с критериями отметки задач в качестве "общих", так что не могу ничего посоветовать. – yozh Oct 05 '11 at 17:05
  • 1
    Аннотация четко помогает обнаружить ошибку на этапе компиляции. В этом состоит суть вопроса 2. Именно отсуствие аннотации дает возможность некорректно определить переопределяемый метод. – a_gura Oct 05 '11 at 17:37
8

Метод equals не переопределяет Object.equals(Object obj).

Ответы:

  1. При одинаковых хэшкодах для каждого 10-ка инстансов имеем разные результаты вызова equals(Object obj), поэтому с точки зрения HashSet мы имеем неэквивалентные инстансы. (в equals(RobustCheck b) мы не попадаем).
  2. Аннториуем метод equals c помощью @Override.
angry
  • 8,677
  • 18
  • 74
  • 182
a_gura
  • 13,169
2

Никогда не любил такие задачки, не умею думать =( Но тут вроде бы очевидно: 260, потому что:

26 букв х 10 раз = будет 260

Не хватает ошибок?))

angry
  • 8,677
  • 18
  • 74
  • 182
Gorets
  • 12,402
  • Не очевидно! Зря вы не любите такие задачки. Ваши программы будут жрать много памяти как минимум. Вы решаете задачу за 260 байт, а я за 26, десятикратная выгода, не находите? – Dex Oct 05 '11 at 15:31
  • Почему не очевидно, когда там вложеный цикл на 26 букв, а поверх его цикл на 10 итераций? В чем подвох?) – Gorets Oct 06 '11 at 08:25
  • Ладно, я понял, просто сразу видно было, как работает переопределение и не видел ошибки в работе. – Gorets Oct 06 '11 at 09:03