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. Чего не хватает в данной программе, чтобы она указала ошибку на этапе компиляции?
2
  • 1
    Пожалуйста, не отмечайте такие действительно интересные вопросы, как учебное-задание. И такой вопросик. Как думаете robustness в данном контексте это ирония ? Может лучше robustless (знаю, что слово малоупотребимое)?
    – avp
    Commented 6 окт. 2011 в 9:17
  • @avp, просто взято из лекций по IT-Security из раздела Robust programming и не придумал я лучшего слова.
    – Dex
    Commented 10 окт. 2011 в 10:05

4 ответа 4

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

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

1
  • @avp, очень рад, что ответ помог вам понять что-то новое :) @Dex, выкладывайте, уверен, такие задачки полезны для многих изучающих Java. Commented 6 окт. 2011 в 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 :)

6
  • А вы попробуйте, да еще бы пару пояснений:)
    – Dex
    Commented 5 окт. 2011 в 15:44
  • Имхо, правиольный ответ подразмевает только добавление аннотации. Все остальное - это уже следствие.
    – a_gura
    Commented 5 окт. 2011 в 16:04
  • Нет, в данном случае аннотация не поможет, так как сигнатуры разные. Commented 5 окт. 2011 в 16:07
  • @yozh, вот, собственно, я на этом попался, когда первый раз увидел сей код и задачу, не имея компилятора под рукой. Пришлось подтягивать знания в области. Думаю сделать задачу общей, как считаете?
    – Dex
    Commented 5 окт. 2011 в 16:14
  • 1
    Аннотация четко помогает обнаружить ошибку на этапе компиляции. В этом состоит суть вопроса 2. Именно отсуствие аннотации дает возможность некорректно определить переопределяемый метод.
    – a_gura
    Commented 5 окт. 2011 в 17:37
8

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

Ответы:

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

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

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

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

3
  • Не очевидно! Зря вы не любите такие задачки. Ваши программы будут жрать много памяти как минимум. Вы решаете задачу за 260 байт, а я за 26, десятикратная выгода, не находите?
    – Dex
    Commented 5 окт. 2011 в 15:31
  • Почему не очевидно, когда там вложеный цикл на 26 букв, а поверх его цикл на 10 итераций? В чем подвох?)
    – Gorets
    Commented 6 окт. 2011 в 8:25
  • Ладно, я понял, просто сразу видно было, как работает переопределение и не видел ошибки в работе.
    – Gorets
    Commented 6 окт. 2011 в 9:03

Всё ещё ищете ответ? Посмотри��е другие вопросы с метками или задайте свой вопрос.