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

Лекция 2. Generics

.pdf
Скачиваний:
12
Добавлен:
09.05.2015
Размер:
186.57 Кб
Скачать

Лекция 2. Generics

Java generics - ещё один механизм, появившийся в Java 1.5 для поддержки обобщенного программирования. Эта возможность позволяет создавать более статически типизированный код. Соответственно, программы теперь должны стать надежнее и проще в отладке. До этого момента самым широко распространенным языком, поддерживающим обобщенное программирование, был C++. Не странно, что многие ожидали от дженерик-ов Java чего-то похожего на шаблоны C++. На деле оказалось, что различия между generic-ами Java и шаблонами С++ довольно велики. В основном generic-и в Java получились проще, чем их C++-аналог, однако они не являются упрощенной версией шаблонов C++ и имеют ряд значительных отличий. Так, в языке появилось несколько новых концепций, касающихся generic-ов – это маски и ограничения.

Начать работать с generic-ами в Java очень просто. Легче всего это показать на примере коллекций. Для начала сравним код без применения generic-ов и код с generic-ами. Вот пример кода без применения generic-ов:

List strList = new ArrayList(); strList.add("some text");

//ОК, хотя коллекция предназначалась для хранения строк! strList.add(new Integer(0));

String str = (String)strList.get(0);

//Ошибка приведения типов во время выполнения (ClassCastException) Integer i = (Integer)strList.get(0);

Недостатки этого кода очевидны, и обидно, что язык позволял такое писать. В runtime подобный код приведет к генерации исключения ClassCastException.

А вот код с generic-ами:

List<String> strList = new ArrayList<String>(); strList.add("some text");

strList.add(new Integer()); // сообщение об ошибке компилятора

String str = strList.get(0);

Integer i = strList.get(0); // сообщение об ошибке компилятора

Разница вполне очевидна, нет надобности в приведении типов, проводится проверка типов на этапе компиляции, и код стал более надежным – получить ClassCastException уже сложнее. У экземпляра ArrayList, параметризованного String, метод get() будет возвращать String, и методу put() в качестве аргумента ничего, кроме String, передать не удастся.

Объявить generic-класс совсем несложно. Вот пример такого объявления:

class GenericList<E> {

E getFirst() { ... } void add(E obj) { ... }

}

Это пример generic-класса с одним параметром E, определяющим тип элементов списка, который реализует данный класс. Таких параметров может быть сколь угодно много.

Доступ к generic-параметру возможен в любом не статическом контексте параметризованного

класса.

Хочется обратить внимание на такой момент:

List<String> strList = new ArrayList<String>(); // 1

List<Object> objList = strList;

// 2 сообщение об ошибке компилятора

Строка 2 выдаст ошибку времени компиляции. Это может показаться неочевидным. String является наследником Object, и при приведении коллекций, казалось бы, все должно работать нормально, но на самом деле это не так. Если разрешить такое приведение, то после него можно будет добавить в List любой объект, унаследованный от Object. Такая ситуация легко может стать причиной проблем. Предположим, что у нас есть класс Car и два его наследника – SportCar и Truck. Если можно было бы преобразовать список со SportCar к списку Car, то мы бы смогли добавить туда экземпляр Truck, т.к. он является наследником Car. Получилось так, что в коллекцию с элементами одного типа мы добавили элемент другого типа, которой не является наследником первого. Это сводит на нет все преимущества generic-ов, ни о какой типобезопасности и речи быть не может (подобное поведение называется ковариантностью, ковариантность совершенно логична для неизменяемых коллекций, но, как показано выше, для изменяемых коллекций она не подходит).

Если generic-классу вообще не передаются параметры, считается, что в качестве параметров переданы Object, т.е. строки 1 и 2 в следующем примере эквивалентны:

List objList

= new ArrayList();

//

1

List<Object>

objList1 = new ArrayList<Object>(); //

2

 

 

 

 

Существуют не только generic-классы, но и generic-методы. Объявление generic-метода может выглядеть так:

public static <T> T getFirst(Collection<T> col) {...}

Конструкторы также могут быть параметризованы. Синтаксис вызова и объявления такой же, как и у функций.

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

Ограничения

В отличие от С++, generic-и в Java позволяют задать класс, от которого должен быть унаследован параметр generic-а, или интерфейс, который он должен реализовать. Это делается с помощью ключевого слова extends.

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

<T> T findNearest(Collection<T> glyphs, int x, int y) { ... }

Функция выглядит неплохо, но, тем не менее, не лишена недостатков. Получается так, что функции можно передать коллекцию любого типа. Это усложняет реализацию функции, порождая необходимость проверки типа элемента. Будет гораздо лучше написать так:

<T extends Glyph> T findNearest(Collection<T> glyphs, int x, int y) { ... }

