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

Джош Блох

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

Глава 5 • Средства обобщенного программирования (Generics)

в коллекцию в приложении, в котором смешаны обобщенный и уже существующий код.

Второе ограничение класса Favo rites заключается в том, что он не может использоваться в нематериальных типах (статья 25). Другими словами, вы можете хранить избранное String или String[], но толь­ ко не List<String>. Если вы попытаетесь сохранить List<String>, программа не будет компилироваться. Причина этого заключается

втом, что вы не можете получить объект Class для List<String>:

вList<String>.class, имеется синтаксическая ошибка, но, с другой

стороны, это и хорошо. List<String> и List<Integer> использу­ ют один общий объект Class, которым является List.class. Он бы натворил немало бед свойствами объекта Class, если бы «литералы

типа» List<String>. class и List<Integer>. class были допустимыми

и возвращали одну и ту же ссылку на объект.

Нет достаточно удовлетворительного обхода второго ограниче­ ния. Есть технология, называемая метка супертипа, которая длин­ ным путем обращается к этому ограничению, однако она сама имеет свои ограничения [Gafter07].

Метки типа, используемые Favorites, являются неограниченны­ ми: getFavorite и putFavorite принимают любой объект Class. Ино­ гда вам может потребоваться ограничить типы, которые могут пере­ даваться методу. Этого можно достичь ограниченной меткой типа, которая просто является меткой типа, устанавливающей ограничения на то, какие типы могут быть представлены, используя ограничен­ ный параметр типа (статья 27) или ограниченный групповой символ (статья 28).

Аннотации API (статья 35) расширенно используют ограничен­ ные метки типа. Например, вот метод для чтения аннотаций при вы­ полнении. Этот метод происходит от интерфейса AnnotatedElement, который реализуется рефлективным типом, представляющим клас­ сы, методы, поля и другие программные элементы:

public <Т extends Annotation

Т getAnnotation(Class<T> annotationType);

200

С татья 29

Аргумент annotationType является ограниченной меткой типа, представляющей тип аннотации. Метод возвращает элементы анно­ тации данного типа, если таковые имеются, или null, если нет. В сущ­ ности, аннотированные элементы являются безопасными неоднород­ ными контейнерами, ключи которых являются типами аннотации.

Предположим, что у вас есть объект типа Class<?> и вы хотите передать его методу, которому требуется ограниченная метка типа, та­ кая как getAnnotation. Вы можете передать объект в Class<? Extends Annotation^ но эта передача непроверенна, поэтому будет выведено предупреждение во время компиляции (статья 24). К счастью, класс Class дает нам метод экземпляра, который безопасно (и динамично) выполняет данную передачу. Метод называется asSubclass и переда­ ет объект Class, на котором он должен представлять подкласс класса, представленного его аргументами. Если передача удастся, то метод возвращает свои аргументы, если нет, то выводится ошибка ClassCastException.

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

// Use of asSubclass to safely cast to a bounded type token static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {

Class<?> annotationType = null; // Unbounded type token try {

annotationType = Class.forName(annotationTypeName); } catch (Exception ex) {

throw new IllegalArgumentException(ex);

}

return element.getAnnotation( annotationType.asSubclass(Annotation.class));

}

Подведем итог. Нормальное использование средств обобщен­ ного программирования, представленное коллекцией A PI, ограни­

201

Глава 5 • Средства обобщенного программирования (Generics)

чивает вас фиксированным количеством параметров на контейнер. Вы можете обойти это ограничение, поместив параметр в ключ вместо контейнера. Вы можете использовать объекты Class в качестве клю­ чей для таких безопасных неоднородных контейнеров. Объект Class, используемый таким образом, называется меткой типа. Вы можете также использовать обычный тип ключа. Например, вы могли бы за ­ ставить тип DatabaseRow представлять строку базу данных (контей­ нер) и обобщенный тип Column<T> в качестве его ключа.

202

 

 

/

I" 1

л а в a

о

1

s'

_____ J

Перечислимые типы

и аннотации

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

Используйте перечислимые типы вместо констант int

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

// Перечислимый шаблон int - с ужасными недостатками! public static final int APPLE_FUJI = 0;

203

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

public static final int public static final int public static final int public static final int public static final int

APPLE_PIPPIN = 1; APPLE_GRANNY_SMITH = 2; ORANGE_NAVEL = 0; 0RANGE_TEMPLE = 1; 0RANGE_BL00D = 2;

Технология, известная как перечислимый шаблон int, обладает многими недостатками. Она не обеспечивает безопасность типов и не очень удобна. Компилятор не будет жаловаться, если вы передадите яблоко методу, который ожидает апельсин, сравните яблоки с апель­ синами оператором == или хуже:

// Вкусный цитрусовый приправлен яблочным соусом! int i = (APPLE_FUJI - 0RANGE_TEMPLE) / APPLE_PIPPIN;

Обратите внимание, что название каждой константы яблока со­ держит префикс APPLE_ и что название каждой константы апельсина содержит 0RANGE_. Это потому, что Java не предоставляет простран­ ства имен для перечислимых групп int. Префиксы препятствуют со­ впадению имен, если две перечислимые группы int будут иметь кон­ станты с одинаковыми названиями.

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

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

Вы можете столкнуться с вариантом шаблона, в котором кон­ станты String используются вместо констант int. Этот вариант, из­ вестный как перечислимый шаблон String, желательно еще меньше

204

С т а т ь я 30

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

