Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ПРИС_шпоры.doc
Скачиваний:
0
Добавлен:
01.03.2025
Размер:
3.46 Mб
Скачать

27. Java. Обобщения: понятие и синтаксис.

Простые 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-метода определяется до возвращаемого значения (выделено красным). Generic-методы могут быть как статическими, так и не статическими.

Синтаксис вызова generic-метода c явным указанием параметров может показаться несколько необычным тем, кто пользовался шаблонами С++. Generic-параметр в Java ставится перед именем функции, а не после:<Integer>swap(ints, 1, 3);

strings.<Integer>zip(ints);

Так сделано из-за того, что запись f(a<b, c>(d)) неоднозначна и может встретиться в коде не только как вызов generic-метода a, результат которого передается в функцию f.

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

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

Ограничения

В отличие от С++, 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 <T extends Glyph> GlyphsContainter

{

...

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

}

Как в методах, так и в классах можно задать более одного базового интерфейса, который должен реализовывать generic-параметр. Это делается при помощи следующего синтаксиса: class <T extends Glyph & MoveableGlyph> MoveableGlyphsContainter

{

...

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

}

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

interface Bar1 {...}

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

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