Теперь все встает на свои места – в функцию можно передать только коллекцию, элементы которой реализуют интерфейс Glyph. Generic-и сделали свое дело, код получился более типобезопасным.

Extends можно применять и для параметров generic-классов:

class GlyphsContainter<T extends Glyph> {

...

public void addGlyph(T glyph){...}

}

Как в методах, так и в классах можно задать более одного базового интерфейса, который должен реализовывать generic-параметр. Это делается при помощи следующего синтаксиса:

class GlyphsContainter<T extends Glyph & MoveableGlyph> {

...

public void addGlyph(T glyph){...}

}

Теперь generic-параметр должен реализовывать не только интерфейс Glyph, но и MoveableGlyph. Ограничений на количество интерфейсов, которые должен реализовывать переданный тип, нет. Но класс можно передать только один, т.к. в Java нет множественного наследования. Типы в этом списке могут быть generic-типами, но ни один конкретный интерфейс не может появляться в списке более одного раза, даже с разными параметрами:

interface Bar<T> {} interface Bar1 {}

class Foo<T extends Bar<T> & Bar1> {} // ok

class Foo<T extends Bar<T> & Bar<Object> & Bar1> {} // ошибка

Маски

Маски являются еще одним отличием Java generic-ов от шаблонов С++. Они используются тогда, когда нужно абстрагироваться от конкретных аргументов типа, и позволяют использовать его там, где не требуется знать конкретные типы параметров. Например, это может быть метод, который очищает список, или метод, которому не интересен тип ключа карты (Map), т.к. он оперирует только со значениями. Приведенные ниже примеры помогут более подробно разобраться, что такое маски и для чего они нужны.

Синтаксически маска – это выражение, состоящее из символа ‘?’, и, возможно, ограничений,

например, ‘? extends T’ или ‘? super T’, где T – тип.

Чаще маски применяются при описании типов параметров методов, но их также можно использовать просто в объявлении переменных в теле метода или членов класса.

Маски с ограничением extends

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

void drawAll(Collection<? extends Glyph> glyphs) {

...

}

В данном примере метод принимает только коллекции, элементы которых – наследники класса Glyph. Вариант без использования маски (просто с Glyph) здесь не подойдет, т.к. при этом метод будет принимать только коллекции Glyph, но не его потомков.

Хочется также заметить, что если неизвестен точный тип параметра переданного класса, то использовать его можно с ограничениями. Например, компилятор не даст вызвать метод, который принимает generic-параметр в качестве аргумента.

Хотя использование масок с ограничением extends в параметрах методов является наиболее распространенным, в этом случае вполне можно обойтись без них. Следующий код будет полностью аналогичен коду, приведенному выше:

<T extends Glyph> void drawAll(Collection<T> glyphs) {

...

}

Маски с ограничением super

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

Этот тип ограничения задается с помощью ключевого слова super. Проще всего показать применение этого типа на примере коллекций и интерфейса Comparator:

public class TreeMap<K, V> extends AbstractMap<K, V>

implements SortedMap<K, V>, Cloneable, java.io.Serializable {

public TreeMap(Comparator<? super K> c) {

...

}

...

}

В качестве аргумента конструктора данный класс принимает Comparator, который может сравнивать всех предков K (типа ключа карты). Без введения этого типа маски невозможно бы было воспользоваться Comparator-ом. Это также позволяет получить дополнительную поддержку унаследованного кода, где Comparator-ы сравнивают объекты, приведенные к типу Object.

Кроме того, маски данного типа меньше ограничивают использование generic-класса. С ними можно вызывать функции, которые принимают в качестве параметра generic-аргумент, передавая туда

тип, указанный в качестве параметра super, а также его наследников. Вот пример такого вызова, это безопасно – тип параметра точно подойдет, т.к. является предком параметра:

public static void addElement(List<? super Glyph> list) { Glyph val = new CircleGlyph();

list.add(val);

}

Маски без ограничений

В качестве маски допустимо указать просто “?”. Это означает, что generic-параметр может быть любого типа и коду, который использует generic с такой маской, тип параметра не важен:

Collection<?> objs = new ArrayList<String>(); for (Object obj: objs )

System.out.println(obj.toString()); objs.add("text"); // ошибка времени компиляции objs.add(new Object()); // ошибка времени компиляции

В этом случае, при получении ссылки на объект, тип которого задан как ”?”, его можно интерпретировать только как Object. Это безопасно, т.к. все ссылочные типы являются потомками Object. Вызывать методы, принимающие ссылки на тип, определенный такой маской, нельзя.

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

static void doSomeWork(Map<?, ? extends Glyph> map) {...}

Тип “?” может быть также использован как аргумент generic-а и при вызове функций, что позволяет писать следующий код:

class Collections {

...

public static <T> List<T> unmodifiableList(List<T> list) { ... }

}