К счастью, в версии 1.5 язык дает нам альтернативу, которая по­ могает избежать недостатков перечислимых шаблонов int и string и предлагает ряд дополнительных преимуществ. Это перечислимые типы [JLS, 8.9]. Вот как они выглядят в простейшей форме:

public enum Apple { FUJI, PIPPIN, GRANNY_SMITH } public enum Orange { NAVEL, TEMPLE, BLOOD }

При поверхностном взгляде может показаться, что перечисли­ мые типы похожи на свои аналоги в других языках, таких как С, C + + , и C # , но это сходство обманчиво. Перечислимые типы Java — это полноценные классы, обладающие большими возможностями, чем их аналоги в других языках, где перечислимые типы являются по сути значениями int.

Основная идея перечислимых типов в Java проста: они являются классами, которые экспортируют один экземпляр каждой перечисли­ мой константы, используя открытое статическое завершенное поле. Перечислимые типы являются абсолютно завершенными благодаря тому, что у них нет конструкторов. Клиенты не могут ни создавать экземпляры перечислимых типов, ни расширять их, не создаются никакие другие экземпляры, кроме декларированных перечислимых констант. Другими словами, перечислимые типы подвергаются кон­ тролю создания экземпляров. Они являются обобщением синглтона (статья 3), являясь по сути одним перечислимым элементом. Для читателей, знакомых с первой редакцией этой книги, перечислимые типы предоставляют лингвистическую поддержку безопасных пере­ числимых шаблонов [BlochOl, статья 21].

205

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

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

Перечислимые типы с одинаковыми наименованиями констант сосуществуют «мирно» потому, что у каждого типа имеется свое про­ странство имен. Вы можете добавлять или менять порядок констант в перечислимом типе без необходимости повторно компилировать клиентов, потому что поля, экспортирующие константы, предостав­ ляют защитный слой между перечислимым типом и его клиентами: значения констант не компилируются в клиентов, как в случае с пе­ речислимым шаблоном int. Наконец, вы можете перевести перечис­ лимые типы в печатаемые строки вызовом метода toSt ring.

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

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

206

С т а т ь я 30

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

// Перечислимый тип с данными и отношением

public enum Planet {

 

MERCURY(3.302e+23,

2.439e6),

VENUS

(4.869e+24,

6.052e6),

EARTH

(5.975e+24,

6.378e6),

MARS (6.419e+23, 3.393e6),

JUPITER(1.899e+27,

7.149e7),

SATURN

(5.685e+26,

6.027e7),

URANUS

(8.683e+25,

2.556e7),

NEPTUNE(1.024e+26,

2.477e7);

private final double mass; // In kilograms private final double radius; // In meters

private final double surfaceGravity; // In m / s"2

//Universal gravitational constant in m"3 / kg s~2 private static final double G = 6.67300E-11;

//Constructor

Planet(double mass, double radius) { this.mass = mass;

this, radius = radius;

surfaceGravity = G * mass / (radius * radius);

}

public double mass() { return mass; } public double radius() { return radius; }

public double surfaceGravity() { return surfaceGravity; } public double surfaceWeight(double mass) {

return mass * surfaceGravity; // F = ma }

}

207

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

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

Перечислимые типы по своей природе неизменяемы, так что все поля должны быть завершенными (статья 15). Они могут быть от­ крытыми, но лучше сделать их закрытыми и снабдить открытыми средствами доступа (статья 14). В случае с Planet конструктор также рассчитывает и сохраняет значение гравитации у поверхности, но это лишь оптимизация. Гравитация может быть заново рассчитана на ос­ нове массы и радиуса каждый раз, когда используется метод surfaceWeight, который берет массу объекта и возвращает его вес на плане­ те, представленной константой.

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

public class WeightTable {

public static void main(String[] args) {

double earthWeight = Double.parseDouble(args[0]);

double mass = earthWeight / Planet.EARTH, surfaceGravity(); for (Planet p : Planet.valuesQ) System.out.printf(“Weight on %s is %f%n”,

p, p.surfaceWeight(mass));

}

}

Обратите внимание, что у Planet, как и у всех перечислимых ти­ пов, есть статический метод value, который возвращает массив его значений в той последовательности, в какой они были объявлены. Также заметьте, что метод toString возвращает декларированные названия каждого значения перечислимого типа, давая возможность их легко распечатать методами println и printf. Если вас не устра­

208

С т а т ь я 30

ивает представление данной строки, вы можете изменить его мето­ дом toString. Вот результат запуска нашей небольшой программы WeightTable с аргументом командной строки 175:

Weight on MERCURY is 66.133672

Weight on VENUS is 158.383926

Weight on EARTH is 175.000000

Weight on MARS is 66.430699

Weight on JUPITER is 442.693902

Weight on SATURN is 186.464970

Weight on URANUS is 158.349709

Weight on NEPTUNE is 198.846116

Если вы впервые видите, как действует метод printf в Java, об­ ратите внимание, что он отличается от аналога в С в том, что исполь­ зует %п там, где в С используют \п.

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

Если перечислимый тип полезен, то он должен быть классом высшего уровня; если его использование привязано к классу выс­ шего уровня, то он должен быть классом-членом класса высше­ го уровня (статья 22). Например, перечислимый тип java math. RoundingMode представляет режим округления десятичной дроби. Эти режимы округления используются классом BigDecimal и дают полезную абстракцию, которая фундаментально не привяза­ на к BigDecimal. Если сделать RoundingMode перечислимым типом верхнего уровня, разработчики библиотеки будут заставлять про­

209