Главная страница
Навигация по странице:

  • // Ошибка нарушение транзитивности public boolean equals (Object о) { If (!(o instanceof Point))return false; // Если о - обычный

  • // Добавляет новый аспект, не нарушая соглашений для equals

  • // Нет метода hashCode!

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

  • Effective Java tmprogramming Language GuideJ o s h u a b lo c h


    Скачать 1.05 Mb.
    НазваниеEffective Java tmprogramming Language GuideJ o s h u a b lo c h
    Дата03.04.2018
    Размер1.05 Mb.
    Формат файлаpdf
    Имя файлаBlokh_Dzh_-_Java_Effektivnoe_programmirovanie.pdf
    ТипДокументы
    #40178
    страница5 из 25
    1   2   3   4   5   6   7   8   9   ...   25

    // Ошибка нарушение симметрии boolean equals (Objeet о) {
    if (1(0 instaneeof ColorPoint))
    return false;
    ColorPoint ср = (ColorPoint) o; return super.equals(o) & & cp.color == color; Проблема этого метода заключается в том,

    TO вы можете получить разные результаты, сравнивая обычную точку с цветной и наоборот. Прежняя процедура сравнения игнорирует цвета новая всегда возвращает false из-за того, что указан неправильный тип аргумента. для пояснения создадим одну обычную точку и одну цветную
    Point р
    = new Point (1, 2);
    ColorPoint ер
    = new ColorPoint (1, 2, Color.RED); Выражение р) возвратит true, ар) возвратит false. Вы можете попытаться решить эту проблему, заставив метод
    ColorPoint. equals игнорировать цвет при выполнении "смешанных сравнений"
    // Ошибка нарушение транзитивности
    public boolean equals (Object о) {
    If (!(o instanceof Point))
    return false;
    // Если о - обычный
    Point, выполнить сравнение // без проверки цвета
    if (! (о instanceof ColorPoint)) return o.equals(this);
    // Если о - ColorPoint, выполнить полное сравнение
    ColorPoint ер = (ColorPoint) o; return super.equals(o) && cp.color = = color;
    }
    Такой подход обеспечивает симметрию, но за счет транзитивности
    ColorPoint р
    = new ColorPoint(1, 2, Color.RED);
    Point р
    = new Point(1, 2);
    ColorPoint р
    = new ColorPoint(1, 2, Color.BLUE); В этом случае выражения р) и р) возвращают значение true, а р1.equals(р3) возвращает false - прямое нарушение транзитивности. Первые два сравнения игнорируют цвет, в третьем цвет учитывается. Так где же решение Оказывается, это фундаментальная проблема эквивалентных отношений в объектно-ориентированных языках. Не существует способа расширить класс, порождающий экземпляры, и добавить к нему новый аспект, сохранив при этом соглашения для метода equals. Зато есть изящный обходной путь. Следуйте совету из статьи
    14: Предпочитайте композиции наследование. Вместо того чтобы заставлять ColorPoint расширять класс Point, поместите в ColorPoint закрытое поле Point и открытый метод представления (статья 4), который в том же месте, где находится цветная точка, показывает обычную точку
    // Добавляет новый аспект, не нарушая соглашений для equals public class ColorPoint { private Point point; private Color color; public ColorPoint(int х, int у, Color color) { point = new Point(x, у this.color = color;
    /**
    * Для данной цветной точки возвращает точку-представление
    */
    public Point asPoint() return point; public boolean equals(Object о) { if (!(o instanceof ColorPoint» return false;
    ColorPoint ср = (ColorPoint)o; return cp.point.equals(point) & & cp.color.equals(color);
    }
    // Остальное опущено
    }
    В библиотеках для платформы Java содержатся классы, которые являются подклассами для класса, создающего экземпляры, и при этом придают ему новый аспект. Например, Т является подклассом класса java.util,Date и добавляет поле для наносекунд. Реализация метода equals в Т нарушает правило симметрии, и это может привести к странному поведению программы, если объекты Timestamp и Date использовать водной коллекции или смешивать как-нибудь иначе. В документации к классу Тесть предупреждение, предостерегающее программиста от смешивания объектов Date и Timestamp. Пока вы не смешиваете их, у вас проблем не будет, но если вы сделаете это, устранение возникших в результате ошибок может быть непростым. Класс Timestamp не является правильными подражать ему не надо. Заметим, что вы можете добавить аспект в подкласс абстрактного класса, не нарушая при этом соглашений для метода equals. Это важно для тех разновидностей иерархии классов, которые вы получите, следуя совету из статьи
    20: Заменяйте объединение иерархией классов. Например, вы можете иметь простой абстрактный класса также подклассы Ci,rcle, добавляющий поле радиуса, и Rectangle, добавляющий поля длины и ширины. Только что продемонстрированные проблемы не будут возникать до тех пор, пока нет возможности создавать экземпляры суперкласса. Непротиворечивость. Четвертое требование в соглашениях для метода equals гласит если два объекта равны, они должны быть равны все время, пока один из них (или оба) не будет изменен. Это не столько настоящее требование, сколько напоминание о том, что изменяемые объекты в разное время могут быть равны разным объектам, а неизменяемые объекты - не могут. Когда выпишите класс, подумайте, не следует ли его сделать неизменяемым (статья
    13). Если вы решите, что это необходимо, позаботьтесь о том, чтобы ваш метод equals выполнял это ограничение равные объекты должны оставаться все время равными, а неравные объекты - соответственно, неравными. Отличие от null (non-nullity)
    . Последнее требование гласит, что все объекты должны отличаться от нуля (null). Трудно себе представить, что в ответ на вызов o.equals(null) будет случайно возвращено значение true, однако вполне вероятно случайное инициирование исключительной ситуации
    NullPointerException. Общие соглашения этого не допускают. Во многих классах методы equals имеют защиту в виде явной проверки аргумента на null: public boolean equals(Object о) { if (от акая проверка не является обязательной. Проверяя равенство аргумента, метод equals должен сначала привести аргумент к нужному типу, чтобы затем можно было воспользоваться соответствующими механизмами доступа или напрямую обращаться
    к его полям. Перед приведением типа метод equals должен воспользоваться оператором instanceof для проверки того, что аргумент имеет правильный тип public boolean equals(Object о if (I(o instanceof МуТуре))
    return false; Если бы эта проверка типа отсутствовала, а метод equals получил бы аргумент неправильного типа, то он бы инициировал исключительную ситуацию ClassCastException, что нарушает соглашения для метода equals. Однако здесь есть оператор instanseof, и если его первый операнд равен null, то независимо от типа второго операнда он возвратит false (JLS,15.19.2]. Поэтому при передаче null проверка типа вернет false, и, следовательно, нет необходимости делать отдельную проверку для null: Собрав все это вместе, получаем рецепт для создания высококачественного метода equals: Используйте оператор
    == для проверки, является ли аргумент ссылкой на указанный объект, Если является, возвращайте true . Это всего лишь способ повьшreния производительности программы, которая будет низкой, если процедура сравнения оказывается трудоемкой.
    2. Используйте оператор instanceof для проверки, имеет ли аргумент правильный тип. Если не имеет, возвращайте false. Обычно правильный тип - это тип того класса, которому принадлежит данный метод. В некоторых случаях это может быть какой-либо интерфейс, реализуемый данным классом. Если класс реализует интерфейс, который уточняет соглашения для метода equals, тов качестве типа указывайте этот интерфейс, что позволит выполнять сравнение классов, реализующих интерфейс. Подобным свойством обладают интерфейсы коллекций Set, List, Мари Мар.
    Entry.
    3. Приводите аргумент к правильному типу. Поскольку эта операция следует за проверкой i.nstanceof, она гарантированно будет выполнена.
    4. Пройдитесь по всем "значимым" полям класса и убедитесь в том, что значение поля в аргументе и значение того же поля в объекте соответствуют друг другу. Если проверки для всех полей прошли успешно, возвращайте результат true, в противном случае - false. Если на шаге 2 тип был определен как интерфейс, вы должны получать доступ к значимым полям аргумента, используя методы самого интерфейса. Если же тип аргумента определен как класс, тов зависимости от условий вам, возможно, удастся получить прямой доступ к полям аргумента. Для простых полей, за исключением типов float и double, для сравнения применяйте оператор
    ==. Для полей со ссылкой на объекты рекурсивно
    вызывайте метод equals. Для поля float преобразуйте его значение в int с помощью метода
    Float. floatTolntBi ts, а затем сравнивайте полученные значения, используя оператор -- для полей double преобразуйте их значения в long с помощью метода Double. doubleToLongBi ts, а затем сравнивайте полученные значения long, используя оператор
    ==. Особая процедура обработки полей float и double нужна потому, что существуют особые значения Float. NaN, о .
    Of, а также аналогичные значения для типа double. См. документацию по Float. equals.) При работе с полями массивов применяйте перечисленные правила для каждого элемента отдельно. Некоторые поля, предназначенные для ссылки на объекты, вполне оправданно могут иметь значение null. Чтобы не допустить возникновения исключительной ситуации
    NullPointerException, для сравнения подобных полей используйте следующую идиому
    (field
    = = null
    ? о. field
    = = null : field. equals( о. Field)) Если field и о. field часто ссылаются на один и тот же объект, 'следующий альтернативный вариант может оказаться быстрее
    (field
    = = о. field
    1 1
    (field
    !
    = null
    & & field. equals( о, field))) для некоторых классов, например для представленного выше CaselnsensitiveString, сравнение полей оказывается гораздо сложнее, чем простая проверка равенства. Так ли это, должно быть понятно из спецификации на соответствующий класс. Если так, то, возможно, потребуется придать каждому объекту некую каноническую форму. В результате метод equals сможет выполнять' простое и точное сравнение канонических форм вместо того, чтобы пользоваться более трудоемкими неточным вариантом сравнения. Описанный прием более подходит для неизменяемых классов (статья 13), поскольку, когда объект меняется, приходится приводить его каноническую форму в соответствие последним изменениям. На производительность метода equals может оказывать влияние очередность сравнения полей. для достижения наилучшей производительности нужно в первую очередь сравнивать те поля, которые будут различаться с большей вероятностью, либо те, которые сравнивать проще. В идеале оба эти качества должны совпадать. Не следует сравнивать поляне являющиеся частью логического состояния объекта, например поля Object, используемые для синхронизации операций. Нет необходимости сравнивать избыточные поля, значение которых можно вычислить, основываясь на "значащих полях" объекта, однако сравнение этих полей может, повысить производительность метода equals. Если значение
    избыточного поля равнозначно суммарному описанию объекта в целом, то сравнение подобных полей позволит сэкономить на сравнении действительных данных, если будет выявлено расхождение.
    5. Закончив написание собственного метода equals, задайте себе вопрос является ли он симметричным, транзитивными непротиворечивым (Оставшиеся два свойства обычно получаются сами собой) Если ответ отрицательный, разберитесь, почему не удалось реализовать эти свойства, и подправьте метод соответствующим образом. в качестве конкретного примера метода equals, который был выстроен по приведенному выше рецепту, можно посмотреть г из статьи
    8. Несколько заключительных предостережений Переопределяя метод equals, всегда переопределяйте метод hashCode (статья
    8). Не старайтесь быть слишком умным. Если вы проверяете лишь равенство полей, соблюдать условия соглашений для метода equals совсем нетрудно. Если же в поисках равенства вы излишне агрессивны, можно легко нарваться на неприятности. Так, использование синонимов в каком бы тони было обличии обычно оказывается плохим решением. Например, класс File не должен пытаться считать равными символьные связи (в системе UNIX), относящиеся к одному и тому же файлу. К счастью, он этого и не делает. Не надо писать метод equals, использующий ненадежные ресурсы. Если выделаете это, то соблюсти требование непротиворечивости будет крайне трудно. Например, метод equals в классе java.net.URL использует д хостов, соответствующих сравниваемым адресам
    URL. Процедура преобразования имени хоста в IР-адрес может потребовать выхода в компьютерную сеть, и нет гарантии, что это всегда будет давать один и тот же результат. Это может привести к нарушению соглашений для метода equals, сравнивающего адреса URL, и на практике уже, создавало проблемы. К сожалению, описанную схему сравнения уже нельзя поменять из-за требований обратной совместимости) За некоторыми исключениями, методы equals обязаны выполнять детерминированные операции с объектами, находящимися в памяти. Декларируя метод equals, ненужно указывать вместо Object другие типы объектов. Нередко программисты пишут метод equals следующим образом, а потом часами ломают голову над тем, почему он не работает правильно public boolean equals(MyClass о) {
    }
    Проблема заключается в том, что этот метод не переопределяет) метод Object. equals, чей аргумент имеет типа перегружает его (overload) (статья 26). Подобный "строго типизированный" метод equals можно создать в дополнение к обычному методу equals, однако поскольку оба метода возвращают один и тот результат, нет никакой причины делать это. При определенных условиях это может дать минимальный выигрыш в производительности, ноне оправдывает дополнительного усложнения программы (статья
    37). Переопределяя метод, всегда пер ео предел яй те h ash C o d e Распространенным источником ошибок является отсутствие переопределения метода hashCode. Вы должны переопределять метод hashCode в каждом классе, где переопределен метод equals. Невыполнение этого условия приведет к нарушению общих соглашений для метода Object.hashCode, а это не позволит вашему классу правильно работать в сочетании с любыми коллекциями, построенными на использовании хэш-таблиц, в том числе си. Приведем текст соглашений, представленных в спецификации j ауа.
    lang.Object: Если вовремя работы приложения несколько раз обратиться к одному и тому же объекту, метод hashCode должен постоянно возвращать одно и тоже целое число, показывая тем самым, что информация, которая используется при сравнении этого объекта с другими метод equals), не поменялась. Однако если приложение остановить и запустить снова, это число может стать другим. Если метод equals(Object) показывает, что два объекта равны друг другу, то вызвав для каждого из них метод hashCode, вы должны получить в обоих случаях одно и тоже целое число. Если метод equals(Object) показывает, что два объекта неравны друг другу, вовсе необязательно, что метод hashCode возвратит для них разные числа. Между тем программист должен понимать, что генерация разных чисел для неравных объектов может повысить эффективность хэш-таблиц. Главным является второе условие равные объекты должны иметь одиноко - вый хаш·код. Если вы не переопределите метод hashCode, оно будет нарушено два различных экземпляра сточки зрения метода equals могут быть логически равны, Однако для метода hashCode из класса Object это всего лишь два объекта, не имеющих между собой ничего общего. Поэтому метод hashCode скорее всего возвратит для этих объектов два случайных числа. а неодинаковых, как того требует соглашение.
    В качестве примера рассмотрим следующий упрощенный класс
    PhoneNumber, в котором метод equals построен по рецепту из статьи
    7: import java.util.*;
    public final class PhoneNumber {
    private final short areaCode;
    private final short exchange;
    private final short extension;
    public PhoneNumber(int areaCode, int exchange,
    int extension) {
    rangeCheck(areaCode, 999, "area code");
    rangeCheck(exchange, 999, "exchange");
    rangeCheck(extension, 9999, "extension");
    this.areaCode = (short) areaCode;
    this.exchange = (short) exchange;
    this.extension = (short) extension;
    }
    private static void rangeCheck(int arg, int max,
    String name) {
    if (arg < 0 || arg > max)
    throw new IllegalArgumentException(name +": " + arg);
    }
    public boolean equals(Object o) {
    if (o == this)
    return true;
    if (!(o instanceof PhoneNumber))
    return false;
    PhoneNumber pn = (PhoneNumber)o;
    return pn.extension == extension &&
    pn.exchange == exchange &&
    pn.areaCode == areaCode;
    }
    // Нет метода hashCode!
    // Остальное опущено Предположим, что вы попытались использовать этот класс с
    HashMap:
    Мар m = new HashMap(); m. put (new PhbneNumber( 408, 867, 5309), "Jenny"); Вы вправе ожидать, что m.get(new PhoneNumber(408, 867, 5309)) возвратит строку "Jenny", однако он выдает null. Заметим, что здесь задействованы два экземпляра класса PhoneNumber: один используется для вставки в таблицу HashMap, а другой,
    равный ему экземпляр- для поиска. Отсутствие в классе PhoneNumber переопределенного метода hashCode приводит к тому, что двум равным экземплярам соответствует разный хэш-код, те. имеем нарушение соглашений для этого метода. Как следствие, метод get ищет указанный телефонный номер в другом сегменте хэш-таблицы, а не там, где была сделана запись с помощью метода put. Разрешить эту проблему можно, поместив в класс PhoneNumber правильный метод hashCode. Как же должен выглядеть метод hashCode? Написать действующий, ноне слишком хороший метод нетрудно. Например, следующий метод всегда приемлем, но пользоваться им не надо никогда
    // Самая плохая из допу от им ы х хэш функций никогда // не пользу йт еоь
    ею
    public int hashCode() { return 42; } Данный метод приемлем, поскольку для равных объектов он гарантирует возврат одного итого же хэш-кода. Плохо то, что он гарантирует получение одного итого же хэш-кода для любого объекта. Соответственно, любой объект будет привязан к одному и тому же сегменту хэш-таблииы, асами хэш- таблииы вырождаются в связные списки, для программ, время работы которых с ростом хэш-таблии должно увеличиваться линейно, имеет место квадратичная зависимость. Для больших хэш-таблии это равносильно переходу от работоспособного к неработоспособному варианту. Хорошая хэш-функция стремится генерировать для неравных объектов различные хэш-коды. И это именно то, что подразумевает третье условие в соглашениях для hashCode. В идеале хэш-функция должна равномерно распределять любое возможное множество неравных экземпляров класса по всем возможным значениям хэш-кода. Достичь этого может быть чрезвычайно сложно. К счастью, не так трудно получить хорошее приближение. Приведем простой рецепт Присвойте переменной result тип int) некоторое ненулевое число, скажем,
    17. Для каждого значимого поля f в вашем объекте (те. поля, значение которого принимается в расчет методом equals), выполните следующее а
    Вычислите для поля хэш-код с (тип int):
    а
    1.
    Если поле имеет тип boolean, вычислите
    (f
    ? О : Если поле имеет тип byte, char, short или int, вычислите Если поле имеет тип long, вычислите
    (int)(f - (f
    > > > Если поле имеет тип float, вычислите
    Float. floatтoIntBits(f).
    5.
    Если - тип double, вычислите
    Double. doubleToLongBits(f), а затем преобразуйте полученное значение, как указано в п.
    2.a.3.

    37 Если поле является ссылкой на объекта метод equals данного класса сравнивает это поле, рекурсивно вызывая другие методы equals, также рекурсивно вызывайте для этого поля метод hashCode. Если требуется более сложное сравнение, вычислите для данного поля каноническое представление (canonical representation), а затем вызовите для него метод hashCode. Если значение поля равно null, возвращайте О можно любую другую константу, но традиционно используется О ). Если поле является массивом, обрабатываете его так, как если бы каждый его элемент был отдельным полем. Иными словами, вычислите хэш-код для каждого значимого элемента, рекурсивно применяя данные правила, а затем объедините полученные значения так, как описано в п. Ь. b. Объедините хэш-код с, вычисленный на этапе ас текущим значением поля resul t следующим образом result = 37*result + с Верните значение resul t. Закончив писать метод hashCode, спросите себя, имеют ли равные экземпляры одинаковый хэш-код. Если нет, выясните, в чем причина, и устраните проблему. Из процедуры получения хэш-кода можно исключить избыточные поля. Иными словами, можно исключить любое поле, чье значение можно вычислить, исходя из значений полей, задействованных в рассматриваемой процедуре. Вы обязаны исключать из процедуры все поля, которые не используются входе проверки равенства. Иначе может быть нарушено второе правило в соглашениях для hashCode. На этапе 1 используется ненулевое начальное значение. Благодаря этому не будут игнорироваться те обрабатываемые в первую очередь поля, у которых значение хэш-кода, полученное на этапе а, оказалось нулевым. Если жена этапе 1 в качестве начального значения использовать нуль, тони одно из этих обрабатываемых в первую очередь полей не сможет повлиять на общее значение хэш-кода, что способно привести к увеличению числа коллизий. Число 17 выбрано произвольно. Умножение на шаге Ь создает зависимость значения хэш-кода от очередности обработки полей, а это обеспечивает гораздо лучшую хэш-функцию в случае, когда в классе много одинаковых полей. Например, если из хэш-функции для класса
    String, построенной поэтому рецепту, исключить умножение, то все анаграммы (слова, полученные от некоего исходного слова путем перестановки букв) будут иметь один и тот же хэш-код. Множитель 37 выбран потому, что является простым нечетным числом. Если бы это было четное число и приумножении произошло переполнение,
    информация была бы потеряна, поскольку умножение числа на 2 равнозначно его , арифметическому сдвигу. Хотя преимущества от применения простых чисел не столь очевидны, именно их принято использовать для этой цели. Используем описанный рецепт для класса PhoneNumber. В нем есть три значимых поля, все имеют тип short. Прямое применение рецепта дает следующую хэш-функцию public int hashCode() { int result = 17; result = 37*result + areaCode; result = 37*result + exchange; result = 37*result + extension;
    \ return result; Поскольку этот метод возвращает результат простого детерминированного вычисления, исходными данными для которого являются три значащих поля в экземпляре
    PhoneNumber, очевидно, что равные экземпляры
    PhoneNumber будут иметь равный хэш-код. Фактически этот метод является абсолютно правильной реализацией hashCode для класса
    PhoneNumber наряду с методами из библиотек Java версии 1.4. Он прост, довольно быстр и правильно разносит неравные телефонные номера по разным сегментам хэш-таблицы. Если класс является неизменными при этом важны затраты на вычисление хэш-кода, вы можете сохранять хэш-код в самом объекте вместо того, чтобы вычислять его всякий раз заново, как только в нем появится необходимость. Если вы полагаете, что большинство объектов данного типа будут использоваться как ключи в хэш-таблице, вам следует вычислять соответствующий хэш-код уже в момент создания соответствующего экземпляра. С другой стороны, вы можете выбрать инициализацию , отложенную до первого обращения к методу hashCode статья 48). Хотя достоинства подобного режима для нашего класса
    PhoneNumbers не очевидны, покажем, как это делается
    1   2   3   4   5   6   7   8   9   ...   25


    написать администратору сайта