Главная | Обратная связь | Поможем написать вашу работу!
МегаЛекции

Глава 8. Generic-классы в Java




 

Generics – это встроенная в язык особенность, которая позволяет сделать программирование более универсальным и надежным. Является аналогом шаблонных классов в С++. Шабло́ны в C++ - это средства, «предназначенные для кодирования обобщённых алгоритмов, без привязки к некоторым параметрам (например, типам данных, размерам буферов, значениям по умолчанию)» (Википедия). Однако имеют и ряд существенных отличий.

Рассмотрим примеркласса Box. С оздадим простой класс Box, который управляет объектами разных типов. Он должен иметь только два метода: add, который добавляет объект в коробку, и get, который извлекает его:

 

public class Box {

private Object object;

public void add(Object object) {

this.object = object;

}

public Object get() { return object; }

}

 

Так как эти методы принимают и возвращают ссылку на Object, можно работать с объектами любых непримитивных типов. Однако если нужно ограничить применение нашего класса только для объектов одного типа, например, Integer, можно только указать это в документации, а компилятор ничего не будет знать об этом:

 

public class BoxDemo1 {

public static void main(String[] args) {

// ONLY place Integer objects into this box!

Box integerBox = new Box();

integerBox.add(new Integer(10));

Integer someInteger = (Integer)integerBox.get();

System.out.println(someInteger);

}

}

 

Если мы сохраним число 10 как String, то при преобразовании Object в Integer возникнет ошибка. Это совершенно очевидная ошибка, но код откомпилируется без ошибок, однако во время выполнения программыи будет выброшено исключение типа ClassCastException. Если бы класс Box был построен с учетом generics, то эта ошибка была бы обнаружена во время компиляции.

Переделаем класс Box по правилам generics. Создадим объявление типа generic (generic type declaration), изменив код "public class Box" на "public class Box<T>"; введя переменную типа (type variable) по имени T, которую можно использовать везде внутри класса. Тот же прием может быть применен и к интерфейсам. T – это специальный вид переменной, чье значение – это тип, который может быть любым: интерфейсом, классом, но не примитивным типом данных. Назовем его формальным параметром типа класса Box.

 

