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

Джош Блох

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

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

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

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

public final class CaselnsensitiveString { private String s;

public CaseInsensitiveString(String s) { if (s == null)

throw new NullPointerException(); this.s = s;

}

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

@0verride public boolean equals(Object o) { if (o instanceof CaselnsensitiveString) return s.equalsIgnoreCase(

((CaselnsensitiveString) o) .s);

if (o instanceof String) // Одностороннее взаимодействие! return s.equalsIgnoreCase((String) o);

return false;

}

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

}

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

CaselnsensitiveString cis = new CaseInsensitiveString(“Polish”); String s = “polish”;

Как и предполагалось, выражение cis.equals(s) возвращает true. Проблема заключается в том, что, хотя метод equals в классе CaselnsensitivenessString знает о существовании обычных строк,

50

С татья 8

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

List<CaseInsensitiveString> list =

new ArrayList<CaseInsensitiveString>(); list.add(cis);

Какое значение после этого возвратит

выражение list, con-

tains(s)? Кто знает. В текущей версии JD K

от компании Sun вы­

яснилось, что он возвращает false, но это всего лишь особенность реализации. В другой реализации с легкостью может быть возвраще­ но true или во время выполнения будет инициирована исключитель­ ная ситуация. Как только вы нарушили соглашение для equals, вы просто не можете знать, как поведут себя другие объекты, столкнувшись с вашим объектом.

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

@0verride public boolean equals(Object о) { return о instanceof CaselnsensitiveString &&

((CaselnsensitiveString) o) .s.equalsIgnoreCase(s);

}

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

51

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

public class Point { private final int x; private final int y;

public Point(int x, int y) { this.x = x;

this.у = у;

}

@0verride public boolean equals(Object o) { if (!(o instanceoff Point))

return false; Point p = (Point)o;

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

}

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

}

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

public class ColorPoint extends Point { private final Color color;

public ColorPoint(int x, int y, Color color) { super(x, y);

this.color = color;

}

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

}

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

52

С татья 8

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

@0verride public boolean equals(Object о) { if (!(o instanceof ColorPoint))

return false;

return super.equals(o) && ((ColorPoint) o) .color == color;

}

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

Point р = new Point(1, 2);

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

После этого выражение p.equals(cp) возвратит true, acp.equals(p)

возвратит false. Вы можете попытаться решить эту проблему, за­ ставив метод ColorPoint. equals игнорировать цвет при выполнении «смешанных сравнений»:

// Ошибка - нарушение транзитивности

@0verride public boolean equals(Object о) { if (!(o instanceof Point))

return false;

//Если о - обычная точка, выполнить сравнение без проверки цвета if (!(о instanceof ColorPoint))

return о.equals(this);

//Если о - цветная точка, выполнить полное сравнение

return super.equals(o) && ср.color == color;

}

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

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

Point p2 = new Point(1, 2);

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

53

getClass

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

В этот момент выражения р1. equals(р2) и р2. equals(рЗ) воз­ вращают значение true, а р1. equals(рЗ) возвращает false — прямое нарушение транзитивности. Первые два сравнения игнорируют цвет, в третьем цвет учитывается.

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

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

stanceof в методе equals:

// Ошибка - нарушен принцип подстановки Барбары Лисков (с. 40)

@0verride public boolean equals(0bject о) { if (о == null || o.getClassO !- getClassO)

return false; Point p = (Point) o;

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

}

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

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

//Инициализируем единичную окружность, содержащую все точки

//этой окружности.

private static final Set<Point> unitCircle; static {

unitCircle - new HashSet<Point>(); unitCircle.add(new Point( 1, 0));

54

С татья 8

кUAWAWAU,m tiunM »LW.Im wгн/глцrt^>w>ww>iwwaaw » a

unitCircle.add(new Point( 0, 1)); unitCircle.add(new Point(-1, 0)); unitCircle.add(new Point( 0, -1));

}

public static boolean onllnitCircle(Point p) { return unitCircle.contains(p);

}

Может, это и не самый быстрый способ применения данной функции, но работает он превосходно. Предположим, вы хотите рас­ ширить Point неким тривиально простым способом, который не до­ бавляет значение компоненту, скажем, используя его конструктор для отслеживания, сколько экземпляров было создано:

public class CounterPoint extends Point { private static final Atomiclnteger counter = new AtomicInteger();

public CounterPoint(int x, int y) { super(x, y); counter.incrementAndGet();

}

public int numberCreated() { return counter.get(); }

}

