Обобщения (Generics)
Основные сведения об обобщениях
Термин обобщение по сути означает параметризированный тип. Специфика параметризированных типов состоит в том, что они позволяют создавать классы, интерфейсы и методы, в которых тип данных указывается в виде параметра. Используя обобщения, можно создать единственный класс, который будет автоматически работать с различными типами данных. Классы, интерфейсы и методы, оперирующие параметризированными типами, называются обобщенными, как, например, обобщенный класс или обобщенный метод.
Обобщения привносят безопасность типов, которой раньше так недоставало, поскольку в этом случае автоматически выполняются неявные приведения. обобщения расширяют возможности повторного использования кода, делая этот процесс безопасным и надежным. Пример объявления интерфейса с обобщением:Главное преимущество обобщенного кода состоит в том, что он будет автоматически работать с типом данных, переданным ему в качестве параметра.
public interface IGenQ<T> { // Поместить элемент в очередь void put(T ch) throws QueueFullException; // Извлечь элемент из очереди T get() throws QueueEmptyException; }Пример объявления класса с обобщением:
public class GenQueue<T> implements IGenQ<T> { private T[] q; // массив для хранения элементов очереди private int putloc, getloc; // индексы вставки и извлечения элементов очереди // Создание пустой очереди из заданного массива public GenQueue(T[] aRef) { q = aRef; putloc = getloc = 0; } // Поместить элемент в очередь @Override public void put(T obj) throws QueueFullException { if (putloc == q.length - 1) throw new QueueFullException(q.length); q[putloc++] = obj; } // Извлечь элемент из очереди @Override public T get() throws QueueEmptyException { if (getloc == putloc) throw new QueueEmptyException(); return q[getloc++]; } // Отобразить тип Т public void showType() { System.out.println("Tип Т - это " + q.getClass().getName()); // getClass() - Возвращает класс среды выполнения этого Object //public final Class getClass() //getName() - возвращает имя класса (строку) } }- где T - имя параметра типа. Это имя - заполнитель, подлежащий замене фактическим типом, передаваемым конструктору GenQueue() при создании объекта. Следовательно, имя T используется в классе GenQueue всякий раз, когда возникает необходимость в использовании параметра типа. Обратите внимание на то, что имя T заключено в угловые скобки ( < > ). Этот синтаксис является общим: всякий раз, когда объявляется параметр типа, он указывается в угловых скобках. Поскольку класс GenQueue использует параметр типа, он является обобщенным классом.
В объявлении класса Gen имя для параметра типа могло быть выбрано совершенно произвольно, но по традиции выбирается имя T. Вообще говоря, для этого рекомендуется выбирать имя, состоящее из одной прописной буквы. Другими распространенными именами параметров типа являются V и Е.
Далее в программе имя T используется при объявлении массива q:
private T[] q;
Как уже отмечалось, имя параметра типа T служит заполнителем, вместо которого при создании объекта класса Gen указывается конкретный тип. Поэтому массив q будет иметь тип, передаваемый в виде параметра T при получении экземпляра объекта класса GenQueue.
Параметр aRef конструктора имеет тип T. Это означает, что конкретный тип параметра aRef определяется типом, передаваемым в виде параметра T при создании объекта класса GenQueue.
Кроме того, параметр типа T можно указывать в качестве типа значения, возвращаемого методом:
public T get() throws QueueEmptyException { if (getloc == putloc) throw new QueueEmptyException(); return q[getloc++]; }Пример использования обобщенного класса:
// Создать очередь для хранения целых чисел Integer iStore[] = new Integer[10]; GenQueue<Integer> q = new GenQueue<Integer>(iStore);* Прежде чем мы начнем продвигаться дальше, важно подчеркнуть, что на самом деле никакие разные версии класса GenQueue (как и вообще любого друтоrо класса) компилятором Java не создаются. В действительности компилятор просто удаляет всю информацию об обобщенном типе, выполняя все необходимые приведения типов и тем самым заставляя код вести себя так, словно была создана специфическая версия класса Gen, хотя в действительности в программе существует только одна версия GenQueue - обобщенная. Процесс удаления информации об обобщенном типе называется очистка, и к этой теме мы еще вернемся в данной главе.
Обратите внимание на то, что при вызове конструктора класса GenQueue указывается также аргумент типа Integer. Это необходимо потому, что тип объекта, на который указывает ссылка (в данном случае - q), должен соответствовать GenQueue<Integer>. Если тип ссылки, возвращаемой оператором new, будет отличаться от GenQueue<Integer>, то компилятор сообщит об ошибке. Например, это произойдет при попытке скомпилировать следующую строку кода:
GenQueue<Integer> q = new GenQueue<Double>(iStore); // Ошибка!
GenQueue<Integer> "<Integer>" как бы становится неотъемлемой частью имени класса GenQueue и в инструкциях где нужно указать имя класса - указывается не GenQueue а GenQueue<Integer> например:
- в указании типа ссылки на объект: GenQueue<Integer> q = ...;
- в создании экземпляра класса: q = new GenQueue<Integer>(iStore);
Переменная q относится к типу GenQueue<Integer>, а следовательно, она не может быть использована для хранения ссылки на объект типа GenQueue<Double>.
Этот вид проверки - одно из основных преимуществ обобщенных типов, поскольку они обеспечивают типовую безопасность.
Обобщения работают только с объектами
Когда объявляется экземпляр обобщенного типа, аргумент, передаваемый параметру типа, должен быть типом класса. Использовать для этой цели простые типы, например int или char, нельзя.
Gen<int> strOb = new Gen<int>(53); // Ошибка, нельзя использовать простой типОчевидно, что невозможность подобной передачи простых типов не является серьезным ограничением, поскольку всегда имеется возможность использовать объектные оболочки для инкапсуляции значений. Кроме того, механизм автоупаковки и автораспаковки Java - делает использование оболочек прозрачным.
Различение обобщений по аргументам типа
Ключом к пониманию обобщений является тот факт, что ссылки на разные специфические версии одного и того же обобщенного типа несовместимы между собой. Так, наличие следующих строк кода привело бы к ошибке во время компиляции:
GenQueue<Integer> iQ = new GenQueue<Integer>(iStore); GenQueue<String> sQ = new GenQueue<String>(sStore); iQ = sQ; // Ошибка!* Несмотря на то что обе переменные, iQ и sQ, относятся к типу GenQueue<T>, они ссылаются на объекты разного типа, поскольку в их объявлениях указаны разные арrументы типа. Это и есть частный пример той безопасности типов, которая обеспечивается использованием обобщенных типов, способствующим предотвращению возможных ошибок.
Обобщенный класс с двумя параметрами типа
Обобщенные типы допускают объявление нескольких параметров типа. Параметры задаются в виде списка элементов, разделенных запятыми. В качестве примера ниже приведен класс TwoGen, в котором определены два параметра типа.
class TwoGen<T,V> { Т ob1; V ob2; // Передать конструктору класса ссылки на объекты типов Т и V TwoGen(T ol, V о2) { ob1 = ol; ob2 = о2; } // Отобразить типы Т и V void showTypes() { System.out.println("Tип Т - это " + ob1.getClass() .getName()); System.out.println("Tип V - это " + ob2.getClass() .getName()); } Т getob1 () { return ob1; } V getob2 () { return ob2; } }Демонстрация класса TwoGen:
class SimpGen { puЬlic static void main(String args[]) { TwoGen<Integer, String> tgObj = new TwoGen<Integer, String>(88, "Обобщения"); // Отобразить типы tgObj.showTypes(); // Получить и отобразить значения int v = tgObj.getob1(); System.out.println("знaчeниe: " + v); String str = tgObj.getob2(); System.out.println("знaчeниe: " + str); }В данном случае тип Integer передается в качестве параметра типа Т, а тип String - в качестве параметра типа V. И хотя в этом примере типы аргументов отличаются, они могут и совпадать. Например, следующая строка кода вполне допустима:
TwoGen<String, String> х = new TwoGen<String, String>("A", "В");Очевидно, что если типы аргументов совпадают, то определять два параметра типа в обобщенном классе нет никакой надобности.
Общая форма обобщенного класса
Синтаксис, представленный в предыдущих примерах, можно обобщить. Ниже приведена общая форма объявления обобщенного класса.
class имя_класса<список_параметров_типа> { // ...
А вот как выглядит синтаксис объявления ссылки на обобщенный класс:
имя_класса<список_аргументов_типа> имя_переменной = new имя_класса<список_аргументов_типа>(список_аргументов_конструктора);
Ограниченные типы
В предыдущих примерах параметры типа могли заменяться любым типом класса. Такая подстановка годится для многих целей, но иногда полезно ограничить допустимый ряд типов, передаваемых в качестве параметра типа. Допустим, требуется создать обобщенный класс для хранения числовых значений и выполнения над ними различных математических операций, включая получение обратной величины или извлечение дробной части. Допустим также, что в этом классе предполагается выполнение математических операций над данными любых числовых типов: как целочисленных, так и с плавающей точкой.
Для подобных случаев в Java предусмотрены ограниченные типы. При указании параметра типа можно задать верхнюю границу, объявив суперкласс, который должны наследовать все аргументы типа. Это делается с помощью ключевого слова extends:
<Т extends суперкласс>
class NumericFns<T extends Number> { //... class Pair<T, V extends T> { //...Это объявление сообщает компилятору о том, что параметр типа T может быть заменен только суперклассом или его подклассами. Таким образом, суперкласс определяет верхнюю границу в иерархии классов Java.
Ограниченные типы особенно полезны в тех случаях, когда нужно обеспечить совместимость одного параметра типа с другим. Рассмотрим в качестве примера представленный ниже класс Pair. В нем хранятся два объекта, которые должны быть совместимы друг с другом.
class Pair<T, V extends T> { Т first; V second; Pair(Т а, V b) { first = а; second = b; } // ... }В классе Pair определены два параметра типа, T и V, причем тип V расширяет тип T. Это означает, что тип V должен быть либо того же типа, что и T, либо его подклассом. Благодаря такому объявлению гарантируется, что два параметра типа, передаваемые конструктору класса Pair, будут совместимы друг с другом.
Использование шаблонов аргументов
Безопасность типов - вещь полезная, но иногда она может мешать созданию идеальных во всех других отношениях конструкций. Допустим, требуется реализовать метод absEqual(), возвращающий значение true в том случае, если два объекта уже упоминавшегося класса NumericFns содержат одинаковые абсолютные значения. Допустим также, что этот метод должен оперировать любыми типами числовых данных, которые могут храниться в сравниваемых объектах. Так, если один объект содержит значение l.25 типа Double, а другой - значение -1.25 типа Float, то метод absEqual() должен возвращать логическое значение true. Один из способов реализации метода absEqual() состоит в том, чтобы передавать этому методу параметр типа NumericFns, а затем сравнивать его абсолютное значение с абсолютным значением текущего объекта и возвращать значение true, если эти значения совпадают. Например, вызов метода absEqual() может выглядеть следующим образом:
NumericFns<Double> dOb = new NumericFns<Double>(l.25); NumericFns<Float> fOb = new NumericFns<Float>(-1.25); if (dOb.absEqual(fOb)) System.out.println("Aбcoлютныe значения совпадают"); else System.out.println("Aбcoлютныe значения отличаются");Проблемы начнутся при первой же попытке объявить параметр типа NumericFns! Каким он должен быть? Казалось бы, подходящим должно быть следующее решение, где T указывается в качестве параметра типа.
// Это не будет работать! boolean absEqual(NumericFns<T> ob) { if (Math.abs(num.doubleValue()) == Math.abs(ob.num.doubleValue()) return true; return false; }Проблема состоит в том, что приведенное выше решение будет работать только тогда, когда объект класса NumericFns, передаваемый в качестве параметра, имеет тот же тип, что и текущий объект!!!
Например, если текущий объект относится к типу NumericFns<Double>, то параметр ob также должен быть типа NumericFns, а следовательно, сравнить текущий объект с объектом типа NumericFns<Double> не удастся. Таким образом, выбранное решение не является обобщенным.
Для того чтобы создать обобщенный метод absEqual(), следует использовать еще одно средство обобщений - ШАБЛОН АРГУМЕНТА. Шаблон обозначается метасимволом ? , которому соответствует неизвестный тип данных. Используя этот метасимвол, можно переписать метод absEqual() в следующем виде.
// Проверить равенство абсолютных значений двух объектов boolean absEqual(NumericFns<?> ob) { // обратите внимание на метасимвол if (Math.abs(num.doubleValue()) == Math.abs(ob.num.doubleValue()) return true; return false; }В данном случае выражение NumericFns соответствует любому типу объекта из класса NшnericFns<?>, что позволяет сравнивать абсолютные значения в двух произвольных объектах класса NumericFns.
а так сделать не получится:
boolean absEqual(NumericFns ob) { //... Ошибка:
- Incorrect number of arguments for type NumericFns<T>;
it cannot be parameterized with arguments <T, Number>
- Syntax error on token "extends", , expected
И последнее замечание:
не следует забывать, что шаблоны аргументов не влияют на тип создаваемого объекта в классе NumericFns. Для этой цели служит оператор extends, указываемый в объявлении класса NumericFns. Шаблон лишь указывает на соответствие ЛЮБОМУ ДОПУСТИМОМУ объекту класса NumericFns. (package ms.learning.generics; WildcardDemo)
Ограниченные шаблоны
Шаблоны аргументов можно ограничивать в основном так же, как и параметры типов. Ограниченные шаблоны особенно полезны при создании методов, которые должны оперировать только объектами подклассов определенного суперкласса.
Допустим требуется создать метод, который оперирует только объектами типа NumericFns<тип>, где тип - это класс Number или его подклассы. Для этой цели нужно воспользоваться ограниченным шаблоном аргумента. Ниже приведен пример объявления метода absEqual(), которому в качестве аргумента может быть передан только объект класса NumericFns, параметр типа которого обязан соответствовать классу Number или его подклассам.
boolean absEqual(NumericFns<? extends Number> ob) { // !ограниченный! шаблон аргумента if ( Math.abs(num.doubleValue()) == Math.abs(ob.num.doubleValue()) ) return true; return false; }В общем случае для установки верхней границы шаблона аргумента используется выражение следующего вида:
<? extends суперкласс> (т.е. допускаются все потомки, и указанный суперкласс (родитель))
- где после ключевого слова extends указывается суперкласс, т.е. имя класса, определяющего верхнюю границу, включая и его самого. Это означает, что в качестве аргумента допускается указывать не только подклассы данного класса, но и сам этот класс.
По необходимости можно указать также нижнюю границу. Для этого используется ключевое слово super, указываемое в следующей общей форме:
<? super подкласс> (т.е. допускаются все родители, и указанный подкласс (потомок))
В данном случае в качестве аргумента допускается использовать только суперклассы, от которых наследует подкласс, включая его самого.
Спросим у эксперта:
Можно ли привести один экземпляр обобщенного класса к другому?
Да, можно. Но только в том случае, если типы обоих классов совместимы и их аргументы типа совпадают. Рассмотрим в качестве примера обобщенный класс Gen:
class Gen { // ... }Далее допустим, что переменная х объявлена так:
Gen<Integer> х = new Gen<Integer>();В этом случае следующее приведение типов может быть выполнено, поскольку переменная х - это экземпляр класса Gen<Integer>:
(Gen<Integer>) х // ДопустимоА следующее приведение типов не может быть выполнено, поскольку переменная хне является экземпляром класса Gen:
(Gen<Long>) х // Недопустимо
Обобщенные методы
Как было показано в предыдущих примерах, методы в обобщенных классах могут использовать параметр типа своего класса, а следовательно, автоматически становятся обобщенными относительно параметра типа. Однако можно объявить обобщенный метод, который сам по себе использует параметры типа. Более того, такой метод может быть объявлен в обычном, а не обобщенном классе.
Пример объявления обобщённого метода:
// Определить, совпадает ли содержимое двух массивов static <T extends Comparable<T>, V extends T> boolean arraysEqual(T[] x, V[] y) { // Массивы, имеющие разную длину, не могут быть одинаковыми if (x.length != y.length) return false; for (int i = 0; i < x.length; i++) { if (!x[i].equals(y[i])) return false; // массивы отличаются } return true; // содержимое массивов совпадает }Пример вызова обобщённого метода:
Integer nums[] = { 1, 2, 3, 4, 5 }; Integer nums2[] = { 1, 2, 3, 4, 5 }; // Аргументы типа Т и V неявно определяются при вызове метода if (arraysEqual(nums, nums)) System.out.println("nums эквивалентен nums");Для вызова используется обычный синтаксис, не требующий указания аргументов типа. Дело в том, что типы аргументов распознаются автоматически, соответственно определяя типы Т и V. Например, в приведённом выше вызове типом первого аргумента является Integer, который и подставляется вместо типа T. Таким же является и тип второго аргумента, а следовательно, тип параметра V также заменяется на Integer. Таким образом, выражение для вызова метода arraysEqual() составлено корректно, и сравнение массивов между собой может быть выполнено.
Рассмотрим подробнее исходный код метода arraysEqual(). Прежде всего взгляните на ero объявление.
static <T extends Comparable<T>, V extends T> boolean arraysEqual(T[] x, V[] y) { // ... }Параметры типа объявляются перед возвращаемым типом. Также обратите внимание на то, что Т наследует интерфейс Comparable<T>. Интерфейс Comparable определен в пакете java.lang. Класс, реализующий интерфейс Comparable, определяет упорядочиваемые объекты. Таким образом, установление Comparable в качестве верхней границы допустимых типов гарантирует, что метод arraysEqual() можно использовать только в отношении объектов, допускающих сравнение. Интерфейс Comparable - обобщенный, и ero параметр типа задает тип сравниваемых объектов.
Чтобы объявить параметр обобщённого типа, укажите имя параметра типа, за которым следует ключевое слово extends и его верхняя граница. Обратите внимание, что в этом контексте extends используется в общем смысле для обозначения либо расширений (как в классах), либо реализаций (как в интерфейсах).
Заметьте, что тип v ограничен сверху типом Т. Следовательно, тип V должен либо совпадать с типом T, либо быть ero подклассом. В силу этого метод arrayEquals() может вызываться лишь с аргументами, которые можно сравнивать между собой.
Синтаксис объявления метода arraysEqual() может быть обобщен.Кроме того, этот метод объявлен как статический и поэтому вызывается без привязки к какому-либо объекту. Однако вы должны понимать, что обобщенные методы могут быть как статическими, так и нестатическими. Никаких ограничений в этом отношении не существует
Ниже приведена общая форма объявления обобщенного метода:
<список_параметров_типа> возвращаемый_тип имя_метода(список параметров) (// ...
Во всех случаях параметры типа разделяются в списке запятыми. В объявлении обобщенного метода этот список предшествует объявлению возвращаемого типа.
Обобщенные конструкторы
Конструктор может быть обобщенным, даже если сам класс не является таковым.
class Summation { private int sum; <Т extends NumЬer> Summation(T arg) { // обобщённый конструктор sum = О; for(int i=O; i <= arg.intValue(); i++) sum += i; } }Независимо от того, какой числовой тип используется, соответствующее значение преобразуется в тип Integer при вызове intValue(), после чего вычисляется требуемая сумма. Таким образом, класс Summation не обязательно объявлять обобщенным - достаточно сделать обобщенным только его конструктор.
в общем аналогично обобщённому методу, только не указывается возвращаемый тип.
Обобщенные интерфейсы
Как уже было показано на примере класса GenericMetodDemo,
static <T extends Comparable<T>, V extends T> boolean arraysEqual(T[] x, V[] y) { ... }обобщенными могут быть не только классы и методы, но и интерфейсы. Использование в этом примере стандартного интерфейса Comparable<T> гарантировало возможность сравнения элементов двух массивов. Разумеется, вы также можете объявить собственный обобщенный интерфейс. Обобщенные интерфейсы объявляются аналогично тому, как объявляются обобщенные классы.
Пример обобщенного интерфейса:
// Подразумевается, что класс, реализующий этот // интерфейс, содержит одно или несколько значений. interface Containment<T> { //В данном случае параметр типа T задает тип объектов содержимого. // Метод contains() проверяет, содержится ли // некоторый элемент в объекте класса, // реализующего интерфейс Containment. boolean contains(T о); }
В нем должен быть объявлен как минимум тот же параметр типа, что и в объявлении интерфейса. Например, такой вариант объявления класса MyClass недопустим:Любой класс, реализующий обобщенный интерфейс, также должен быть обобщённым!
class MyClass implements Containment<T> { // Ошибка!
В данном случае ошибка состоит в том, что в классе MyClass не объявлен параметр типа, а это означает, что передать параметр типа интерфейсу Containrnent невозможно.
Пример реализации обобщенного интерфейса:Класс, реализующий обобщенный интерфейс, может не быть обобщенным только в одном случае: если при объявлении класса для интерфейса указывается конкретный тип.
class MyClass implements Containrnent { // Допустимо // Реализовать интерфейс Containment с помощью массива, // предназначенного для хранения значений. class MyClass<T> implements Containment<T> { Т[] arrayRef; MyClass (Т[] о) { arrayRef = о; } // Реализовать метод contains() puЬlic boolean contains(T о) { for(T х: arrayRef) if (x.equals(o)) return true; return false; } }
* ms: Если создать объект Ob класса MyClass и в конструктор передать массив Integer(ов), то метод "contains(T о)" ,будет работать пока ему будут передовать Integer(ы). Как только будет передан например 9.25 (Double) - будет ошибка. Так как объект Ob является вариантом реализации интерфейса Containment для типа Integer, а значение 9.25 относится к типу Double.
Вас теперь вряд ли удивит, что один или несколько параметров типа, определяемых обобщенным интерфейсом, могут быть ограниченными. Это позволяет указать, какие именно типы данных допустимы для интерфейса. (как и с классами)
Например, если вы хотите ограничить применимость интерфейса Containrnent числовыми типами, то его можно объявить следующим образом:
interface Containment<T extends Number> { //... }Теперь любой класс, реализующий интерфейс Containrnent, должен передавать ему значение типа, удовлетворяющее тем же ограничениям. Например, класс MyClass, реализующий данный интерфейс, должен объявляться следующим образом:
class MyClass<T extends Number> implements Containment<T> { //... }Обратите внимание на то, как параметр типа T объявляется в классе MyClass, а затем передается интерфейсу Containment. На этот раз интерфейсу Containment требуется тип, расширяющий тип NumЬer, поэтому в классе MyClass, реализующем этот интерфейс, должны быть указаны соответствующие ограничения. Если верхняя граница задана в объявлении класса, то нет никакой необходимости указывать ее еще раз после ключевого слова implements. Если же вы попытаетесь это сделать, то компилятор выведет сообщение об ошибке. Например, следующее выражение некорректно и не будет скомпилировано.
// Ошибка! class MyClass<T extends Number> implements Containment<T extends Number> { // ...}Коль скоро параметр типа задан, он просто передается интерфейсу без дальнейших видоизменений.
Ниже приведена общая форма объявления обобщенного интерфейса:
interface имя_интерфейса<список_параметров_типа> { // ... }где список_параметров_типа содержит список параметров, разделенных запятыми.
При реализации обобщенного интерфейса в объявлении класса также должны быть указаны параметры типа. Общая форма объявления класса, реализующего обобщенный интерфейс, приведена ниже.
class имя_класса<список_параметров типа> implements имя_интерфейса<список_параметров_типа>
Базовые типы и унаследованный код
Поскольку в версиях Java, предшествующих JDK 5, поддержка обобщенных типов отсутствовала, необходимо бьvю предпринять меры к тому, чтобы обеспечить совместимость новых программ с унаследованным кодом.
Чтобы облегчить адаптацию существующего кода к обобщениям, Java позволяет использовать обобщенные классы без указания аргументов типа. В результате для класса создается так называемый "сырой" (далее - базовый) тип. Базовые типы совместимы с унаследованным кодом, которому ничего не известно об обобщенных классах. Главный недостаток использования базовых типов заключается в том, что безопасность типов, обеспечиваемая обобщениями, при этом утрачивается.
Ниже приведен пример программы, демонстрирующей использование базового типа:
// Демонстрация использования базового типа class Gen<T> { Т оb; // объявить объект типа Т // Передать конструктору ссылку на объект типа Т Gen(T о) { оb = о; } // Вернуть объект оb Т getоb() { return оb; } }
// Продемонстрировать использование базового типа // Создать объект класса Gen для типа Integer Gen<Integer> iOb = new Gen<Integer>(88); // Создать объект класса Gen для типа String Gen<String> strOb = new Gen<String>("Tecтиpoвaниe обобщений"); // Создать базовый объект класса Gen и передать ему // значение типа Double Gen raw = new Gen(new Double(98.6)); // Здесь требуется приведение типов, так как тип неизвестен double d = (Double) raw.getob(); System.out.println("знaчeниe: " + d);У этой программы имеется ряд интересных особенностей. Прежде всего, базовый тип обобщенного класса Gen создается в следующем объявлении:
Gen raw = new Gen(new Double(98.6));В данном случае аргументы типа не указываются. В итоге создается объект класса Gen, тип T которого заменяется типом Object.
Базовые типы не обеспечивают безопасность типов. Переменной базового типа может быть присвоена ссылка на любой тип объекта класса Gen. Справедливо и обратное: переменной конкретного типа из класса Gen может быть присвоена ссылка на объект класса Gen базового типа. Обе операции потенциально опасны, поскольку они действуют в обход механизма проверки типов, обязательной для обобщений.
Недостаточный уровень безопасности типов демонстрируют примеры в строках кода в конце данной программы, помещенных в комментарии. Рассмотрим их по отдельности.
Сначала проанализируем следующую строку кода:
// int i = (Integer) raw.getob(); // ошибка времени выполненияВ этом операторе присваивания в объекте raw определяется значение переменной оb, которое приводится к типу Integer. Однако в объекте raw содержится не целое число, а значение типа Double. На стадии компиляции этот факт выявить невозможно, поскольку тип объекта raw неизвестен. Следовательно, ошибка возникнет на стадии выполнения программы.
В следующих строках кода ссылка на объект класса Gen базового типа присваивается переменной strOb (предназначенной для хранения ссылок на объекты типа Gen<String>).
strOb = raw; // допустимо, но потенциально неверно // String str = strOb.getob(); // ошибка времени выполненияСамо по себе присваивание синтаксически правильно, но все же сомнительно. Переменная strOb ссылается на объект типа Gen<String>, а следовательно, она должна содержать ссылку на объект, содержащий значение типа String, но после присваивания объект, на который ссылается переменная strOb, содержит значение типа Double. Поэтому, когда во время выполнения программы предпринимается попытка присвоить переменной str содержимое объекта, на который ссылается переменная strOb, возникает ошибка. Причиной ошибки является то, что в этот момент переменная strOb ссылается на объект, содержащий значение типа Double. Таким образом, присваивание ссылки на объект базового типа переменной, ссылающейся на объект обобщенного типа, делается в обход механизма безопасности типов.
В следующих строках кода демонстрируется ситуация, обратная предыдущей.
raw = iOb; // допустимо, но потенциально неверно // d = (Double) raw.getob(); // ошибка времени выполненияВ данном случае ссылка на объект обобщенного типа присваивается переменной базового типа. И это присваивание синтаксически правильно, но приводит к ошибке, возникающей во второй строке кода. В частности, переменная raw указывает на объект, содержащий значение типа Integer, но при приведении типов предполагается, что он содержит значение типа Double. Эту ошибку также нельзя выявить на стадии компиляции, так как она проявляется только на стадии выполнения программы.
В связи с тем что использование базовых типов сопряжено с потенциальными рисками, в подобных случаях компилятор javac выводит так называемые непроверенные предупреждения, указывающие на возможность нарушения безопасности типов. В рассматриваемой программе причиной таких предупреждений являются следующие строки кода.
Gen raw = new Gen(new Double(98.6)); strOb = raw; // Допустимо, но потенциально неверно.В первой строке кода содержится обращение к конструктору класса Gen без указания аргумента типа, что приводит к выдаче компилятором соответствующего предупреждения. При компиляции второй строки предупреждающее сообщение возникнет из-за попытки присвоить переменной, ссьmающейся на объект обобщенного типа, ссылки на объект базового типа.
На первый взгляд может показаться, что предупреждение об отсутствии проверки типов должна порождать и приведенная ниже строка кода, однако этого не происходит.
raw = iOb; // допустимо, но потенциально неверноВ данном случае компилятор не выдает никаких предупреждающих сообщений, потому что такое присваивание не вносит никаких дополнительной потери безопасности типов кроме той, которая уже бьmа привнесена при создании переменной raw базового типа.
Из всего вышесказанного можно сделать следующий вывод: базовыми типами следует пользоваться весьма ограниченно и только в тех случаях, когда унаследованный код объединяется с новым, обобщенным кодом. Базовые типы - лишь вспомогательное средство, необходимое для обеспечения совместимости с унаследованным кодом, и их использования во вновь создаваемом коде следует избегать.
Автоматическое определение аргументов типов компилятором
Начиная с версии JDK 7 для создания экземпляров обобщенного типа предусмотрен сокращенный синтаксис. В качестве примера обратимся к классу TwoGen, представленному в начале:
class TwoGen<T,V> { Т ob1; V ob2; // Передать конструктору класса ссылки на объекты типов Т и V TwoGen(T ol, V о2) { ob1 = ol; ob2 = о2; } // ... }В случае версий Java, предшествующих JDK 7, для создания экземпляра класса TwoGen пришлось бы использовать примерно следующий код:
TwoGen<Integer, String> tgOb = new TwoGen<Integer, String>(42, "testing");Здесь аргументы типа (в данном случае Integer и String) указываются дважды: сначала при объявлении переменной tgOb, а затем при создании экземпляра класса TwoGen с помощью оператора new.
Версия JDK 7 позволяет переписать приведенное выше объявление в следующем виде:
TwoGen<Integer, String> tgOb = new TwoGen<>(42, "testing");Обратите внимание на ту часть кода, в которой создается экземпляр объекта обобщенного типа. Угловые скобки ( < > ), обозначающие пустой список аргументов типа, предписывают компилятору самостоятельно определить типы аргументов, требующиеся конструктору, исходя из контекста (так называемое выведение типов). Главное преимущество такого подхода состоит в том, что он позволяет существенно сократить размер неоправданно громоздких объявлений.
Приведенную выше форму объявления экземпляра класса можно обобщить:
сlаss-name<список_аргументов_типа> имя_переменной = new имя_класса<>(список_аргументов_конструктора);
В подобных случаях список аргументов типа в операторе new должен быть пустым. Как правило, автоматическое выведение типов компилятором возможно и при передаче параметров методам. Так, если объявить в классе TwoGen следующий метод:
boolean isSame(TwoGen<T, V> о) { if (ob1 == о.оb1 && оЬ2 == о.оЬ2) return true; else return false; }то в JDK 7 будет вполне допустим вызов следующего вида:
if (tgOb.isSame(new TwoGen<>(42, "тестирование"))) System.out.println("Coвпaдaют"};В этом случае аргументы типа, которые должны передаваться методу isSame(), опускаются. Их типы могут быть автоматически определены компилятором, а следовательно, их повторное указание было бы излишним.
Возможность использования пустого списка аргументов типа появилась в версии JDK 7, и поэтому в более ранних версиях компилятора Java она недоступна.
Очистка
Механизм очистки действует следующим образом. При компиляции кода, написанного на языке Java, все сведения об обобщенных типах удаляются. Это означает, что параметры типа заменяются верхними границами их типа, а если границы не указаны, то их функции выполняет класс Object. После этого выполняется приведение типов, заданных аргументами типа. Подобная совместимость типов также контролируется компилятором. Это означает, что во время выполнения программы параметры типа просто не существуют. Этот механизм имеет отношение лишь к исходному коду.
Ошибки неоднозначности
Включение в язык обобщенных типов породило новый тип ошибок, от которых приходится защищаться, - НЕОДНОЗНАЧНОСТЬ. Ошибки неоднозначности возникают в тех случаях, когда процесс очистки порождает два на первый взгляд различающихся объявления обобщений, которые разрешаются в один и тот же очищенный тип, что приводит к возникновению конфликта.
Рассмотрим пример, в котором используется перегрузка методов:
// Неоднозначность, вызванная очисткой перегруженных методов class MyGenClass<T, V> { T оb; V оb2; //... // Эти два объявления перегруженных методов порождают // неоднозначность, и поэтому код не компилируется void set (Т о) { оb1 = о; } void set (V о) { оb2 = о; } }Обратите внимание на то, что в классе MyGenClass объявлены два обобщенных типа: T и V. Здесь делается попытка перегрузить метод set() на основе параметров T и V. Это представляется вполне разумным, поскольку типы T и V - разные. Однако здесь возникают два затруднения, связанные с неоднозначностью.
Во-первых, в определении класса MyGenClass ничто не указывает на то, что типы T и V - действительно разные. Например, не является принципиальной ошибкой создание объекта типа MyGenClass так, как показано ниже.
MyGenClass<String, String> obj = new MyGenClass<String, String>()В данном случае типы T и V будут заменены типом String. В результате оба варианта метода set() становятся совершенно одинаковыми, что, безусловно, является ошибкой.
Во-вторых, более серьезное затруднение возникает в связи с тем, что в результате очистки типов оба варианта метода set() преобразуются к следующему виду:
void set (Object o) { // ... }Таким образом, попытке перегрузить метод set() класса MyGenClass присуща неоднозначность. В данном случае вместо перегрузки методов вполне можно использовать два метода с различными именами.
Ограничения в отношении использования обобщений
Существуют некоторые ограничения, которые вы должны учитывать, когда используете обобщения. Эти ограничения касаются создания объектов параметров типа, статических членов, исключений и массивов. Ниже каждое из этих ограничений рассматривается по отдельности.
Невозможность создания экземпляров параметров типа
Создать экземпляр параметра типа невозможно. Рассмотрим в качестве примера следующий класс:
// Невозможно получить экземпляр типа Т class Gen<T> { Т ob; Gen() { ob = new Т(); //Недопустимо!!! } }В данном примере попытка получить экземпляр типа T приводит к ошибке. Причину этой ошибки понять нетрудно: компилятору ничего не известно о типе создаваемого объекта, поскольку тип T является заполнителем, информация о котором удаляется во время КОМПИЛЯЦИИ.
Ограничения статических членов класса
В статическом члене нельзя использовать параметры типа, объявленные в его классе. Так, все объявления статических членов в приведенном ниже классе недопустимы.
class Wrong<T> { // Неверно, поскольку невозможно создать статическую // переменную типа Т static Т оb; // Неверно, поскольку невозможно использовать переменную типа Т // в статическом методе static Т getob() { return оb; } }Несмотря на наличие описанного выше ограничения, допускается объявлять обобщенные статические методы, которые определяют собственные параметры типа, как это было сделано ранее:
static <T extends Comparable<T>, V extends T> boolean getob(T[] x, V[] y) { //... }
Ограничения обобщенных массивов
На массивы обобщенного типа накладываются два существенных ограничения.
- Во-первых, нельзя получить экземпляр массива, тип элементов которого определяется параметром типа.
- И во-вторых, нельзя создать массив обобщенных ссылок на объекты конкретного типа. Оба эти ограничения демонстрируются в приведенном ниже примере:
// Обобщенные типы и массивы class Gen<T extends Number> { Т оb; Т vals[]; // допустимо Gen(T о, Т[] nums) { оb = о; } // Следующее выражение недопустимо // vals = new T[10]; // невозможно создать массив типа Т В данном случае ограничение, налагаемое на массив типа T, состоит в том, что компилятору не известно, какого типа массив следует в действительности создавать. // Однако такой оператор допустим vals = nums; // присвоение ссылки на существующий // массив допускается Это выражение работает, поскольку тип массива, передаваемого конструктору Gen() при создании объекта, известен и совпадает с типом T. } }
class GenArrays { public static void main{String args[]) { Integer n[] = { 1, 2, З, 4, 5 }; Gen<Integer> iOb = new Gen<Integer>(50, n); // Невозможно создать массив обобщенных ссылок // на объекты конкретного типа // Gen<Integer> gens[] = new Gen<Integer>[10]; // Ошибка! // Следующее выражение допустимо Gen<?> gens[] = new Gen<?>[10]; }
Ограничения обобщенных исключений
Обобщенный класс не может расширять класс Throwable. Это означает, что создавать обобщенные классы исключений невозможно.