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

Джош Блох

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

Глава 6 Перечислимые типы и аннотации

Throwable ехс = wrappedExc.getCause(); Class<? extends Exception>[] excTypes = m.getAnnotation(ExceptionTest.class).value(); int oldPassed = passed;

for (Class<? extends Exception> excType : excTypes) { if (excType.islnstance(exc)) {

passed++;

break;

}

}

if (passed == oldPassed)

System.out.printf("Test %s failed: %s %n”, m, exc);

}

}

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

Как уже говорилось, за исключением системных программи­ стов большей части программистов не потребуется определять типы аннотаций. Все программисты должны, тем не менее, использовать предопределенные типы аннотаций, которыми их обеспечивает платформа Java (статьи 36 и 24). Также рассмотрите использова­ ние любых аннотаций, предоставляемых ID E или инструментами статического анализа. Такие аннотации могут улучшить качество диагностической информации, предоставляемой этими инструмен­ тами. Обратите внимание, чтобы эти аннотации были стандарти­ зированы, так что вам придется проделать некоторую работу, если вы захотите сменить инструменты или если стандарт, наконец, по­ явится.

240

Big ram

С тать я 36

Используйте аннотацию Override последовательно

Когда аннотации добавились в версии 1.5, несколько типов ан­ нотаций добавилось к библиотекам [JLS, 9.6.1]. Для обычного про­ граммиста самым важным из них является Override. Эта аннотация может использоваться только при декларировании методов, и она может определить, что декларация аннотируемого метода переопре­ деляет декларацию в супертипе. Если вы последовательно будете ис­ пользовать данную аннотацию, то она защитит вас от большого коли­ чества ужасных ошибок. Рассмотрим эту программу, в которой класс

представляет биграмму, или упорядоченную пару символов:

// Можете ли определить, где ошибка? public class Bigram {

private final char first; private final char second;