Принцип подстановки Барбары Лисков утверждает, что лю­ бое важное свойство типа должно содержаться также в его подтипе. Таким образом, любой метод, написанный для типа, должен также работать и на его подтипах [Liskov87]. Но предположим, мы пе­ редаем экземпляр CounterPoint методу onllnitCircle. Если класс Point использует метод equals, основанный на getClass, то метод onllnitCircle возвратит значение false независимо от значений х и у экземпляра CounterPoint. Это происходит потому, что кол­ лекция, такая как Hash Set, используемая методом onllnitCircle, ис­ пользует метод equals для проверки содержимого, и ни один экзем­ пляр CounterPoint не равен ни одному экземпляру Point. Если тем не менее вы корректным образом используете метод equals на ос­

55

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

нове instanceof на экземпляре Point, тот же самый метод onllnitCircle будет работать хорошо, если будет представлен экземпляром

CounterPoint.

В то время как нет удовлетворительного способа расширить по­ рождающий экземпляры класс и добавить компоненты значений, есть замечательный обходной путь. Следуйте рекомендациям из ста­ тьи 16, «Наследованию предпочитайте компоновку». Вместо того чтобы экземпляром ColorPoint расширять экземпляр Point, создайте в ColorPoint закрытое поле Point и открытый метод view (статья 5), который возвратил бы обычную точку в ту же самую позицию, что

ицветную точку

//Добавляет компонент значения, не нарушая соглашения eqials. public class ColorPoint {

private final Point point; private final Color color;

public ColorPoint(int x, int y, Color color) {

if (color == null)

throw new NullPointerException(); point = new Point(x, y); this.color = color;

}

/* *

* Возвращает вид этой цветной точки.

*/

public Point asPointO { return point;

}

(©Override public boolean equals(Object o) { if (!(o instanceof ColorPoint))

return false;

ColorPoint cp = (ColorPoint) o;

return cp.point.equals(point) && cp.color.equals(color);

}

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

56

С татья 8

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

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

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

Вне зависимости от того, является ли класс неизменяемым или нет, не ставьте метод equals в зависимость от ненадежных ресур-

57

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

сов. Очень трудно соблюдать требование непротиворечивости, если вы нарушаете его запреты. Например, метод equals, принадлежащий java.net.URL, полагается на сравнение IP адресов для хоста, ассо­ циирующегося с этим U R L . Перевод имени хоста в IP адрес может потребовать доступа к сети, и нет гарантии, что с течением времени результат не изменится. Это может привести к тому, что метод U R L equals нарушит соглашения equals и на практике будут наблюдать­ ся проблемы. сожалению, такое поведение невозможно изменить в связи с требованиями совместимости.) З а очень небольшим исклю­ чением, методы equals должны выполнять детерминистские расчеты на находящихся в памяти объектах.

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

@0verride public boolean equals(Object о) { if (о == null)

return false;

}

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

@0verride public boolean equals(Object о) { if (!(o instanceof MyType))

58

С татья 8

return false;

}

Если бы эта проверка типа отсутствовала, а метод equals получил аргумент неправильного типа, то он бы инициировал исключительную ситуацию ClassCastException, что нарушает соглашения для метода equals. Однако здесь представлен оператор instanseof, и если его пер­ вый операнд равен null, то, вне зависимости от типа второго операнда, он возвратит false [JLS, 15.20.2]. Поэтому, если был передан null, проверка типа возвратит false и, соответственно, вам нет необходимо­ сти делать отдельную проверку для null. Собрав все это вместе, полу­ чаем рецепт для создания высококачественного метода equals:

1.Используйте оператор == для проверки, является ли ар­ гумент ссылкой на указанный объект. Если это так, воз­ вращайте true. Это всего лишь способ повысить производи­ тельность программы, которая будет низкой, если процедура сравнения может быть трудоемкой.

2.Используйте оператор instanceof для проверки, имеет ли аргумент правильный тип. Если это не так, возвращай­ те false. Обычно правильный тип — это тип того класса, которому принадлежит данный метод. В некоторых случаях это может быть какой-либо интерфейс, реализуемый этим классом. Если класс реализует интерфейс, который уточняет соглашения для метода equals, то в качестве типа указывайте этот интерфейс, что позволит выполнять сравнение классов, реализующих этот интерфейс. Подобным свойством облада­

ют интерфейсы коллекций Set, List, Мар и Map. Entry.

3. Приводите аргумент к правильному типу. Поскольку эта операция следует за проверкой instanceof, она гарантиро­ ванно будет выполнена.

4. Пройдитесь по всем «значимым» полям класса и убеди­ тесь в том, что значение такого поля в аргументе и зна­ чение того же поля в объекте соответствуют друг другу.

59

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