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

Джош Блох

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

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

объявлять абстрактный метод apply в перечислимом типе, как вы бы делали это в нерасширяемом перечислимом типе с релизацией мето­ да, специфичного для экземпляра. Это из-за того, что абстрактный метод (apply) является членом интерфейса (Operation).

Есть возможность не только передать один экземпляр расши­ ряемого перечислимого типа везде, где ожидается основной перечис­ лимый тип, — можно передать все расширение перечислимого типа и использовать его элементы в дополнение или вместо элементов ос­ новного типа. Например, вот версия программы, которая выполняет все операции расширения, определенные выше:

public static void main(String[] args) { double x = Double.parseDouble(args[0]); double у = Double.parseDouble(args[1]); test(ExtendedOperation.class, x, y);

}

private static <T extends Enum<T> & Operation> void test(Class<T>opSet, double x, double y) {

for (Operation op : opSet.getEnumConstants()) System.out.printf("%f %s %f = %f%n”,

x, op, y, op.apply(x, y));

}

Обратите внимание, что класс литерал для расширяемого опера­ ционного типа (ExtendedOperation. class) передается от main в test для описания набора расширяемых операций. Класс литерал служит в ка­ честве маркера связанного типа (статья 29). Довольно сложная де­

кларация параметра opSet (<Т extends Enum<T> & Operation>Class<T>)

гарантирует, что объект Class представляет и перечислимый тип и подтип, принадлежащий к Operation, который является в точности тем, что требуется для итерации элементов и выполнения операций, связанных с каждым из них.

Второй альтернативой будет использование Collections extends Operation^ который является связанным типом группового символа (статья 28), в качестве типа для параметра opSet:

230

С тать я 34

public static void main(String[] args) { double x = Double.parseDouble(args[0]); double у = Double.parseDouble(args[1]);

test(Arrays.asList(ExtendedOperation.valuesO), x, y);

}

private static void test(Collection<? extends Operation> opSet, double x, double y) {

for (Operation op : opSet) System.out.printf(“%f %s %f = %f%n”, x, op, y, op.apply(x, y));

}

Получившийся код менее сложен, и метод test более гибкий: он позволяет вызывающему объединить операции из нескольких ти­ пов реализации. С другой стороны, вы откажетесь от использования EnumSet (статья 32) и EnumMap (статья 33) для определенной опера­ ции, так что вам лучше воспользоваться маркером связанного типа, если только вам не требуется гибкость для объединения операций в нескольких типах реализации.

Обе вышеупомянутые программы произведут этот результат, если их запускать с аргументами командной строки 4 и 2:

4.000000 ~ 2.000000 = 16.000000

4.000000 % 2.000000 = 0.000000

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

231

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

Подведем итог. Хотя вы не можете написать расширяемый перечислимый тип, вы можете имитировать его, написав интер­ фейс с основным перечислимым типом, который реализует ин­ терфейс. Это позволяет клиентам писать свои собственные перечис­ лимые типы, реализующие интерфейс. Эти перечислимые типа могут потом использоваться везде, где основной перечислимый тип может быть использован при условии, что API написаны на условиях ин­ терфейса.

Предпочитайте аннотации шаблонам присвоения имен

До версии 1.5 было общепринятым использование именования ша­ блонов (naming pattern) для определения, что некоторые элементы про­ граммы требуют специального подхода инструментом или структурой. Например, среда тестирования JUnit изначально требовала от поль­ зователей назначать методы тестирования, начиная их наименования со слова test [Веск04]. Этот прием работает, но у него есть несколько недостатков. Во-первых, типографические ошибки могут привести к не­ видимым ошибкам. Например, предположим, что вы случайно назва­

ли метод тестирования tsetSafetyOverride вместо testSafetyOverride.

JUnit не сообщит вам об ошибке, но и не будет выполнять тестирова­ ние, что приведет к ложному чувству безопасности.

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

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

232

С татья 35

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

Аннотации [JLS 9.7] решают все эти проблемы. Предположим, вы хотите определить тип аннотации для назначения простых тестов, которые запускаются автоматически и завершаются с ошибкой, если появляется исключение. Вот как такой аннотационный тип с назва­ нием Test выглядит:

// Marker annotation type declaration import java.lang.annotation.*;

j it ir

* Indicates that the annotated method is a test method. * Use only on parameterless static methods.

*/

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD) public @interface Test {

}

Декларация для типа аннотации Test сама по себе является анно­ тированной аннотациями Retention и Target. Такие аннотации на де­ кларации типа аннотации называются мета-аннотации. Мета-анно­

тация @Retention(RetentionPolicy.RUNTIME) означает, что аннотация

Test будет задержана при выполнении. Без этого аннотация Test была бы невидима инструменту тестирования. Мета-аннотация @Тагget(ElementType. METHOD) обозначает, что аннотация Test разрешена только на декларациях методов: она не может применяться при объ­ явлении классов, полей и других элементов.

233

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

Обратите внимание на комментарий при декларации аннотации Test, который говорит «Используйте только статические методы без параметров». Было бы хорошо, если бы компилятор мог наложить это ограничение, но он не может. Есть определенные ограничения на количество проверок на ошибки, которые компилятор может вы­ полнить для вас даже с аннотациями. Если вы поместите аннотацию Test на декларацию метода экземпляра или метода с одним или бо­ лее параметров, программа тестирования все равно откомпилируется, предоставив инструменту тестирования разбираться с проблемами при выполнении.

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

// Программа, содержащая аннотации с маркерами public class Sample {

