Главная » Java, Советы » Переопределяя метод equals, соблюдайте общие соглашения

0

 

Переопределение метода equals кажется простой операцией, однако есть множество способов неправильного ее выполнения, и последствия этого могут быть ужасны. Простейший способ избежать проблем: вообще не переопределять метод equals. В этом случае каждый экземпляр класса будет равен только самому себе. Это решение будет правильным, если выполняется какое-либо из следующих условий:

 

 

Каждый экземпляр класса внутренне уникален. Это утверждение справедливо для таких классов как Thread, которые представляют не величины, а активные сущности. Реализации метода equals, предлагаемая классом Object, для этих классов работает совершенно правильно.

 

·         24Вас не интересует, предусмотрена ли в классе проверка "логического равенства". Например, в классе java.util.Random можно было бы переопределить метод equals с тем, чтобы проверять, будут ли два экземпляра Random генерировать одну и ту же последовательность случайных чисел, однако разработчики посчитали, что клиенты не должны знать о такой возможности и она им не понадобится. В таком случае тот вариант метода equals, который наследуется от класса Object, вполне приемлем.

·         Метод equals уже переопределен в суперклассе, и функционал, унаследованный от суперкласса, вполне приемлем дли данного класса. Например, большинство реализаций интерфейса Set наследует реализацию метода equals от Класса AbstractSet, List наследует реализацию от AbsctractList, а Мар – от AbstractMap.

·         Класс является закрытым или доступен только в пределах пакета, и вы уверены, что его метод eQuals никогда не будет вызван. Сомнительно, что в такой ситуации метод eQuals  следует переопределять, разве что на тот случай, если его однажды случайно вызовут:

Public Boolean equals (Object о) {

Throw new UnsupportedOperationException ();

}

 

Так когда же имеет смысл переопределять Object. equals? Тогда, когда для класса определено понятие логической эквивалентности (Jogica! equality), которая не совпадает с тождественностью объектов, а метод equals в суперклассе не был переопределен с тем, чтобы реализовать требуемый функционал. Обычно это случается с классами значении, такими как Integer или Date. Программист, сравнивающий ссылки на объекты значений с помощью метода equals, желает, скорее всего, выяснить, являются ли они логически эквивалентными, а не просто узнать, указывают ли эти ссылки на один и тот же объект. Переопределение метода equals необходимо не только для того, чтобы удовлетворить ожидания программистов, оно позволяет использовать экземпляры класса в качестве ключей в некоей схеме или элементов в некоем наборе, Имеющих необходимое и предсказуемое поведение.

Существует один вид классов значений, которым не нужно переопределение метода equals,- перечисление типов (статья 21). Поскольку для классов этого типа гарантируется, что каждому значению соответствует не больше одного объекта, метод equals из Object для этих классов будет равнозначен методу логического сравнения.

 

25Переопределяя метод equals, вы должны твердо придерживаться принятых для него общих соглашений. Воспроизведем эти соглашения по тексту спецификации java,lang.Object:

Метод equals реализует отношение эквивалентности:

·     Рефлективность, для любой ссылки на значение х выражение х.equals(x) должно возвращать true.

·     Симметричность, для любых ссылок на значения х и у

выражение х. equals(y) должно возвращать t гue тогда и только тогда, когда y.equals(x) возвращает true.

·     Транзитивность, для любых ссылок на значения х, у и z,

если x.equals(y) возвращает true и y.equals(z) возвращает true, то и выражение х. equals(z) должно возвращать true.

·     Непротиворечивость. Для любых ссылок на значения х и у,

если несколько раз вызвать х. equals(y), постоянно будет возвращаться значение true либо постоянно будет возвращаться значение false при условии, что никакая информация, используемая при сравнении объектов, не поменялась.

·     Для любой ненулевой ссылки на значение х выражение х. equals(null) должно возвращать false.

Если у вас нет склонности к математике, все это может показаться ужасным, однако игнорировать это нельзя! Если вы нарушите условия, то рискуете получить программу, которая работает неустойчиво или заканчивается с ошибкой, а установить источник ошибок крайне сложно. Перефразируя Джона Донна (John Dоппе), можно сказать: ни один класс – не остров. ("Нет человека, что был бы сам по себе, как остров … " – Джон Донн, "Взывая на краю".- Прим. пер.) Экземпляры одного класса часто передаются другому классу. Работа многих классов, в том числе всех классов коллекции, зависит от того, соблюдают ли передаваемые им объекты соглашения для метода equals.  

Теперь рассмотрим внимательнее соглашения для метода equals. На самом деле они не так уж сложны. Как только вы их поймете, придерживаться их будет совсем не Трудно.

Рефлективность. Первое требование говорит о том, что объект должен быть равен самому себе. Трудно представить себе непреднамеренное нарушение этого требования. Если вы нарушили его, а затем добавили экземпляр в ваш класс коллекции, то метод contain этой коллекции почти наверняка сообщит вам, что в коллекции нет экземпляра, которой вы только что добавили.

