Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Джош Блох

.pdf
Скачиваний:
57
Добавлен:
08.03.2016
Размер:
27.13 Mб
Скачать

Глава 3 Методы, общие для всех объектов

Если проверки для всех полей прошли успешно, возвращайте результат true, в противном случае возвращайте false. Если на шаге 2 тип был определен как интерфейс, вы должны по­ лучать доступ к значимым полям аргумента, используя мето­ ды самого интерфейса. Если же тип аргумента определен как класс, то, в зависимости от условий, вам, возможно, удастся получить прямой доступ к полям аргумента.

Для простых полей, за исключением типов float и double, для сравнения используйте оператор ==. Для полей со ссылкой на объек­ ты рекурсивно вызывайте метод equals. Для поля float используйте метод Float.compare. Для полей double используйте метод Double. Compare. Особая процедура обработки полей float и double нужна потому, что существуют особые значения Float. NaN, -0. Of, а также аналогичные значения для типа double. Подробности см. в докумен­ тации к Float, equals. При работе с полями массивов применяйте ме­ тоды Array.equals, появившиеся в версии 1.5. Некоторые поля, пред­ назначенные для ссылки на объекты, вполне оправданно могут иметь значение null. Чтобы не допустить возникновения исключительной ситуации NullPointerException, для сравнения подобных полей ис­ пользуйте следующую идиому:

(field == null ? о.field == null : field.equals(o.field))

Если field и o. field часто ссылаются на один и тот же объект, следующий альтернативный вариант может оказаться быстрее:

(field == о.field || (field != null && field.equals(o.field)))

Для некоторых классов, таких как представленный ранее СаselnsensitiveString, сравнение полей оказывается гораздо более сложным, чем простая проверка равенства. Если это так, то вам, возможно, потребуется каждому объекту придать некую канониче­ скую форму. Таким образом, метод equals сможет выполнять простое и точное сравнение этих канонических форм вместо того, чтобы поль­ зоваться более трудоемким и неточным вариантом сравнения. Опи­ санный прием более подходит для неизменяемых классов (статья 15),

60

С татья 8

поскольку, когда объект меняется, приходится приводить и его кано­ ническую форму в соответствие последним изменениям.

На производительность метода equals может оказывать влияние очередность сравнения полей. Чтобы добиться наилучшей произво­ дительности, вы должны в первую очередь сравнивать те поля, ко­ торые будут различаться с большей вероятностью, либо те, которые сравнивать проще. В идеале оба этих качества должны совпадать. Не следует сравнивать поля, которые не являются частью логическо­ го состояния объекта, например поля Object, которые используются для синхронизации операций. Вам нет необходимости сравнивать из­ быточные поля, значение которых можно вычислить, отталкиваясь от «значащих полей» объекта, однако сравнение этих полей может повысить производительность метода equals. И если значение из­ быточного поля равнозначно суммарному описанию объекта в целом, то сравнение подобных полей позволит вам сэкономить на сравнении действительных данных, если будет выявлено расхождение.

5. Закончив написание собственного метода equals, задайте себе три вопроса: является ли он симметричным, явля­ ется ли транзитивным и является ли непротиворечивым?

И не просто задайте себе вопрос, напишите тест для про­ верки соблюдений этих условий. Если ответ отрицательный, разберитесь, почему не удалось реализовать эти свойства, и подправьте метод соответствующим образом. Конечно же, ваш метод equals должен отвечать и двум другим свойствам (рефлексивности и отличию от нулевого значения).

В качестве конкретного примера метода equals, который был выстроен по приведенному выше рецепту, можно посмотреть PhoneNumber.equals из статьи 9. Несколько заключительных пре­ достережений:

Переопределяя метод equals, всегда переопределяйте ме­

тод hashCode (статья 9).•

Не старайтесь быть слишком умными. Если вы просто про­ веряете равенство полей, соблюдать условия соглашений для

67

Глава 3 Методы, общие для всех объектов

