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

Джош Блох

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

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

Последний пункт соглашений для сотрагеТо, являющийся скорее сильным предположением, чем настоящим условием, постулирует, что проверка равенства, осуществляемая с помощью метода сотрагеТо, обычно должна давать те же самые результаты, что и метод equals. Если это условие выполняется, считается, что упорядочение, задавае­ мое методом сотрагеТо, согласуется с проверкой равенства (consistent with equals). Если же оно нарушается, то упорядочение называется не согласующимся с проверкой равенства (inconsistent with equals). Класс, чей метод сотрагеТо устанавливает порядок, не согласующийся с условием равенства, будет работоспособен, однако отсортированные коллекции, содержащие элементы этого класса, могут не соответство­ вать общим соглашениям для соответствующих интерфейсов коллек­ ций (Collection, Set или Мар). Дело в том, что общие соглашения для этих интерфейсов определяются в терминах метода equals, тогда как в отсортированных коллекциях используется проверка равенства, ко­ торая реализуется методом сотрагеТо, а не equals. Если это произой­ дет, катастрофы не случится, но иногда это следует осознавать.

Например, рассмотрим класс Big Decimal, чей метод сотрагеТо не со­ гласуется с проверкой равенства. Если вы создадите HashSet и добави­ те в него новую запись BigDecimal(«1.0»), а затем BigDecimal(«1.00»), то этот набор будет содержать два элемента, поскольку два добавлен­ ных в этот набор экземпляра класса BigDecimal не будут равны, если их сравнивать с помощью метода equals. Однако, если вы выполняете ту же самую процедуру с Т reeSet, а не HashSet, полученный набор будет содержать только один элемент, поскольку два представленных экзем­ пляра BigDecimal будут равны, если их сравнивать с помощью метода сотаргеТо. (Подробнее см. документацию на BigDecimal.)

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

90

CaselnsensitiveSt ring

С татья 12

исключительную ситуацию NullPointerException. В точности то же самое вы получите, если просто приведете аргумент к правильному типу, а затем попытаетесь обратиться к его членам.

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

из статьи 8:

public final class CaselnsensitiveString implements Comparable<CaseInsensitiveString> {

public int compareTo(CaseInsensitiveString cis) {

return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);

}

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

}

Обратите внимание, что класс CaselnsensitiveString реализует

интерфейс Comparable<CaseInsensitiveSt ring>. Это значит, что ссыл­

ка на Caselnsensitive String может сравниваться только с другими ссылками на Caselnsensitive String. Это нормальный пример, кото­ рому нужно следовать, объявляя классы для реализации интерфейса Comparable. Обратите вниманиие, что параметром метода CompareTo является CaselnsensitiveString, а не Object. Это необходимо для ра­ нее упомянутого декларирования классов.

Поля простого типа нужно сравнивать с помощью операторов. Для сравнения полей с плавающей точкой используйте Double. Com­ pare или Float.Compare вместо операторов соотношения, которые не соблюдают соглашения для метода compareTo, если их применять для значений с плавающей точкой. Для сравнения массивов исполь­ зуйте данные инструкции для каждого элемента.

91

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

Если у класса есть несколько значимых полей, порядок их срав­ нения критически важен. Вы должны начать с самого значимого поля и затем следовать в порядке убывания значимости. Если сравнение дает что-либо помимо нуля (который означает равенство), все, что вам нужно сделать, — просто возвратить этот результат. Если самые значимые поля равны, продолжайте сравнивать следующие по зна­ чимости поля и т.д. Если все поля равны, равны и объекты, а потому возвращайте нуль. Такой прием демонстрирует метод согпрагеТо для класса PhoneNumber из статьи 9:

public int compareTo(PhoneNumber pn) { // Сравниваем коды зон

if (areaCode < pn.areaCode) return -1;

if (areaCode > pn.areaCode) return 1;

//Коды зон равны, сравниваем префиксы if (prefix < pn.prefix)

return -1;

if (prefix > pn.prefix) return 1;

//Коды зон и префиксы равны, сравниваем номера линий if (lineNumber< pn.lineNumber)

return -1;

if (lineNumber > pn.lineNumber) return 1;

return 0; // Все поля равны

}

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

92

Integer. MAX_VALUE

С татья 12

public int compareTo(PhoneNumber pn) { // Сравниваем коды зон

int areaCodeDiff = areaCode - pn.areaCode; if (areaCodeDiff != 0)

return areaCodeDiff;

// Коды зон равны, сравниваем префиксы int prefixDiff = prefix - pn.prefix; if (prefixDiff != 0)

return prefixDiff;

// Коды зон и номера АТС равны, сравниваем номера линий return lineNumber - pn.lineNumber;

}