Симметрия. Второе требование гласит, что любые два объекта должны сходиться во мнении, равны ли они между собой. В отличие от предыдущего, представить

 

непреднамеренное нарушение этого требования несложно. Например, рассмотрим следующий класс:

 

  /**

* Строка без учета регистра. Регистр исходной строки сохраняется

* методом toString, однако, при сравнениях игнорируется.

*/

 

public final class CaseInsensitiveString {

    private String s;

 

    public CaseInsensitiveString(String s) {

        if (s == null)

            throw new NullPointerException();

        this.s = s;

    }

 

// Ошибка: нарушение симметрии!

public boolean equals(Object o) {

        if (o instanceof CaseInsensitiveString)

            return s.equalsIgnoreCase(

                ((CaseInsensitiveString)o).s);

        if (o instanceof String)  // One-way interoperability!

            return s.equalsIgnoreCase((String)o);

        return false;

    }

 

                       /*  Одностороннее взаимодействие!

    // Fixed

    public boolean equals(Object o) {

        return o instanceof CaseInsensitiveString &&

            ((CaseInsensitiveString)o).s.equalsIgnoreCase(s);

    }

 

*/

 

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

 

 

   static void main(String[] args) {

        CaseInsensitiveString cis = new CaseInsensitiveString("Polish");

        String s = "polish";

 

        System.out.println(cis.equals(s));

        System.out.println(s.equals(cis));

    }

}

Исходя из лучших побуждений, метод equals в этом классе наивно пытается взаимодействовать с обычными строками. Предположим, что у нас есть одна строка, независимая от регистра, и вторая – обычная.

Caselnsensiti veString cis = new Caselnsensiti veString ("Polish");

String s = "polish";

Как и предполагалось, выражение cis,equals(s) возвращает tгue. Проблема заключается в том, что хотя метод equals в классе CaselnsensitiveStгing знает о существовании обычных строк, метод equals в классе String не догадывается о строках, нечувствительных к регистру. Поэтому выражение s.equals(cis) возвращает false, явно нарушая симметрию. Предположим, вы помещаете в коллекцию строку, нечувствительную к регистру:

List list =new ArrayList();

list.add(cis);

 

Какое значение возвратит выражение list.contains(s)? Кто знает. В текущей версии JOK от компании Sun выяснилось, что оно возвращает false, но это всего лишь особенность реализации. В другой реализации может быть возвращено true или во время выполнения будет инициирована исключительная ситуация. Нарушив соглашение для equals, вы не можете знать, как поведут себя другие объекты, столкнувшись с вашим объектом.

Для устранения этой проблемы удалите из метода equals попытку взаимодействия с классом String.  Сделав это, вы сможете перестроить метод так, чтобы он содержал один оператор возврата:

Public Boolean equals (Object о) {

Return о instanceof CaseInsensitiveString&&

((CaseInsensitiveString) o), s.equalsIgnoreCase(s);

Транзитивность. Третье требование в соглашениях для метода equals гласит: если один объект равен второму, а второй объект равен третьему, то и первый объект должен быть равен третьему объекту. И вновь несложно представить непреднамеренное нарушение этого требования. Допустим, что программист создает подкласс, придающий своему суперклассу новый аспект. иными словами, подкласс привносит некую информацию, окаэываl0~УЮ влияние на процедуру сравнения. Начнем с простого неизменяемого класса, соответствующего точке в двухмерном пространстве:

public class Point {

 private final int x;

private final int y;

public Point(int x, int y) {

        this.x = x;

        this.y = y;

}

public boolean equals(Object o) {

        if (!(o instanceof Point))

            return false;

        Point p = (Point)o;

        return p.x == x && p.y == y;

    }

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

Предположим, что вы хотите расширить этот класс, добавив понятие цвета:

public elass ColorPoint  extends Point

private Color color;

public colorPoint(int х, int у, Color color) {

super(x, у);

this.color = color;

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

Как должен выглядеть метод equals? Если вы оставите его как есть, реализация метода будет наследоваться от класса Point, и при сравнении с помощью методов equals информация о цвете будет игнорироваться. Хотя такое решение и не нарушает общих соглашений для метода equals, очевидно, что оно неприемлемо. Допустим, вы пишите метод equals, который возвращает значение t rue, только если его аргументом является цветная точка, имеющая то же положение и тот же цвет:

 

// Ошибка: нарушение симметрии!

publie 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);

Выражение р.equals(cp) возвратит true, а cр.equals(p) возвратит 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;

}29

 

Такой подход обеспечивает симметрию, но за счет транзитивности:

ColorPoint р1 = new ColorPoint(1, 2, Color.RED);

Point р2 = new Point(1, 2);

ColorPoint р3 = new ColorPoint(1, 2, Color.BLUE);

В этом случае выражения р1.equals(p2) и р2.equals(p3) возвращают значение 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);

}

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

}30