метода equals совсем не трудно. Если же в поисках равенства вы излишне агрессивны, можно легко нарваться на неприят­ ности. Так, использование синонимов в каком бы то ни было обличье обычно оказывается плохим решением. Например, класс File не должен пытаться считать равными символьные связи, которые относятся к одному и тому же файлу. К сча­ стью, он этого и не делает.

Декларируя метод equals, не надо указывать вместо Ob­ ject другие типы объектов. Нередко программисты пишут метод equals следующим образом, а потом часами ломают го­ лову над тем, почему он не работает правильно:

public boolean equals(MyClass о) {

}

Проблема заключается в том, что этот метод не переопределя­ ет (override) метод Object.equals, чей аргумент имеет тип Object, а перегружает его ( overload, статья 41). Подобный «строго типи­ зированный» метод equals можно создать в дополнение к обычному методу equals, однако, поскольку оба метода возвращают один и тот же результат, нет никакой причины это делать. Хотя при опреде­ ленных условиях это может дать минимальный выигрыш в произ­ водительности, но это не оправдывает дополнительного усложнения программы (статья 55).

Переопределяя метод equals, всегда переопределяйте hashCode

Распространенным источником ошибок является то, что нет пе­ реопределения метода hashCode. Вы должны переопределять ме­ тод hashCode в каждом классе, где переопределен метод equals.

Невыполнение этого условия приведет к нарушению общих согла-

62

С татья 9

шений для метода Obj ect.hashCode, а это не позволит вашему классу правильно работать в сочетании с любыми коллекциями, построен­ ными на использовании хэш-таблиц, в том числе с HashMap, HashSet

и HashTable.

Приведем текст соглашений, представленных в спецификации Object [JavaSE6]:

Если во время работы приложения несколько раз обратиться

кодному и тому же объекту, метод hashCode должен посто­ янно возвращать одно и то же целое число, показывая тем самым, что информация, которая используется при сравне­ нии этого объекта с другими (метод equals), не поменялась. Однако, если приложение остановить и запустить снова, это число может стать другим.

• Если метод equals (Obj ect) показывает, что два объекта рав­ ны друг другу, то, вызвав для каждого из них метод hashCode, вы должны получить в обоих случаях одно и то же целое число.•

Если метод equals(Object) показывает, что два объекта друг другу не равны, вовсе не обязательно, что метод hashCode возвратит для них разные числа. Между тем программист должен понимать, что генерация разных чисел для неравных объектов может повысить эффективность хэш-таблиц.

Главным условием является второе: равные объекты долж­ ны иметь одинаковый хэш-код. Если вы не переопределите метод hashCode, оно будет нарушено: два различных экземпляра с точки зрения метода equals могут быть логически равны, однако для мето­ да hashCode из класса Obj ect это всего лишь два объекта, не имеющие между собой ничего общего. Поэтому метод hashCode, скорее всего, возвратит для этих объектов два случайных числа, а не одинаковых, как того требует соглашение.

В качестве примера рассмотрим следующий упрощенный класс PhoneNumbeг, в котором метод equals построен по рецепту из статьи 8:

63

Глава 3 Методы, общие для всех объектов