Такая уловка работает прекрасно, но применять ее следует крайне осторожно. Не пользуйтесь ею, если у вас нет уверенности, что рас­ сматриваемое поле не может иметь отрицательное значение, или, что бывает еще чаще, разность между наименьшим и наибольшим возмож­ ными значениями поля меньше или равна значению (231 -1). Причина, по которой этот прием не всегда работает, обычно

заключается в том, что 32-битное целое число со знаком является не­ достаточно большим, чтобы показать разность двух 32-битных целых чисел с произвольным знаком. Если i — большое положительное це­ лое число, a j — большое отрицательное целое число, то при вычис­ лении разности (i - j) произойдет переполнение и будет возвращено отрицательное значение. Соответственно, полученный нами метод compareTo работать не будет: для некоторых аргументов будет воз­ вращен бессмысленный результат, тем самым будут нарушены первое и второе условия соглашения для метода compareTo. И эта проблема не является чисто теоретической, она уже вызывала сбои в реальных системах. Выявить причину подобных отказов может быть трудно, по­ скольку неправильный метод compareTo со многими входными значени­ ями работает правильно.

93

Классы и интерфейсы

И ^ассы и интерфейсы занимают в языке программирования Java центральное положение. Они являются основными элементами аб­ стракции. Язык Java содержит множество мощных элементов, ко­ торые можно использовать при построении классов и интерфейсов. В данной главе даются рекомендации, которые помогут вам наилуч­ шим образом использовать эти элементы, чтобы ваши классы и ин­ терфейсы были удобными, надежными и гибкими.

#

Сводите к минимуму доступность классов и членов

Единственный чрезвычайно важный фактор, отличающий хорошо спроектированный модуль от неудачного, — степень сокрытия от дру­ гих модулей его внутренних данных и других деталей реализации. Х о ­ рошо спроектированный модуль скрывает все детали реализации, четко разделяя свой API и его реализацию. Модули взаимодействуют друг с другом только через свои API, и ни один из них не знает, какая обра­ ботка происходит внутри другого модуля. Представленная концепция, называемая сокрытием информации (information hiding) или инкапсу­ ляцией (encapsulation), представляет собой один из фундаментальных принципов разработки программного обеспечения [Parnas72].

94

С татья 13

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

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

вцелом система не будет пользоваться успехом.

Язык программирования Java имеет множество возможностей для сокрытия информации. Одна из таких функций — механизм управления доступом (access control) [JLS, 6.6], задающий степень доступности (accessibility) для интерфейсов, классов и членов клас­ сов. Доступность любой сущности определяется тем, в каком месте она была декларирована и какие модификаторы доступа, если тако­ вые есть, присутствуют в ее декларации (private, protected или public). Правильное использование этих модификаторов имеет большое зна­ чение для сокрытия информации.

Главное правило заключается в том, что вы должны сделать каждый класс или член максимально недоступным, насколько

95

Глава 4 Классы и интерфейсы

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

Для классов и интерфейсов верхнего уровня (не являющихся вло­ женными) существуют лишь два возможных уровня доступа: доступ­ ный только в пределах пакета (package-private) и открытый (public). Если вы объявляете класс или интерфейс верхнего уровня с модифи­ катором public, он будет открытым, в противном случае он будет до­ ступен только в пределах пакета. Если класс или интерфейс верхнего уровня можно сделать доступным только в пакете, он должен стать таковым. При этом класс или интерфейс становится частью реализа­ ции этого пакета, а не частью его внешнего API. Вы можете модифи­ цировать его, заменить или исключить из пакета, не опасаясь нанести вред имеющимся клиентам. Если же вы делаете класс или интерфейс открытым, на вас возлагается обязанность всегда его поддерживать во имя сохранения совместимости.

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

Для членов класса (полей, методов, вложенных классов и вло­ женных интерфейсов) существует четыре возможных уровня досту­ па, которые перечислены здесь в порядке увеличения доступности:

закрытый (private) — данный член доступен лишь в преде­ лах того класса верхнего уровня, где он был объявлен.

доступный лишь в пределах пакета (package-private) — член доступен из любого класса в пределах того пакета, где он был объявлен. Формально этот уровень называется доступом

96

С татья 13

по умолчанию (default access), и именно этот уровень доступа вы получаете, если не было указано модификаторов доступа.