List<?> unknownList = new ArrayList<String>();

List<?> lt = Collections.unmodifiableList(unknownList);

Этот код безопасен и компилируется без ошибок, но вот такое, например, уже откомпилировано не будет:

public static <T> void addElement(List<? extends T> list, T val) {...}

List<?> unknownList = new ArrayList<String>();

 

addElement(unknownList, "String");

// ошибка

времени компиляции

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

Часть того, что умеют маски, можно решить и без них, пользуясь только generic-параметрами. Но существуют ситуации, когда без масок не обойтись. К таким ситуациями относится случай, где требуется ограничение super, а также случай, когда коду не требуется знать тип параметра, или он просто недоступен. Например, классу требуется переменная-член generic-класса или интерфейса, и ему нужно абстрагироваться от параметра типа. Иллюстрацией такого случая может служить класс, реализующий интерфейс LoginModule, который является частью JAAS (Java Authentication and Authorization Service). В этом интерфейсе определен метод initialize, который получает карту параметров модуля. Типы этих параметров заранее не определены, так что они могут быть какими угодно, их тип определяется во время исполнения и может быть разным для каждого параметра. После вызова initialize класс, реализующий интерфейс, должен сохранить эти параметры и использовать при вызове метода login:

public class DBLoginModule implements LoginModule { private Map<String, ?> options;

...

public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {

this.options = options;

...

}

public boolean login() throws LoginException {

final Object dataSourceJndiObj = options.get("datasource.jndi");

if (!(dataSourceJndiObj instanceof String)) {

throw new LoginException("Invalid login module parameters.");

}

dataSourceJndi = (String)dataSourceJndiObj;

...

}

...

}

Generic-и и исключения

Возможность использовать параметр generic-класса или метода в throws позволяет при описании абстрактного метода не ограничивать разработчика, использующего класс или интерфейс, конкретным типом исключения. Но использовать тип, заданный в качестве параметра, в catchвыражениях нельзя.

Кроме того, можно сгенерировать исключение, тип которого задается generic-параметром, но экземпляр должен быть создан извне. Это ограничение порождается одним из ограничений Java genericов - нельзя создать объект, используя оператор new, тип которого является параметром generic-а.

abstract class Processor<T extends Throwable> { abstract void process() throws T; // ok

void doWork() { try {

process();

} catch (T e) { // ошибка времени компиляции

}

}

void doThrow(T except) throws T { throw except; // ok

}

}

Хочется только добавить, что тип, переданный в качестве параметра, должен обязательно быть наследником Throwable.

Как это работает

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

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

class Foo implements Comparable<Foo> { public int compareTo(Foo foo) {

...

}

}

Чтобы в рантайме код работал корректно, в процессе его компиляции будет создан специальный метод, реализующий метод из интерфейса Comparable:

class Foo implements Comparable<Foo> {

public int compareTo(Object foo) { return compareTo((Foo)foo);

}

public int compareTo(Foo foo) {

...

}

}

Несмотря на то, что такой код компилироваться не будет, в байт-коде он вполне корректен.

Недостатки

Из-за особенностей реализации generic-и в Java 1.5 обладают несколькими недостатками, которые портят общее впечатление.

Многие из недостатков generic-ов исправить очень не просто из-за особенностей концепций самой платформы. Например, возможность динамической подгрузки типов лишает возможности статической проверки наличия нужного конструктора при создании экземпляра параметра generic-а. С