@Test public static void m1() { } // Test should pass public static void m2() { }

@Test public static void m3() { // Test should fail throw new RuntimeException(“Boom”);

}

public static void m4() { }

@Test

public void

m5() { } // INVALID USE: nonstatic method

public static void m6() { }

 

@Test

public static void m7() { // Test should

fail

throw new RuntimeException(“Crash”);

 

}

 

 

 

public

static void m8() { }

 

}

 

 

 

У класса Sample

имеется семь статических

методов, четыре

из которых аннотируются как наборы. Два из них, m3 и ш7, выводят исключения, и два, п1 и т5, не выводят. Но один из аннотируемых

234

С тать я 35

методов, который не выводит исключения, т5, является экземпляром метода, поэтому нельзя использовать аннотацию в данном случае. В итоге Sample содержит четыре теста: один будет успешным, другой нет, и один не является разрешенным. Эти четыре метода, которые не аннотируются аннотацией Test, будут проигнорированы инстру­ ментом тестирования.

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

// Программа для обработки аннотаций с маркерами import java.lang.reflect.*;

public class RunTests {

public static void main(String[] args) throws Exception { int tests = 0;

int passed = 0;

Class testClass = Class.forName(args[0]);

for (Method m : testClass.getDeclaredMethods()) { if (m.isAnnotationPresent(Test.class)) {

tests++; try {

m.invoke(null);

passed++;

}

catch (InvocationTargetException wrappedExc) { Throwable exc = wrappedExc.getCause(); System.out.println(m + “ failed: “ + exc);

} catch (Exception exc) { System.out.println(“INVALID @Test: " + m);

}

}

}

System,out.printf("Passed: %d, Failed: %d%n”,

235

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

passed, tests - passed);

}

}

Этот инструмент берет полноценное имя класса в командную строку и запускает все аннотируемые методом Test методы класса

рефлективно путем вызова Method, invoke. Метод isAnnotationPresent

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

из InvocationTargetEsception с помощью метода getCause.

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

на Sample запускается RunTests:

public static void Sample.m3() failed: RuntimeException: Boom INVALID @Test: public void Sample.m5()

public static void Sample.m7() failed: RuntimeException: Crash Passed: 1, Failed: 3

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

// Annotation type with a parameter import java,lang.annotation.*;

I * -k

 

 

* Indicates that

the annotated method

is a test method that

* must throw the

designated exception

to succeed.

236

С тать я 35

*/

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType,METHOD) public @interface ExceptionTest {

Class<? extends Exception> value();

}

Тип параметра для этой аннотации — Class<? Extends Exception>. Этот тип символа подстановки довольно громоздкий. Он оз­ начает «объект Class некоего класса, расширяющий Exception» и позволяет пользователю аннотации задать любой тип исключения. Такое использование является примером маркера связанного типа (статья 29). Вот как это выглядит на практике. Обратите внимание, что литералы класса используются здесь как значения параметров аннотации:

// Программа, содержащая аннотацию с параметром public class Sample2 {

@ExceptionTest(ArithmeticException. class) public static void m1() { // Test should pass

int i = 0; i = i / i;

}

@ExceptionTest(ArithmeticException.class)

public static void m2() { // Should fail (wrong exception) int[] a = new int[0];

int i = a[1];

}

@ExceptionTest(ArithmeticException.class)

public static void m3() { } // Should fail (no exception)

}

Теперь давайте изменим инструмент, запускающий тест, чтобы обработать новую аннотацию. Это действие состоит из добавления следующего кода к методу main:

if (m.isAnnotationPresent(ExceptionTest.class)) { tests++;

237

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

try {

m.invoke(null);

System.out.printf(“Test %s failed: no exception%n”, m); } catch (InvocationTargetException wrappedEx) {

Throwable exc = wrappedEx.getCause();

Class<? extends Exception> excType =

m . getAnnotation(ExceptionTest .c l a s s ) .v a l u e ( );

if (excType. islnstance(exc)) {

passed++;

} else { System.out.printf(

“Test %s failed: expected %s, got %s%n”, m, excType.getName(), exc);

}

} catch (Exception exc) { System.out.println(“INVALID @Test: “ + m);

}

}

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

ет исключение TypeNotPresentException.

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

С т а т ь я 3.

пользования. Предположим, что мы меняем тип параметра аннота ции ExceptionTest так, чтобы он был массивом объекта Class:

// Тип аннотации с параметрами массива

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface ExceptionTest {

Class<? extends Exception>[] value();

}

Синтаксис для массива параметров в аннотации довольно гибш< Он оптимизирован для одноэлементных массивов. Все предыдущи аннотации ExceptionTest все еще действительны в новой версии Ех ceptionTest с параметрами массива, и в результате получается од ноэлементный массив. Для определения многоэлементного массив необходимо окружить элементы фигурными скобками и отделит их запятыми:

// Код, содержащий аннотацию с параметрами массива

@ExceptionTest({ IndexOutOfBoundsException.class,

NullPointerException.class })

public static void doublyBadO {

List<String> list = new Arrayl_ist<String>();

//The spec permits this method to throw either

//IndexOutOfBoundsException or NullPointerException list.addAll(5, null);

}

Разумно изменить программу, запускающую тест для обработ ки новой версии ExceptionTest. Этот код заменяет оригинальну версию:

if (m.isAnnotationPresent(ExceptionTest.class)) { tests++;

try {

m.invoke(null);

System.out.printf(“Test %s failed: no exception%n”, m); } catch (Throwable wrappedExc) {

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