в отличие от других обсуждавшихся в этой главе методов, метод соmрагеТо в классе Object не декларируется. Пожалуй, это единственный такой метод в интерфейсе java.lang.СоmрагаЫе. По своим свойствам он похож на метод equals из класса Object, за исключением того, что, помимо простой проверки равенства, он позволяет выполнять упорядочивающее сравнение. Реализуя интерфейс СоmрагаЫе, класс показывает, что его экземпляры обладают естественным своиством упорядочения (natиra! ordering). Сортировка массива объектов, реализующих интерфейс СоmрагаЫе, выполняется просто:
Arrays.sort(a);
Для объектов СоmрагаЫе так же просто выполняется поиск, вычисляются предельные значения и обеспечивается поддержка автоматически сортируемых коллекций. Например, следующая программа, использующая тот факт, что класс String реализует I1нтерфейс СоmрагаЫе, печатает в алфавитном порядке список аргументов, указанных в командной строке, удаляя при этом дубликаты:
public class WordList {
public static void main(String[] args) {
Set 5 = new TreeSet();
s,addAll(Arrays.asList(args));
System.out.println(s);
}
}
Реализуя интерфейс СотрагаЫе, вы разрешаете вашему классу взаимодействовать со всем обширным набором общих алгоритмов и реализаций коллекций, которые связаны с этим интерфейсом. Приложив немного усилий, вы получаете огромные возможности. Практически все классы значений в библиотеках платформы Java реализуют интерфейс СотрагаЫе. И если вы пишите класс значений с очевидным свойством естественного упорядочения - алфавитным, числовым либо хронологическим,- вы должны хорошо продумать реализацию этого интерфейса. В этой статье рассказывается о том, как к этому приступить.
Общее соглашение для метода соmрагеТо имеет тот же характер, что и соглашение
Для метода equals. Приведем его текст по спецификации интерфейса СотрагаЫе:
Выполняет сравнение текущего и указанного объекта и определяет их очередность. Возвращает отрицательное целое число, нуль или положительное целое число в зависимости от того, меньше ли текущий объект, равен или больше указанного объекта. Если тип указанного объекта не позволяет сравнивать его с текущим объектом, инициируется исключительная ситуация ClassCastException.
В следующем описании запись sgп(выражение) обозначает математическую функцию signuт, которая, по определению, возвращает -1, О или 1 в зависимости от того, является ли
значение выражения отрицательным, равным нулю или положительным.
· Разработчик должен гарантировать тождество sgn(x.compareTo(y)) == -sgп(у.соmрагеТо(х)) для всех х и у. (Это подразумевает, что выражение х. сотрагеТо(у) должно инициировать исключительную ситуацию тогда и только тогда, когда у. сотрагеТо(х) инициирует исключение.)
· Разработчик должен также гарантировать транзитивность отношения (х.сотрагеТо(у»О && y.compareTo(z»O) подразумевает x.compareTo(z»O.
· Наконец, разработчик должен гарантировать,
что из тождества х. соmрагеТо(у) == О вытекает тождество sgn(x.compareTo(z)) == sgn(y.compareTo(z)) для всех z.
· Настоятельно рекомендуется выполнять условие
(х. сотрагеТо(у) == О) == (х. equalS(y)). Вообще говоря, для любого класса, который реализует интерфейс СотрагаЫе, но нарушает это условие, сей факт должен быть четко оговорен (в сопроводительной документации). Рекомендуется использовать следующую формулировку: "Примечание: данный класс имеет естественное упорядочение, не согласующееся с условием равенства".
Как и соглашения для метода equals (статья 7), соглашения для соmрагеТо не так сложны, как это кажется. Для одного класса любое разумное отношение упорядочения будет соответствовать соглашениям для соmрагеТо. Для сравнения разных классов метод соmрагеТо, в отличие от метода equals, использоваться не должен: если сравниваются две ссылки на объекты различных классов, можно инициировать исключительную ситуацию ClassCastException. Метод compareTo обычно так и делает. И хотя представленное соглашение не исключает сравнения между классами, в библиотеках для платформы Java, в частности в версии 1.4, нет классов, которые поддерживали бы такую возможность.
Точно так же, как класс, нарушающий соглашения для метода 'hashCode, может испортить другие классы, работа которых зависит от хэширования, класс, не соблюдающий соглашений для метода соmрагеТо, способен нарушить работу других классов, использующих сравнение. К классам, связанным со сравнением, относятся упорядоченные коллекции, TreeSet и ТгееМар, а также вспомогательные классы Collections и Arrays, содержащие алгоритмы поиска и сортировки.
Рассмотрим условия соглашения для сотрагеТо. Первое условие гласит, что если вы измените порядок сравнения двухссылок на объекты, про изойдет вполне ожидаемая вещь: если первый объект меньше второго, то второй должен быть больше первого, если первый объект равен второму, то и второй должен быть равен первому, наконец, если первый объект больше второго, то второй должен быть меньше первого. Второе условие: если первый объект больше второго, а второй объект больше третьего, то первый объект должен быть больше третьего. Последнее условие: объекты, сравнение которых дает равенство, при сравнении с любым третьим объектом должны показывать одинаковый результат.
Из этих трех условий следует, что проверка равенства, осуществляемая с помощью метода соmрагеТо, должна подчиняться тем же самым ограничениям, которые продиктованы соглашениями для метода equals: рефлективность, симметрия, транзитивность и отличие от null. Следовательно, здесь справедливо то же самое предупреждение: невозможно расширить порождающий экземпляры класс, вводя новый аспект и не нарушая при этом соглашения для метода сотрагеТо (статья 7). Возможен обходной Путь. Если вы хотите добавить важное свойство к классу, реализующему интерфейс СотрагаЫе, не расширяйте его, а напишите новый независимый класс, в котором для исходного класса выделено отдельное поле. Затем добавьте метод представления, возвращающий значение этого поля. Это позволит вам реализовать во втором классе любой метод сотрагеТо, который вам нравится. При необходимости клиент может рассматривать экземпляр второго класса как экземпляр первого класса.
Последний пункт соглашений для соmрагеТо, являющийся скорее предположением, чем настоящим условием, постулирует, что проверка равенства, осуществляемая с помощью метода соmрагеТо, обычно должна давать те же самые результаты, что и метод equals. Если это условие выполняется, считается, что упорядочение, задаваемое методом соmрагеТо, согласуется с проверкой равенства (consistent with equals). Если же оно нарушается, то упорядочение называется несогласующимся с проверкой равенства (inconsistent with eqиals).
Класс, чей метод соmрагеТо устанавливает порядок, несогласующийся с условием равенства, будет работоспособен, однако отсортированные коллекции, содержащие элементы этого класса, могут не соответствовать общим соглашениям для соответствующих интерфейсов коллекций (Collection, Set или Мар). Дело в том, что общие соглашения для этих интерфейсов определяются в терминах метода equals, тогда как в отсортированных коллекциях ИСПОЛl>зуется проверка равенства, которая реализуется методом соmрагеТо, а не equals. Если это произойдет, катастрофы не будет, но иногда это следует учитывать.
Например, рассмотрим класс BigDecimal, чей метод соmрагеТо не согласуется с проверкой равенства. Если вы создадите HashSet и добавите в него новую запись BigDecimal("1.0"), а затем BigDecimal("1.00"), этот набор будет содержать два элемента, поскольку два добавленных в него экземпляра класса BigDecimal не будут равны, если их сравнивать с помощью' метода equals. Однако если вы выполняете ту же самую процедуру с TreeSet, а не HashSet, полученный набор будет содержать только один элемент, поскольку два представленных экземпляра BigDecimal оказываются равны при их сравнении с помощью метода соmаргеТо. (См. документацию на BigDecimal.)
Процедура написания метода соmрагеТо похожа на процедуру для метода equals, но есть несколько ключевых различий. Перед преобразованием типа нет необходимости проверять тип аргумента. Если аргумент имеет неправильный тип, метод соmрагеТо обязан инициировать исключительную ситуацию ClassCastException. Если аргумент имеет значение null, метод compare Т о должен инициировать исключительную ситуацию NullPointerException. Т о же самое вы получите, если приведете аргумент к правильному типу, а затем попытаетесь обратиться к его членам.
Сравнение полей само по себе является упорядочивающим сравнением, а не сравнением с проверкой равенства. Сравнение полей, имеющих ссылки на объекты, осуществляйте путем рекурсивного вызова метода сотрагеТо. Если поле не реализует интерфейс Сотра rable или вам необходимо не стандартное упорядочение, вы можете использовать явную реализацию интерфейса Comparator. Либо напишите ваш собственный метод, либо воспользуйтесь уже имеющимся, как это было в случае с методом сотра геТо в классе CaselnsensitiveString (статья 7):
Поля простого типа нужно сравнивать с помощью операторов < и >, массивы применяя эти инструкции для каждого элемента. Если у класса есть несколько значимых полей, порядок их сравнения критически важен. Вы должны начать с самого значимого поля и затем следовать в порядке убывания значимости. Если сравнение дает что-либо помимо нуля (означающего равенство), все, что вам нужно сделать,-
возвратить этот результат. Если самые значимые поля равны, продолжайте сравнивать следующие по значимости поля и т. д. Если все поля равны, равны и объекты, поэтому возвращайте нуль. Такой прием демонстрирует метод соmрагеТо для класса PhoneNumber из статьи 8:
public int compareTo(Object о) {
PhoneNumber рn = (PhoneNumber)o;
// Сравниваем коды зон
if (areaCode < pn.areaCode)
return -1;
if (areaCode > pn.areaCode)
return 1;
// Коды зон равны, сравниваем номера АТС
if (exchange < pn.exchange)
retuгn -1;
if (exchange > pn.exchange)
retuгn 1;
// Коды зон и номера АТС равны, сравниваем номера абонентов
if (extension < pn.extension)
return -1;
if (extension > pn.extension)
return 1;
return о; // Все поля равны
}
Этот метод работает прекрасно, но его можно улучшить. Напомним, что в соглашениях для метода соmрагеТо величина возвращаемого значения не конкретизируется, только знак. Вы можете извлечь из этого пользу, упростив программу и, возможно, заставив ее работать немного быстрее:
public int compareTo(Object о) {
PhoneNumber рп = (PhoneNumber)o;
// Сравниваем коды зон
int areaCodeDiff = areaCode - pn.areaCode;
if (areaCodeDiff 1= О)
return areaCodeDiff;
// Коды зон равны, сравниваем номера АТС
int exchangeDiff = exchange - pn.exchange;
if (exchangeDiff != 0)
return exchangeDiff;
// Коды зон и номера АТС равны, сравниваем номера абонентов
return extension - pn.extension;
}
Такая уловка работает прекрасно, но применять ее следует крайне осторожно.
Не пользуйтесь ею, если у вас нет уверенности в том, что рассматриваемое поле не может иметь отрицательное значение или, что бывает чаще, разность между наименьшим и наибольшим возможными значениями поля меньше или равна значению INTEGER. MAX_VALUE (231 -1). Причина, по которой этот прием не работает, обычно заключается в том, что 32-битовое целое число со знаком является недостаточно большим, чтобы показать разность двух 32-битовых целых чисел с произвольным знаком. Если i - большое положительное целое число, а j - большое отрицательное целое число, то при вычислении разности (i-j) произойдет переполнение и будет возвращено отрицательное значение. Следовательно, полученный нами метод соmрагеТо работать не будет: для некоторых аргументов будет выдаваться бессмысленный результат, тем самым будут нарушены первое и второе условия соглашения для метода соmрагеТо. И эта проблема не является чисто теоретической, она уже вызывала сбои в реальных системах. Выявить причину подобных отказов бывает крайне трудно, поскольку не· правильный метод соmрагеТо работает правильно со многими входными значениями.
Глава 4
Классы и интерфейсы
Классы и интерфейсы занимают в языке программирования Java центральное положение. Они являются основными элементами абстракции. Язык Java содержит множество мощных элементов, которые можно использовать при построении классов и интерфейсов. В этой главе даются рекомендации, которые помогут вам наилучшим образом при менять эти элементы, чтобы ваши классы и интерфейсы были удобными, надежными и гибкими.
Сводите к минимуму доступность классов и членов
Единственный чрезвычайно важный фактор, отличающий хорошо спроектированный модуль от неудачного,- степень сокрытия его внутренних данных и иных деталей реализации от других модулей. Хорошо спроектированный модуль скрывает все детали реализации, четко разделяя свой АРI и реализацию. Модули взаимодействуют друг с другом только через свои API, и ни один из них не знает, какая обработка происходит внутри другого модуля. Эта концепция, называемая сокрытием информации (information hiding) или инкапсуляцией (encapsulatiori), представляет собой один из фундаментальных принципов разработки программного обеспечения [Parnas72].
Сокрытие информации важно по многим причинам, большинство из которых Связано с тем обстоятельством, что этот механизм эффективно изолирует друг от друга модули, составляющие систему, позволяя разрабатывать, тестировать, оптимизировать, использовать, исследовать и обновлять их по отдельности. Благодаря этому ускоряется разработка системы, так как различные модули могут создаваться параллельно. Кроме того, уменьшаются расходы на сопровождение приложения, поскольку каждый модуль можно быстро изучить и отладить, минимально рискуя навредить остальным модулям. Само по себе сокрытие информации не может обеспечить
хорошей производительности, но оно создает условия для эффективного управления производительностью. Когда разработка системы завершена и процедура ее профилирования показала, работа каких модулей вызывает падение производительности (статья 37), можно заняться их оптимизацией, не нарушая функционирования остальных модулей. Сокрытие информации повышает возможность повторного использования программ, поскольку каждый отдельно взятый модуль независим от остальных модулей и часто оказывается полезен в иных контекстах, чем тот, для которого он разрабатывался. Наконец, сокрытие информации уменьшает риски при построении больших систем: удачными могут оказаться отдельные модули, даже если в целом система не будет пользоваться успехом.
Язык программирования java имеет множество возможностей для сокрытия информации. Одна из них - механизм управления доступом (access control) [JLS, 6.6], задающий степень доступности (accessibility) для интерфейсов, классов и членов классов. Доступность любой сущности определяется тем, в каком месте она была декларирована и какие модификаторы доступа, если таковые есть, присутствуют в ее декларации (pr1vate, protected или public). Правильное использование этих модификаторов имеет большое значение для сокрытия информации.
Главное правило заключается в том, что вы должны сделать каждый класс или член максимально недоступным. Другими словами, вы должны использовать самый низший из возможных уровней доступа, который еще допускает правильное функционирование создаваемой программы.
Для классов и интерфейсов верхнего уровня (не являющихся вложенными) существуют лишь два возможных уровня доступа: доступный только в пределах пакета (package-private) и открытый (public). Если вы объявляете класс или интерфейс верхнего уровня с модификатором public, он будет открытым, в противном случае он будет доступен только в пределах пакета. Если класс или интерфейс верхнего уровня можно сделать доступным только в пакете, так и нужно поступать. При этом класс или интерфейс становится частью реализации этого пакета, а не частью его внешнего API. Вы можете модифицировать его, заменить или исключить из пакета, не опасаясь нанести вред клиентам. Если же вы делаете класс или интерфейс открытым, на вас возлагается обязанность всегда поддерживать его с целью сохранения совместимости.
Если класс или" интерфейс верхнего уровня, доступный лишь в пределах пакета, используется только в одном классе, вы должны рассмотреть возможность превращения его в закрытый класс (или интерфейс), который будет вложен именно в тот класс, где он используется (статья 18). Тем самым вы еще более уменьшите его доступность. Однако это уже не так важно, как сделать необоснованно открытый класс доступным только в пределах пакета, поскольку класс, доступный лишь в пакете, уже является частью реализации этого пакета, а не его внешнего API.
Для членов класса (полей, методов, вложенных классов и вложенных интерфейсов) существуют четыре возможных уровня доступа, которые перечислены здесь в порядке увеличения доступности:
· Закрытый (private) - данный член доступен лишь в пределах того класса верхнего уровня, где он был объявлен.
· Доступный лишь в пределах пакета (package-private) - член доступен из любого класса в пределах того пакета, где он был объявлен. Формально этот уровень называется доступом по умолчанию
(default access), и именно этот уровень доступа вы получаете,
если не -были указаны модификаторы доступа.
· Защищенный (protected) - член доступен для подклассов того класса, "где этот член был объявлен (с небольшими ограничениями [jLS, 6.6.2]); доступ к члену можно получить из любого класса в пакете, где этот член был объявлен.
· Открытый (public) - член доступен отовсюду.
После того как для вашего класса тщательно спроектирован открытый API, вам следует сделать все остальные члены класса закрытыми. И только если другому классу из того же пакета действительно необходим доступ к какому-то члену, вы можете убрать модификатор private и сделать этот член доступным в пределах всего пакета. Если вы обнаружите, что таких членов слишком много, еще раз проверьте модель вашей системы и попытайтесь найти другой вариант разбиения на классы, при котором они были бы лучше изолированы друг от друга. Как было сказано, и закрытый член, и член, доступный только в пределах пакета, являются частью реализации класса и обычно не оказывают воздействия на его внешний API. Однако они могут "просочиться" во внешний API, если класс реализует интерфейс Serializable (статьи 54 и 55).
Если уровень доступа для члена открытого класса меняется с доступного в пакете на защищенный, уровень доступности данного члена резко возрастает. Для этого класса защищенный член является частью внешнего API, а потому ему навсегда должна быть обеспечена поддержка. Более того, наличие защищенного члена в классе, передаваемом за пределы пакета, представляет собой открытую передачу деталей реализации (статья 15). Потребность в использовании защищенных членов должна возникать сравнительно редко.
Существует одно правило, ограничивающее ваши возможности по уменьшению доступности методов. Если какой-либо метод переопределяет метод суп ер класса, то методу в подклассе не разрешается иметь более низкий уровень доступа, чем был у метода в суперклассе [JLS, 8.4.6.3]. Это необходимо для гарантии того, что экземпляр подкласса можно будет использовать повсюду, где можно было использовать экземпляр суперкласса. Если вы нарушите это правило, то когда попытаетесь скомпилировать этот подкласс, компилятор сгенерирует сообщение об ошибке. Частный случай правила: если класс реализует некий интерфейс, то все методы класса, представленные в этом интерфейсе, должны быть объявлены как открытые (public). Это объясняется тем, что в интерфейсе все методы неявно подразумеваются открытыми.
Открытые поля (в отличие от открытых методов) в открытых классах должны быть редким явлением (если вообще должны ПОЯВЛЯТЬСЯ). Если поле не имеет модификатора final или имеет модификатор и ссылается на изменяемый объект, то, делая его открытым, вы упускаете возможность наложения ограничений на значения,
которые могут быть записаны в этом поле. Вы также лишаетесь возможности предпринимать какие-либо действия в ответ на изменение этого поля. Отсюда простой вывод: классы с открытыми изменяемыми полями небезопасны в системе с несколькими потоками (not thread-safe). Даже если поле имеет модификатор final и не ссылается на изменяемый объект, объявляя его открытым, вы отказываетесь от возможности гибкого перехода на новое представление внутренних данных, в котором это поле будет отсутствовать.
Из правила, запрещающего открытым классам иметь открытые поля, есть одно исключение. С помощью полей public static final классы могут предоставлять вовне константы. Согласно договоренности, названия таких полей состоят из прописных букв, слова в названии разделяются символом подчеркивания (статья 38). Крайне важно, чтобы эти поля содержали либо простые значения, либо ссылки на неизменяемые объекты (статья 13). Поле с модификатором final, содержащее ссылку на изменяемый объект, обладает всеми недостатками поля без модификатора final: хотя саму ссылку изменить нельзя, объект, на который она указывает, может быть изменен - с роковыми последствиями.
Заметим, что массив ненулевой длины всегда является изменяемым. Поэтому практически никогда нельзя декларировать поле массива как public static final. Если в классе будет такое поле, клиенты получат возможность менять содержимое этого массива. Часто это является причиной появления дыр в системе безопасности.
// Потенциальная дыра в системе безопасности
public static final Туре[] VALUES = { ... };
Открытый массив следует заменить закрытым массивом и открытым неизменяемым списком:
private static final Туре[] PRIVATE_VALUES = { ... };
public static final List VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
Другой способ: если на этапе компиляции вам необходима проверка типов и вы готовы пожертвовать производительностью, то можете заменить открытое поле массива открытым методом, который возвращает копию закрытого массива.
private static final Туре[] PRIVATE_VALUES = { ... };
private static final Туре[] values() {
return ( Туре[] ) PRIVATE_VALUES.clone();
}
Подведем итоги. Всегда следует снижать уровень доступа, насколько это возможно. Тщательно разработав наименьший открытый API, вы должны не дать возможности каким-либо случайным классам, интерфейсам и членам стать частью этого API. За исключением полей типа public static final, других открытых полей в открытом классе быть не должно. Убедитесь в том, что объекты, на которые есть ссылки в полях типа public static final, не являются изменяемыми.