защищенный (protected) — член доступен для подклассов того класса, где этот член был объявлен (с небольшими огра­ ничениями [JLS, 6.6.2]) , доступ к члену есть из любого клас­ са в пакете, где этот член был объявлен.

открытый (public) — член доступен отовсюду.

После того как для вашего класса был тщательно спроектирован открытый A PI, вам следует сделать все остальные члены класса за ­ крытыми. И только если другому классу из того же пакета действи­ тельно необходим доступ к такому члену, вы можете убрать модифи­ катор private и сделать этот член доступным в пределах всего пакета. Если вы обнаружите, что таких членов слишком много, еще раз про­ верьте модель вашей системы и попытайтесь найти другой вариант разбиения на классы, при котором они были бы лучше изолированы друг от друга. Как было сказано, и закрытый член, и член, доступ­ ный только в пределах пакета, являются частью реализации класса и обычно не оказывают воздействия на его внешний API. Однако, тем не менее, они могут «просочиться» во внешний API, если этот класс реализует интерфейс Serializable (статьи 74 и 75).

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

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

97

Глава 4 Классы и интерфейсы

се [JLS, 8.4.8.3]. Это необходимо для того, чтобы гарантировать, что экземпляр подкласса можно будет использовать повсюду, где можно было использовать экземпляр суперкласса. Если вы нару­ шите это правило, то, когда вы попытаетесь скомпилировать этот подкласс, компилятор будет генерировать сообщение об ошибке. Частный случай этого правила: если класс реализует некий интер­ фейс, то все методы этого класса, представленные в этом интерфейсе, должны быть объявлены как открытые (public). Это объясняется тем, что в интерфейсе все методы неявно подразумеваются откры­ тыми [JLS 9.1.5].

Открытые поля (в отличие от открытых методов) в открытых классах должны появляться редко (если вообще должны появлять­ ся). Если поле не имеет модификатора final или имеет модификатор и ссылается на изменяемый объект, то, делая его открытым, вы упу­ скаете возможность наложить ограничение на значения, которые могут быть записаны в это поле. Вы также упускаете возможность предпринимать какие-либо действия в ответ на изменение этого поля. Отсюда простой вывод: классы с открытыми изменяемыми полями небезопасны в системе с несколькими потоками (not thread-safe). Даже если поле имеет модификатор final и не ссылается на изменя­ емый объект, объявляя его открытым, вы отказываетесь от возмож­ ности гибкого перехода на новое представление внутренних данных, в котором это поле будет отсутствовать.

То же самое правило относится и к статическим полям, за одним исключением. С помощью полей public static final классы могут выставлять наружу константы, подразумевая, что константы обра­ зуют целую часть абстракции, предоставленной классом. Согласно договоренности, названия таких полей состоят из прописных букв, слова в названии разделены символом подчеркивания (статья 56). Крайне важно, чтобы эти поля содержали либо простые значения, либо ссылки на неизменяемые объекты (статья 15). Поле с модифи­ катором final, содержащее ссылку на изменяемый объект, обладает всеми недостатками поля без модификатора final: хотя саму ссылку

98

С татья 13

нельзя изменить, объект, на который она указывает, может быть из­ менен — с нежелательными последствиями.

Заметим, что массив ненулевой длины всегда является изменя­

емым. Поэтому п р а к т и ч е с к и никогда нельзя д е к л а р и р о в а т ь поле м а с с и в а к а к public static final. Если в классе будет такое поле,

клиенты получат возможность менять содержимое этого массива. Часто это является причиной появления дыр в системе безопасности.

// Потенциальная дыра в системе безопасности!

public static final Thing[] VALUES = { ... } ;

Имейте в виду, что многие среды разработки (ID E) генерируют методы доступа, возвращающие ссылки на закрытые поля массивов, приводят как раз именно к такой ошибке. Есть два способа решения проблемы. Вы можете заменить открытый массив закрытым масси­ вом и открытым неизменяемым списком:

private static final Thing[] PRIVATE_VALUES = { ... } ;

public static final List<Thing> VALUES =

Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

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

private static final Thing[] PRIVATE_VALUES = { ... } ;

private static final Thing[] valuesO {

return PRIVATE_VALUES.clone();

}

Подведем итоги. Всегда следует снижать уровень доступа, на­ сколько это возможно. Тщательно разработав наименьший открытый API, вы должны не дать возможность каким-либо случайным клас­ сам, интерфейсам и членам стать частью этого API. З а исключением полей типа public static final, других открытых полей в открытом классе быть не должно. Убедитесь в том, что объекты, на которые есть ссылки в полях типа public static final, не являются изменяемыми.

99

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