public final class PhoneNumber { private final short areaCode; private final short prefix; private final short lineNumber;

public PhoneNumber(int areaCode, int prefix, int lineNumber) {

rangeCheck(areaCode, 999, “area code’’); rangeCheck(exchange, 999, “prefix’’); rangeCheck(extension, 9999, “line number”); this.areaCode = (short) areaCode; this.prefix = (short) prefix; this.lineNumber = (short) lineNumber;

}

private static void rangeCheck(int arg, int max, String name) {

if (arg < 0 || arg > max)

throw new IllegalArgumentException(name + “: “ + arg)

}

@0verride public boolean equals(Object o) { if (o == this)

return true;

if (!(o instanceof PhoneNumber) return false;

PhoneNumber pn = (PhoneNumber)o;

return pn.lineNumber == lineNumber && pn. prefix == prefix &&

pn. areaCode == areaCode;

}

// Ошибка Нет метода hashCode!

... // Остальное опущено

}

64

С татья 9

Предположим, вы попытались использовать этот класс

с HashMap:

Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>(); m.put(new PhoneNumber(707, 867, 5309), “Jenny");

После этого вы вправе ожидать, что т. get (new PhoneNumber(707, 867, 5309) возвратит строку «Jenny», однако он возвращает null. Заметим, что здесь задействованы два экземпляра класса PhoneNumber: один используется для вставки в таблицу HashMap, другой, рав­ ный ему экземпляр — для поиска. Отсутствие в классе PhoneNumber переопределенного метода hashCode приводит к тому, что двум рав­ ным экземплярам соответствует разный хэш-код, т.е. имеем нару­ шение соглашений для этого метода. Как следствие, метод get ищет указанный телефонный номер в другом сегменте хэш-таблицы, а не там, где была сделана запись с помощью метода put. Даже если два экземпляра попадут в один и тот же сегмент, метод get однозначно выдаст результат null, поскольку у HashMap есть оптимизация, позво­ ляющая кэшировать хэш-код, связанный с каждой записью, и Hash- Мар не озадачивается проверкой равенства объектов, если хэш-код не совпадает.

Разрешить эту проблему можно, просто поместив в класс PhoneNumber правильный метод hashCode. Так как же должен выгля­ деть метод hashCode? Написать действующий, но не слишком хоро­ ший метод очень просто. Например, следующий метод всегда прием­ лем, но пользоваться им не надо никогда:

// Самая плохая из допустимых хэш-функций - никогда ею не пользуйтесь!

@0verride public int hashCodeO { return 42; }

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

65

Глава 3 Методы, общие для всех объектов

нием хэш-таблиц должно увеличиваться линейно, таковое увеличи­ вается в квадратичной зависимости. Для больших хэш-таблиц такое отличие равносильно переходу от работоспособного к неработоспо­ собному варианту.

Хорошая хэш-функция для неравных объектов стремится гене­ рировать различные хэш-коды. И это именно то, что подразумевает третье условия в соглашения для hashCode. В идеале хэш-функция должна равномерно распределять любое возможное множество не­ равных экземпляров класса по всем возможным значениям хэш-кода. Достичь этого идеала может быть чрезвычайно сложно. К счастью, не так трудно получить для него хорошее приближение. Представим простой рецепт:

1. Присвойте переменной result (тип int) некоторое ненулевое число, скажем, 17.

2.Для каждого значимого поля f в вашем объекте (т.е. поля, значение которого принимается в расчет методом equals) вы­ полните следующее:

а.Вычислите для этого поля хэш-код с (тип int):

i.Если поле имеет тип boolean, вычислите (f ? 1 : 0);

й. Если поле имеет тип byte, char, short или int, вычис­

лите (int)f;

iii. Если поле имеет тип long, вычислите (int)(f " (f >>>

32));

iv.Если поле имеет тип float, вычислите Float,

floatToIntBits(f);

v. Если поле имеет тип double, вычислите Double.doubleToLongBits(f),a затем преобразуйте полученное значе­ ние, как указано в 2.a.iii;

vi.Если поле является ссылкой на объект, а метод equals данного класса сравнивает это поле, рекурсивно вызы­

66

С татья 9

вая другие методы equals, так же рекурсивно вызы­ вайте для этого поля метод hashCode. Если требуется более сложное сравнение, вычислите для данного поля каноническое представление (canonical representation),

а затем вызовите метод hashCode уже для него. Если значение поля равно null, возвращайте значение О (можно любую другую константу, но традиционно ис­ пользуется 0);

vii. Если поле является массивом, обрабатываете его так, как если бы каждый его элемент был отдельным по­ лем. Иными словами, вычислите хэш-код для каждо­ го значимого элемента, рекурсивно применяя данные правила, а затем объединяя полученные значения так, как описано в 2.Ь.

Ь.Объедините хэш-код с, вычисленный на этапе а, с теку­ щим значением поля result следующим образом:

result = 31 * result + с;

3. Верните значение result.

4. Когда вы закончили писать метод hashCode, спросите себя, имеют ли равные экземпляры одинаковый хэш-код. Если нет, выясните, в чем причина, и устраните эту проблему.

И з процедуры получения хэш-кода можно исключить избы­ точные поля. Иными словами, можно исключить любое поле, чье значение можно вычислить исходя из значений полей, которые уже включены в рассматриваемую процедуру. Вы обязаны исключать из процедуры все поля, которые не используются в ходе проверки равенства. Если эти поля не исключить, это может привести к нару­ шению второго правила в соглашениях для hashCode.

Н а этапе 1 используется ненулевое начальное значение. Благо­ даря этому не будут игнорироваться те обрабатываемые в первую очередь поля, у которых значение хэш-кода, полученное на этапе 2.а,

67

Глава 3 Методы, общие для всех объектов

оказалось нулевым. Если же на этапе 1 в качестве начального значе­ ния использовался нуль, то ни одно из этих обрабатываемых в пер­ вую очередь полей не сможет повлиять на общее значение хэш-кода, что может привести к увеличению числа коллизий. Число 17 выбрано произвольно.

Умножение в шаге 2.Ь создает зависимость значения хэш-ко­ да от очередности обработки полей, а это дает гораздо лучшую хэш-функцию в случае, когда в классе много одинаковых полей. Н а­ пример, если из хэш-функции для класса String, построенной по это­ му рецепту, исключить умножение, то все анаграммы (слова, полу­ ченные от некоего исходного слова путем перестановки букв) будут иметь один и тот же хэш-код. Множитель 31 выбран потому, что является простым нечетным числом. Если бы это было четное число и при умножении произошло переполнение, информация была бы по­ теряна, поскольку умножение числа на 2 равнозначно его арифмети­ ческому сдвигу. Хотя преимущества от использования простых чисел не столь очевидны, именно их принято использовать для этой цели. Замечательное свойство числа 31 заключается в том, что умножение может быть заменено сдвигом и вычитанием для лучшей производи­ тельности: 31 * I == (i«5) - i. Современные виртуальные машины автоматически выполняют эту небольшую оптимизацию.

Давайте используем этот рецепт для класса PhoneNumber. В нем есть три значимых поля, все имеют тип short:

@0verride public int hashCode() { int result = 17;

result = 31* result + areaCode; result = 31* result + prefix; result = 31* result + lineNumber; return result;

}

Поскольку этот метод возвращает результат простого детерми­ нированного вычисления, исходными данными для которого явля­ ются три значащих поля в экземпляре PhoneNumber, то должно быть

6 8

С татья 9

понятно, что равные экземпляры PhoneNumber будут иметь равный хэш-код. Фактически, этот метод является абсолютно правильной реализацией hashCode для класса PhoneNumber наравне с методами из библиотек платформ Java. Он прост, достаточно быстр и пра­ вильно разносит неравные телефонные номера по разным сегмен­ там хэш-таблицы.

Если класс является неизменным и при этом важны затраты на вычисление хэш-кода, вы можете сохранять хэш-код в самом этом объекте вместо того, чтобы вычислять его всякий раз заново, как только в нем появится необходимость. Если вы полагаете, что большинство объектов данного типа будут использоваться как ключи в хэш-таблице, то вам следует вычислять соответствующий хэш-код уже в момент создания соответствующего экземпляра. В противном случае вы можете выбрать инициализацию, отложенную до первого обращения к методу hashCode (статья 71). Хотя достоинства подоб­ ного режима для нашего класса PhoneNumbers не очевидны, давайте покажем, как это делается:

// Отложенная инициализация, кэшируемый hashCode private volatile int hashCode ; // (см. статью 71)

@0verride public int hashCode() { int result = hashCode;

if (hashCode == 0) { int result = 17;

result = 31 * result + areaCode; result = 31 * result + prefix; result = 31 * result + lineNumber; hashCode = result;

}

return result;

}

Хотя рецепт, изложенный в этой статье, и позволяет создавать довольно хорошие хэш-функции, он не соответствует ни современ­ ным хэш-функциям, ни хэш-функциям из библиотек для платфор­

69

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]