этой и другим проблемами, а также возможными путями их решения, можно ознакомиться здесь (Статья

“Java generics without the pain”. Eric E. Allen. http://www-106.ibm.com/developerworks/Java/library/j-djc02113.html).

Отсутствие runtime-информации о типе параметра generic-а. Для начала стоит напомнить, что никакой информации о типе, передаваемом в качестве параметра generic-а, нет. В какой-то степени этот недостаток сглаживается наличием ограничения extends, которое позволяет делать некоторые выводы о типе параметра.

Одна реализация на все случаи. Самая большая проблема вытекает из того, что в Java 1.5 компилятор не порождает отдельные специализации generic-класса для каждого применения, как это сделано в C++. С одной стороны, это позволило избежать проблем с совместимостью, т.к. не требует внесения изменений в формат байт-кода, но с другой стороны, повлекло за собой множество неприятных ограничений.

Вgeneric-классе или интерфейсе невозможно объявить статическую переменную, тип которой являлся бы параметром типа. Кроме того, статические методы не могут получить доступ к параметрам generic-класса, в котором они находятся, хотя сами могут быть параметризованы и могут использовать свои параметры.

По той же причине нет специализаций generic-ов для конкретных типов параметров. Жаль, конечно, но все же Java, в отличие от С++, позволяет получить на этапе выполнения информацию о типе, что позволяет отчасти компенсировать этот недостаток.

Ни generic-классы, ни generic-интерфейсы не могут быть унаследованными от типа, переданного в качестве параметра. Это невозможно из-за того, что существует только одна реализация generic-типа на все случаи его применения. Кроме того, это привело бы к неоднозначности: неизвестно, что передается в качестве параметра – класс или интерфейс. Это ограничение приводит к одному из самых досадных недостатков generic-ов в Java – невозможно использовать mixin-типы, так популярные

вС++. Этот недостаток отчасти компенсируется runtime-природой Java, так, фреймворки вроде Spring с успехом предоставляют подобные возможности во время исполнения.

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

Невозможно задать значения по умолчанию для параметров типов. Обойти их отсутствие можно

спомощью перегрузки, но выглядит это некрасиво и громоздко.

Нельзя использовать примитивные типы в качестве параметров generic-ов – параметрами могут быть только ссылочные типы. Это легко обойти, используя boxing. Autoboxing делает этот недостаток еще менее существенным, благодаря тому, что при работе с generic-ами, приведение примитивного типа к ссылочному и обратно будет производиться автоматически. Но при интенсивном использовании autoboxing повлечет за собой большие потери в производительности.

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

Вцелом, generic-и в Java получились проще, чем шаблоны в С++, но обладают гораздо меньшими возможностями. Тем не менее, они внесли несколько интересных концепций, таких как маски и ограничения, которые, добавили удобство при работе и помогли решить проблемы, которые

вC++ решались, мягко говоря, не тривиально. Но, как и любое усложнение языка, эти нововведения затрудняют его понимание и изучение - такие вещи, как super-ограничения при использовании масок, могут легко ввести разработчиков, только начинающих изучать язык, в ступор. Появление generic-ов сделало язык Java более выразительным и строгим.

Источники

1.Статья “Generics in the Java Programming Language”. Gilad Bracha. http://Java.sun.com/j2se/1.5/pdf/ generics-tutorial.pdf.

2.Статья “Using and Programming Generics in J2SE 5.0”. Qusay H. Mahmoud. http://Java.sun.com/ developer/technicalArticles/J2SE/generics/.

3.“Java Generics FAQs”. http://www.langer.camelot.de/GenericsFAQ/JavaGenericsFAQ.html.

4.Статья “Adding Generics to the Java Programming Language: Public Draft Specification, Version 2.0”. Gilad Bracha, Norman Cohen, Christian Kemper, Martin Odersky, David Stotamire, Kresten Thorup, Philip Walder. http://www.cs.purdue.edu/homes/hosking/352/generics.pdf.

5.Статья “Java generics without the pain”. Eric E. Allen. http://www-106.ibm.com/developerworks/Java/ library/j-djc02113.html.

6.Статья “Adding Wildcards to the Java Programming Language”. Mads Trogersen, Christian Plesner Hansen, Erik Erns, Peter von Der Ahe, Gliad Bracha and Neal Gafter. http://bracha.org/wildcards.pdf.

Задание.

Представим, что кто-то создал класс аренды, который управляет пулом арендуемых товаров:

import java.util.List;

public class Rental {

private List rentalPool; private int maxNum;

public Rental(int maxNum, List rentalPool) { this.maxNum = maxNum;

this.rentalPool = rentalPool;

}

public Object getRental() {

// blocks until there's something available return rentalPool.get(0);

}

public void returnRental(Object o) { rentalPool.add(o);

}

}

Теперь представьте, что вы хотели сделать подкласс аренды, который был бы предназначен только для аренды автомобилей:

import java.util.List;

public class CarRental extends Rental {

public CarRental(int maxNum, List<Car> rentalPool) { super(maxNum, rentalPool);

}

public Car getRental() {

return (Car) super.getRental();

}

public void returnRental(Car c) { super.returnRental(c);

}

public void returnRental(Object o) { if (o instanceof Car) {

super.returnRental(o); } else {

System.out.println("Cannot add a non-Car"); // probably throw an exception

}

}

}

class Car{

}

И напишем тест для нашей аренды автомобилей:

import java.util.ArrayList; import java.util.List;

public class Test {

public static void main(String[] args) { Car c1 = new Car();

Car c2 = new Car();

List carList = new ArrayList(); carList.add(c1); carList.add(c2);

Rental carRental = new Rental(2, carList); Car carToRent = (Car) carRental.getRental(); carRental.returnRental(carToRent);

}

}

Чем больше вы смотрим на всё это дело, тем больше понимаете, что:

1)При проектировании CarRental вам приходится делать собственную проверку на тип в методе returnRental().

2)Вы действительно не хотели бы создавать отдельные подклассы для каждого возможного вида арендуемой вещи (автомобили, компьютеры, обувь, и так далее).

Но вы же отличные Java-программисты и только что прочитали изучили генерики. И вы знаете, что нужно сделать ;)