public Bigram(char first, char second) { this.first = first;

this.second = second;

public boolean equals(Bigram b) {

return b.first == first && b.second == second;

public int hashCode() {

return 31 * first + second;

public static void main(String[] args) { Set<Bigram> s = new HashSet<Bigram>(); for (int i = 0; i < 10; i++)

for (char ch = ‘a’; ch <= ‘z’; ch++) s.add(new Bigram(ch, ch)); System.out.println(s.size());

241

Bigram, equals
Object.equals,

Глава 6 Перечислимые типы и аннотации

Основная программа постоянно добавляет к набору 26 биграмм, каждая из которых состоит из двух идентичных символов нижнего регистра. Затем она выводит размер набора. Вы ожидаете, что про­ грамма напечатает 26, так как набор не может дублироваться. Если вы попытаетесь запустить программу, то увидите, что она распечата­ ет не 26, а 260. Что не так?

Вполне понятно, что автор класса Bigram намеревался переопре­ делить метод equals (статья 8) и даже не забыл переопределить метод hashCode (статья 9). К сожалению, наш неудачливый программист не пе­ реопределил equals, тем самым перегрузив его (статья 41). Для переопре­ деления Object.equals вы должны определить метод equals, параметры которого принадлежат типу Object, но параметры метода equals класса Bigram не принадлежат типу Object, следовательно, Bigram наследует метод equals из Obj ect. Этот метод проверяет идентичность объекта, как и оператор ==. Каждая из десяти копий каждой биграммы отличается от девяти других, так что они не будут равны согласно который объясняет, почему программа пишет 260.

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

@0verride public boolean equals(Bigram b) { return b.first == first && b.second == second;

}

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

Bigram.java:10: method does not override or implement a method from a supertype

(©Override public boolean equals(Bigram b) {

A

Вы сразу же поймете, что сделали неправильно, стукните себя по лбу и замените испорченную реализацию equals правильной (статья 8):

242

С татья 36

@Override public boolean equals(Object o) { if (!(o instanceof Bigram))

return false; Bigram b = (Bigram) o;

return b.first == first && b.second == second;

}

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

Современные ID E дают нам еще одну причину, почему необ­ ходимо последовательно использовать аннотации Override. У таких IDE есть автоматические проверки, известные как инспекции кода. Если вы примените соответствующую инспекцию кода, то ID E вы­ даст предупреждение, если у вас есть метод, который не содержит аннотации Override, но на самом деле переопределяет метод суперк­ ласса. Если вы используете аннотацию Override последовательно, то эти сообщения предупредят вас о ненамеренном переопределении. Эти сообщения дополняют сообщения об ошибках от компилятора, который предупредит о том, что переопределение не удалось. Между IDE и компилятором вы можете быть уверены, что вы переопределя­ ете методы там, где вы хотите, и нигде более.

Если вы используете версию 1.6 или более позднюю, аннотация Override даст вам еще больше помощи в поиске ошибок. В версии 1.6 стало разрешено использование аннотации Override на деклараци­ ях методов, которые переопределяют декларации из интерфейсов

243

Глава 6 Перечислимые типы и аннотации

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

В абстрактном классе или интерфейсе, тем не менее, лучше ан­ нотировать все методы, которые должны переопределять методы суперкласса или методы суперинтерфейса, вне зависимости от того, конкретные они или абстрактные. Например, интерфейс Set не до­ бавляет новые методы к интерфейсу Collection, поэтому он должен содержать аннотацию Override на всех своих декларациях методов, чтобы гарантировать, что он случайно не добавит новые методы к ин­

терфейсу Collection.

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

Используйте маркерные интерфейсы для определения типов

Маркерный интерфейс — это такой интерфейс, который не содержит деклараций методов, но просто определяет (или «маркирует») класс, который реализует интерфейс как имеющий определенное свойство. Например, рассмотрим интерфейс Serial­ izable (глава 11). Реализуя этот интерфейс, класс сообщает, что его экземпляры могут быть приписаны к ObjectyOutputSt ream (или «сериализованы » ).

244

Object-

С тать я 37

Возможно, вы слышали, что маркерные аннотации (статья 35) делают маркерные интерфейсы устаревшими. Это неверное утверж­ дение. Маркерные интерфейсы обладают двумя преимуществами над маркерными аннотациями. Первое и основное: маркерные ин­ терфейсы определяют тип, который реализуется экземплярами маркированного класса, а маркерные аннотации — нет. Суще­ ствование этого типа позволяет вам фиксировать ошибки во время компиляции, которые вы не можете заметить до выполнения в случае использования маркерных аннотаций.

В случае с маркерным интерфейсом Serializable метод

OutputSt ream, write (Object) не будет

работать, если

его аргу­

мент не реализует интерфейс. Неизвестно, почему автор API

ObjectOut putSt ream не использовал

преимущества

интерфейса

Serializable при объявлении метода write. Тип аргумента мето­ да должен был быть Serializable, а не Object. Попытка вызова

ObjectOut putSt ream, write на объекте, который не реализует Serializ­

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

ся. Если тип аннотации объявлен с указанием цели ElementType.TYPE, то он может применяться к любому классу или интерфейсу. Предпо­ ложим, у вас есть маркер, который применим только к реализаци­ ям определенного интерфейса. Если вы определите его как маркер­ ный интерфейс, то вы сможете расширить единственный интерфейс, к которому он применим, гарантируя, что все маркированные типы являются также подтипами единственного интерфейса, к которому он применим.

Есть спорное утверждение, что интерфейс Set всего лишь огра­ ниченный маркерный интерфейс. Это применимо только к подтипам Collection, но это не добавляет никаких методов, кроме определен­ ных в Collection. Он обычно не рассматривается как маркерный интерфейс, потому что он уточняет соглашения нескольких методов

Collection, которые включают add, equals и hashCode. Но легко пред­

245

Глава 6 • Перечислимые типы и аннотации

ставить маркерный интерфейс, который применим только к подти­ пам нескольких определенных интерфейсов и не уточняет соглашений каких-либо методов интерфейса. Такой маркерный интерфейс мо­ жет описать некоторые инварианты всего объекта или отразить, что экземпляры подходят для обработки методом какого-либо другого класса (таким же образом, как и интерфейс Serializable отражает, что экземпляры подходят для обработки ObjectOutputStream).

Главным преимуществом маркерных аннотаций над маркерными интерфейсами является возможность добавить больше информации к типу аннотации после того, как она уже используется путем добав­ ления одного или более элементов типа аннотации к уже имеющимся по умолчанию [JLS, 9.6], что дает начало простым типам маркерных аннотаций и может развиться в большой тип аннотации со временем. Такая эволюция невозможна с маркерными интерфейсами, поскольку в принципе невозможно добавить метод к интерфейсу после того, как он уже реализован (статья 18).

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

Итак, когда же мы должны использовать маркерную аннотацию,

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

246

С татья 37

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

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

Внекотором роде, эта статья — противоположность статьи 19,

вкоторой говорится: «Если вы не хотите определять тип, не исполь­ зуйте интерфейс». При первом приближении эта статья говорит: «Если вы действительно хотите определить тип, используйте интер­ фейс».

247

Г л а в а

п т ш т т 1 э д м ж м ш т ш д ш ш д ш ^ ^ « ^ ^

Методы

шшданной главе рассматривается несколько аспектов проектирования методов: как обрабатывать параметры и возвращаемые методом зна­ чения, как проектировать сигнатуры методов и как документировать методы. Значительная часть материала относится как к методам, так и к конструкторам. Как и в главе 5, особое внимание здесь уделяется удобству, устойчивости и гибкости программ.

Проверяйте достоверность параметров

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

248

С тать я 38

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

В открытых методах для описания исключений, которые будут инициироваться, когда значения параметров нарушают ограничения, используйте тег @t hrows генератора документации Javadoc (статья 62). Как правило, это будет исключение IllegalArgunentException,

IndexOutOfBoundsException или NullPointerException (статья 60).

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

J -к -к

*Возвращает объект Biglnteger, значением которого является модуль данного

*числа по основанию т. Этот метод отличается от метода remainder тем,

* что всегда

возвращает

неотрицательное значение Biglnteger.

к

 

 

* @рагат

т - модуль,

должен быть положительным числом

* @return

this mod

m

* @throws

ArithmeticException, if m <= 0

*/

public Biglnteger mod(BigInteger m) { if (m.signumO <= 0)

throw new ArithmeticException(“Modulus not positive”);

... // Вычисления

}

249

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