public class Box <T> { private T t; // T stands for "Type" public void add(T t) { this. t = t; } public T get() { return t; }}

Используя класс в программном коде, можно выполнить генерацию класса (generic type invocation), которая заменит тип T на конкретное значение, например, Integer:

Box<Integer> integerBox;

Здесь не создается новый объект Box. Это простое объявление, что integerBox будет ссылкой на "Box для Integer", и теперь это записывается так: Box<Integer>.

Класс Box называется параметризованным типом (parameterized type). Чтобы создать объект этого класса, используем слово new, как обычно, но вставляем <Integer> между именем класса и скобками:

integerBox = new Box<Integer>();

После инициализации integerBox можно вызвать метод get без указания приведения типов, как показано в примере BoxDemo2:

public class BoxDemo2 { public static void main(String[] args) { Box<Integer> integerBox = new Box<Integer>(); integerBox.add(new Integer(10)); Integer someInteger = integerBox.get(); // no cast! System.out.println(someInteger); }} Если Вы попробуете добавить объект типа String, компилятор выдаст сообщение об ошибке.

Важно представлять, что переменные, определяющие тип, не являются самими типами. Нет класса T.java или T.class. T – это не часть класса Box. Фактически во время компиляции вся информация generic удаляется, и остается только класс Box.class.

Заметим, что перечисляться могут несколько параметров типа, но они должны отличаться по именам Box<T,U>.

Методы и конструкторы generic. Параметры типов могут объявляться внутри методов и конструкторов для создания так называемых generic methods и generic constructors. Это делается похожим способом, но диапазон действия параметра ограничен методом или конструктором, в котором он объявлен.

 

public class Box<T> {

private T t;

public void add(T t) { this.t = t; }

public T get() { return t; }

public <U> void inspect(U u){

System.out.println("T: " + t.getClass().getName());

System.out.println("U: " + u.getClass().getName());

}

public static void main(String[] args) {

Box<Integer> integerBox = new Box<Integer>();

integerBox.add(new Integer(10));

integerBox.inspect("some text");

}

}

 

Мы добавили один generic метод, с именем inspect, который определяет один параметр типа U. Этот метод получает ссылку на объект и выводит его тип, а также выводит тип T.

Более разумное использование generic методов показано ниже. Здесь во все объекты типа Box, входящие в список, добавляется ссылка на один и тот же объект:

 

public static <U> void fillBoxes(U u, List<Box<U>> boxes) {

for (Box<U> box: boxes) {

box.add(u);

}

}

 

Чтобы использовать этот метод, код может иметь следующий вид:

Crayon red =...;

List<Box<Crayon>> crayonBoxes =...;

Вызов статического метода будет следующим:

Box.<Crayon>fillBoxes(red, crayonBoxes);

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

Box.fillBoxes(red, crayonBoxes);

Эта особенность называется type inference, она позволяет вызывать generic метод как обычный, без указания типа в угловых скобках.

Ограниченные параметры типа. Возможно, что Вы захотите ограничить типы, которые можно передавать как параметры типа. Например, метод, который оперирует с числами, захочет принимать только объекты типа Number и его подклассов. Это так называемые bounded type parameters.

Для объявления такого типа параметров укажите имя параметра типа, за ним ключевое слово extends Number. Заметим, что extends используется и для классов и для интерфейсов.

 

public class Box<T> {

private T t;

public void add(T t) { this.t = t; }

public T get() { return t; }

public <U extends Number > void inspect(U u){

}

}

 

Теперь при компиляции будет выведено сообщение об ошибке, если inspect вызывается с параметром String.

Если надо указать дополнительные классы или интерфейсы, то используется символ &:

<U extends Number & MyInterface>

Подтипы. Как известно, возможно назначить ссылку на объект одного типа ссылке на объект другого типа, если эти типы совместимы. Например, можно присвоить ссылку на Integer ссылке на Object, так как Object – один из базовых типов Integer.

То же самое справедливо и в отношении generics. Можно в качестве типа указать Number, а в методе add будет разрешено использование аргументов, наследующих от него:

 

Box<Number> box = new Box<Number>();

box.add(new Integer(10)); // OK

box.add(new Double(10.1)); // OK

 

Рассмотрим следующий метод:

public void boxTest(Box<Number> n) { … }

Какой тип аргумента он принимает? Мы видим один аргумент – тип его Box<Number>. Можно ли передавать Box<Integer> или Box<Double>? Ответ - "нет", потому что типы Box<Integer> и Box<Double> не являются наследниками Box<Number>.

 

Понимание проще, если представить реальные картинки, например, клетки для перевозки животных:

interface Cage<E> extends Collection<E>;

Интерфейс Collection – это корневой интерфейс иерархии классов коллекций; он представляет группу объектов. Клетка – испольуется для хранения набора животных, поэтому мы и наследуем от Сollection.

Лев – это животное, поэтому Lion наследует от Animal:

interface Lion extends Animal {}

Lion king =...;

Animal a = king;

Посадим его в клетку:

Cage<Lion> lionCage =...;

lionCage.add(king);

так же добавим бабочку в клетку с бабочками:

interface Butterfly extends Animal {}

Butterfly monarch =...;

Cage<Butterfly> butterflyCage =...;

butterflyCage.add(monarch);

А теперь поговорим о клетке для животных вообще:

Cage<Animal> animalCage =...;

Здесь можно хранить всех животных. Тогда мы можем поместить туда и льва, и бабочку:

animalCage.add(king);

animalCage.add(monarch);

 

Да, лев – животное (Lion – это наследник от Animal), но клетка для льва – это не наследник клетки для животного. Нельзя создать клетку, куда можно было бы поместить и льва, и бабочку. Значит, нет клетки, которую можно рассматривать как некоторую единую:

animalCage = lionCage; // ошибка компиляции

animalCage = butterflyCage; // ошибка компиляции

Без generics такое невозможно.

Wildcards (знак вопроса). Фраза "клетка для животных - animal cage" может означать или "клетка для всех животных - all-animal cage", но может, в зависимости от контекста, означать и другое: клетка, предназначенная не для любого вида животных, а для определенного вида животного, вид которого не пока сообщается. В generics такой неоговариваемый тип помечается символом wildcard - "?".

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

Cage<? extends Animal> someCage =...;

Описание "? extends Animal" определяет неизвестный заранее тип, который наследует от класса Animal или является самим Animal. Это пример bounded wildcard, где Animal определяет ограничение на ожидаемый тип. Теперь мы можем создать клетку для львов или клетку для бабочек.

Можно также указать ключевое слово super вместо extends. Код <? super Animal> читается как тип, являющийся базовым для Animal или сам Animal. Можно просто указать <?>. Это то же самое, что и <? extends Object>.

Так как Cage<Lion> и Cage<Butterfly> не наследуют от Cage<Animal>, они фактически подклассы Cage<? extends Animal>:

someCage = lionCage; // OK

someCage = butterflyCage; // OK

 

Ну а теперь ответим на вопрос: "Можно ли поместить и льва и бабочку в одну клетку someCage?". Ответ будет - "нет".

someCage.add(king); // ошибка компиляции

someCage.add(monarch); // ошибка компиляции

Если someCage – это клета для бабочки, то она позволит выполнить операции для бабочек, а льва туда не поместить. Можно создать метод для того, чтобы животных в данной клетке данного типа покормить:

 

void feedAnimals(Cage<? extends Animal> someCage) {

for (Animal a: someCage) a.feedMe();

}

 

Тогда можно поместить львов и бабочек в отдельные клетки и покормить их в своих клетках:

feedAnimals(lionCage);

feedAnimals(butterflyCage);

Или можно поместить всех в одну клетку и кормить вместе:

feedAnimals(animalCage);

Type erasure. При создании реального типа на основе типа generic компилятор использует прием, называемый type erasure — при этом компилятор удаляет всю информацию, относящуюся к типу и из самого класса и из его методов. Это позволяет поддерживать совместимость с Java – библиотеками, созданными ранее, до generics.

Например, Box<String> транслируется в класс Box, который называется сырым типом (raw type) — это класс или интерфейс без параметов типа. Это означает, что нельзя определить во время выполнения, какого типа объект используется в сгенерированном классе. Следующие операции невозможны:

 

public class MyClass<E> {

public static void myMethod(Object item) {

if (item instanceof E) { //Ошибка компиляции

...

}

E item2 = new E();//Ошибка компиляции

E[] iArray = new E[10]; /Ошибка компиляции

E obj = (E)new Object(); //Unchecked cast warning

}

}

 

Выделенные действия невозможны во время выполнения программы, потому что компилятор удаляет всю информацию о реальном типе аргумента, представленного параметром E на фазе компиляции.

Такая реализация позволяет осуществить совместимость со старым кодом. Вообще использование «сырого кода» - это плохая практика программирования, ее обычно следует избегать.

Поделиться:





Воспользуйтесь поиском по сайту:



©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...