Задача на знание краеугольных камней языка 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
Без всяких заморочек.