Философия Java3
Шрифт:
Мы также видели, что рефлексия открывает перед программистом множество новых возможностей и делает возможным более динамичный стиль программирования. Пожалуй, динамическая природа рефлексии кому-то покажется пугающей. Для тех, кто привык к безопасной статической проверке типов, сама возможность выполнения действий, правильность которых проверяется только на стадии выполнения, а для выдачи информации используются исключения, выглядит шагом в неверном направлении. Некоторые доходят до утверждений, будто сама возможность исключения на стадии выполнения свидетельствует о том, что такого кода лучше избегать. На мой взгляд, чувство безопасности весьма иллюзорно — неожиданности и исключения возможны всегда,
Параметризация
Обычные классы и методы работают с конкретными типами: либо, примитивами, либо с классами. Если ваш код должен работать с разными типами, такая жесткость может создавать проблемы.
Одним из механизмов обеспечения универсальности кода в объектно-ориен-тированных языках является полиморфизм. Например, вы можете написать метод, который получает в аргументе объект базового класса, а затем использует этот метод с любым классом, производным от него. Метод становится чуть более универсальным, а область его применения расширяется. Это относится и к классам — использование базового класса вместо производного обеспечивает дополнительную гибкость. Конечно, наследование возможно только для классов, не являющихся final.
Впрочем, иногда даже рамки одной иерархии оказываются слишком тесными. Если в аргументе метода передается интерфейс вместо класса, то ограничения ослабляются и в них включается все, что реализует данный интерфейс, — в том числе и классы, которые еще не были созданы. Это дает программисту-клиенту возможность реализовать интерфейс, чтобы соответствовать требованиям вашего класса или метода. Таким образом, интерфейсы позволяют выходить за рамки иерархий классов, если только у вас имеется возможность создать новый класс.
Но в некоторых случаях даже интерфейсы оказываются недостаточно гибкими. Интерфейс требует, чтобы ваш код работал в этом конкретном интерфейсе. Если бы было можно указать, что ваш код работает «с некоторым не заданным типом», а не с конкретным интерфейсом или классом, программа приобрела бы еще более общий характер.
В этом и состоит концепция параметризации — одного из самых значительных новшеств Java SE5. Параметризованные типы позволяют создавать компоненты (прежде всего, контейнеры), которые могут легко использоваться с разными типами. Если прежде вы еще никогда не встречались с механизмом параметризации в действии, вероятно, параметризованные типы Java покажутся вам довольно удобным дополнением к языку. При создании экземпляра параметризованного типа преобразования типа выполняются автоматически, а правильность типов проверяется на стадии компиляции. С другой стороны, разработчики с опытом использования параметризованных типов в других языках (скажем, в С++) увидят, что в Java они не соответствуют всем ожиданиям. Если использовать готовый параметризованный тип относительно несложно, при попытке написать собственный тип вас ждут сюрпризы. В частности, в этой главе я постараюсь объяснить, почему параметризованные типы Java получились именно такими.
Не стоит думать, что параметризованные типы Java бесполезны —
Простая параметризация
Одной из важнейших причин для появления параметризации стало создание классов контейнеров (см. главу 11). Контейнер предназначен для хранения объектов, используемых в программе. В принципе это описание подойдет и для массива, но контейнеры обычно обладают большей гибкостью и отличаются по своим характеристикам от простых массивов. Необходимость хранения групп объектов возникает едва ли не в каждой программе, поэтому контейнеры составляют одну из самых часто используемых библиотек классов.
Рассмотрим класс для хранения одного объекта. Конечно, в этом классе можно указать точный тип объекта:
//• generics/Holderl java
class Automobile {}
public class Holderl {
private Automobile a;
public Holderl(Automobile a) { this.a = a; } Automobile get { return a; }
} ///:-
Однако такой «контейнер» получается не слишком универсальным — он не может использоваться только для одного типа. Конечно, было бы неудобно создавать новый класс для каждого типа, который нам встретится в программе.
До выхода Java SE5 можно было бы хранить в классе Object:
II: generics/Holder2.java
public class Holder2 { private Object a;
public Holder2(0bject a) { this a = a; } public void set(Object a) { this.a = a; }
public Object get О { return a; } public static void main(String[] args) {
Holder2 h2 = new Holder2(new AutomobileO); Automobile a = (Automobile)h2 get; h2.set("He Automobile"); String s = (String)h2:get;
h2.set(l); // Автоматически упаковывается в Integer Integer x = (Integer)h2.get,
}
} ///:-
Теперь класс Holder2 может хранить все, что угодно, — в приведенном примере один объект Holder2 используется для хранения трех разных типов данных.
В некоторых случаях бывает нужно, чтобы контейнер мог хранить объекты разных типов, но чаще контейнер предназначается для одного типа объектов. Одна из главных причин для применения параметризованных типов заключается именно в этом: вы можете указать, какой тип должен храниться в контейнере, и заданный тип будет поддерживаться комплиятором.
Итак, вместо Object в определении класса было бы удобнее использовать некий условный заменитель, чтобы отложить выбор до более позднего момента. Для этого после имени класса в угловых скобках указывается параметр типа, который при использовании заменяется фактическим типом. В нашем примере это будет выглядеть так (Т — параметр типа):
II: generics/Holder3.java
public class Holder3<T> { private T a;
public Holder3(T a) { this.a = a; } public void set(T a) { this.a = a; } public T getО { return a; } public static void main(String[] args) { Holder3<Automobile> h3 =
new Holder3<Automobile>(new AutomobileO); Automobile a = h3.get; // Преобразование не требуется // h3.set("He Automobile"); // Ошибка // h3.set(1); // Ошибка
}
} ///:-
При создании Holder3 необходимо указать тип объектов, хранящихся в контейнере, в угловых скобках, как в main. В дальнейшем в контейнер можно будет помещать объекты только этого типа (или производного, так как принцип заменяемости работает и для параметризованных типов). А при извлечении вы автоматически получаете объект нужного типа.