В библиотеках для платформы Java содержатся классы, которые являются подклассами для класса, создающего экземпляры, и при этом придают ему новый аспект. Например, java.sql.Тimestamp является подклассом класса java.util,Date и добавляет поле для наносекунд. Реализация метода equals в Тimestamp нарушает правило симметрии, и это может привести к странному поведению программы, если объекты Timestamp и Date использовать в одной коллекции или смешивать как-нибудь иначе. В документации к классу Тimestamp есть предупреждение, предостерегающее программиста от смешивания объектов Date и Timestamp. Пока вы не смешиваете их, у вас проблем не будет, но если вы сделаете это, устранение возникших в результате ошибок может быть непростым. Класс Timestamp не является правильным, и подражать ему не надо.

Заметим, что вы можете добавить аспект в подкласс абстрактного класса, не нарушая при этом соглашений для метода equals. Это важно для тех разновидностей иерархии классов, которые вы получите, следуя совету из статьи 20: "Заменяйте объединение иерархией классов". Например, вы можете иметь простой абстрактный класс Shape, а также подклассы Ci,rcle, добавляющий поле радиуса, и Rectangle, добавляющий поля длины и ширины. Только что продемонстрированные проблемы не будут возникать до тех пор, пока нет возможности создавать экземпляры суперкласса.

Непротиворечивость. Четвертое требование в соглашениях для метода equals гласит: если два объекта равны, они должны быть равны все время, пока один из них (или оба) не будет изменен. Это не столько настоящее требование, сколько напоминание о том, что изменяемые объекты в разное время могут быть равны разным объектам, а неизменяемые объекты – не могут. Когда вы пишите класс, подумайте, не следует ли его сделать неизменяемым (статья 13). Если вы решите, что это необходимо, позаботьтесь о том, чтобы ваш метод equals выполнял это ограничение: равные объекты должны оставаться все время равными, а неравные объекты – соответственно, неравными.

Отличие от null (non-nullity). Последнее требование гласит, что все объекты должны отличаться от нуля (null). Трудно себе представить, что в ответ на вызов o.equals(null) будет случайно возвращено значение true, однако вполне вероятно случайное инициирование исключительной ситуации NullPointerException. Общие соглашения этого не допускают. Во многих классах методы equals имеют защиту в виде явной про верки аргумента на null:

 

public boolean equals(Object о) {

if (о == null)

return false;

}

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

 

к его полям. Перед приведением типа метод 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:

1.      Используйте оператор == для проверки, является ли аргумент ссылкой на указанный объект, Если является, возвращайте 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 11 (field ! = null && field. equals( о, field)))

для некоторых классов, например для представленного выше CaselnsensitiveString, сравнение полей оказывается гораздо сложнее, чем простая проверка равенства. Так ли это, должно быть понятно из спецификации на соответствующий класс. Если так, то, возможно, потребуется придать каждому объекту некую каноническую форму.

В результате метод equals сможет выполнять’ простое и точное сравнение канонических форм вместо того, чтобы пользоваться более трудоемким и неточным вариантом сравнения. Описанный прием более подходит для неизменяемых классов (статья 13), поскольку, когда объект меняется, приходится приводить его каноническую форму в соответствие последним изменениям.

На производительность метода equals может оказывать влияние очередность сравнения полей. для достижения наилучшей производительности нужно в первую очередь сравнивать те поля, которые будут различаться с большей вероятностью, либо те, которые сравнивать проще. В идеале оба эти качества должны совпадать.

Не следует сравнивать поля, не являющиеся частью логического состояния объекта, например поля Object, используемые для синхронизации операций. Нет необходимости сравнивать избыточные поля, значение которых можно вычислить, основываясь на "значащих полях" объекта, однако сравнение этих полей может, повысить производительность метода equals. Если значение 33избыточного поля равнозначно суммарному описанию объекта в целом, то сравнение подобных полей позволит сэкономить на сравнении действительных данных, если будет выявлено расхождение.

5.      Закончив написание собственного метода equals, задайте себе вопрос: является ли он симметричным, транзитивным и непротиворечивым? (Оставшиеся два свойства обычно получаются сами собой.) Если ответ отрицательный, разберитесь, почему не удалось реализовать эти свойства, и подправьте метод соответствующим образом.

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

·   Переопределяя метод equals, всегда переопределяйте метод hashCode (статья 8).

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

·   Не надо писать метод equals, использующий ненадежные ресурсы. Если вы делаете это, то соблюсти требование непротиворечивости будет крайне трудно. Например, метод equals в классе java.net.URL использует IP-aдpeca хостов, соответствующих сравниваемым адресам URL. Процедура преобразования имени хоста в IР-адрес может потребовать выхода в компьютерную сеть, и нет гарантии, что это всегда будет давать один и тот же результат. Это может привести к нарушению соглашений для метода equals, сравнивающего адреса URL, и на практике уже, создавало проблемы. (К сожалению, описанную схему сравнения уже нельзя поменять из-за требований обратной совместимости.) За некоторыми исключениями, ‘методы equals обязаны выполнять детерминированные операции с объектами, находящимися в памяти.

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

 

public boolean equals(MyClass о) {

}

 

 

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

 

Источник: Джошуа Блох, Java TM Эффективное программирование, Издательство «Лори»

По теме:

  • Комментарии