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

Generic типы: параметризованный или обобщенный код




Generic типы: параметризованный или обобщенный код

После выхода в 1995 г. первоначальной версии 1. 0 в язык Java было добавлено множество новых средств. Одним из наиболее значительных и влиятельных новшеств стали обобщения (generics). Во-первых, их появление означало добавление новых синтаксических элементов в язык. Во-вторых, они повлекли за собой изменения во многих классах и методах самого API ядра.

Вероятно, одно из средств Java, которое в наибольшей степени испытало влияние обобщений – это каркас коллекций (Collections Framework).

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

По сути дела, обобщения — это параметризованные типы. Параметризованные типы важны, поскольку позволяют объявлять классы, интерфейсы и методы, где тип данных, которыми они оперируют, указан в виде параметра. Используя обобщения, можно создать единственный класс, например, который будет автоматически работать с разными типами данных. Классы, интерфейсы или методы, имеющие дело с параметризованными типами, называются обобщениями, обобщенными классами или обобщенными методами.

Важно понимать, что Java всегда предлагала возможность создавать в определенной мере обобщенные классы, интерфейсы и методы, оперирующие ссылками на тип Object.

Поскольку Object — это суперкласс для всех остальных классов, ссылка на Object может обращаться к объекту любого типа. То есть в старом коде обобщенные классы, интерфейсы и методы использовали ссылки на Object для того, чтобы оперировать объектами различного типа. Проблема была в том, что они не могли обеспечить безопасность типов.

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

То есть обобщения расширили ваши возможности повторного использования кода и позволили вам делать это легко и безопасно.

Предупреждение для программистов C++: хотя обобщения похожи на шаблоны в C++, это не одно и то же. Существует ряд фундаментальных отличий между двумя подходами к обобщенным типам. Если у вас имеется опыт применения C++, важно не делать поспешных выводов о том, как обобщения работают в Java.

Простой пример обобщения

Давайте начнем с простого примера обобщенного класса. В следующей программе определены два класса. Первый — это обобщенный класс Gen, а второй — GenDemo, использующий Gen.

// Простой обобщенный класс.

// Здесь T — это параметр типа,

// который будет заменен реальным типом

// при создании объекта типа Gen.

class Gen< T> {

T ob; // объявление объекта типа T

// Передать конструктору ссылку

// на объект типа T.

Gen(T o) {

ob = o;

}

BookNew_JAVA-7. indb 340 02. 06. 2007 1: 07: 11

Глава 14. Обобщения 341

// Вернуть ob.

T getob() {

return ob;

}

// Показать тип T.

void showType() {

System. out. println(" Типом T является " + ob. getClass(). getName());

}

}

// Демонстрация обобщенного класса.

class GenDemo {

public static void main(String args[]) {

// Создать Gen-ссылку для Integers.

Gen< Integer> iOb;

// Создать объект Gen< Integer> и присвоить

// ссылку на iOb. Отметьте применение автоупаковки

// для инкапсуляции значения 88 в объект Integer.

iOb = new Gen< Integer> (88);

// Показать тип данных, используемый iOb.

iOb. showType();

// Получить значение iOb. Обратите внимание,

// что никакого приведения не нужно.

int v = iOb. getob();

System. out. println(" значение: " + v);

System. out. println();

// Создать объект Gen для String.

Gen< String> strOb = new Gen< String> (" Обобщенный тест" );

// Показать тип данных, используемый strOb.

strOb. showType();

// Получить значение strOb. Опять же

// приведение не требуется.

String str = strOb. getob();

System. out. println(" Значение: " + str);

}

}

Результат работы этой программы:

Типом T является java. lang. Integer

Значение: 88

Типом T является java. lang. String

Значение: Обобщенный тест

Давайте внимательно исследуем эту программу.

Во-первых, обратите внимание на объявление Gen в следующей строке:

class Gen< T> {

Здесь T — имя типа-параметра. Это имя используется в качестве заполнителя, куда будет подставлено имя реального типа, переданного Gen при создании реальных типов.

То есть T применяется в Gen всякий раз, когда требуется тип-параметр. Обратите внимание, что T заключен в < >. Этот синтаксис может быть обобщен. Всякий раз, когда объявляется тип-параметр, он указывается в угловых скобках. Поскольку Gen применяет тип-параметр, Gen является обобщенным классом, который называется также параметризованным типом.

Далее T используется для объявления объекта по имени ob, как показано ниже:

T ob; // объявляет объект типа T

Как объяснялось, T — это место подстановки реального типа, который будет указан при создании объекта Gen. То есть ob будет объектом типа, переданного в T. Например, если T передан тип String, то экземпляр ob будет типа String.

Теперь рассмотрим конструктор Gen:

Gen(T o) {

ob = o;

}

Как видите, параметр o имеет тип T. Это значит, что реальный тип o определяется типом, переданным T при создании объекта Gen. К тому же, поскольку и переменнаяпараметр o, и переменная-член ob имеют тип T, они обе получают одинаковый реальный тип при создании Gen.

Параметр типа T также может быть использован для спецификации типа возврата для метода, как в случае метода getob(), показанного здесь:

T getob() {

return ob;

}

Так как ob тоже имеет тип T, его тип совместим с типом, возвращаемым getob().

Метод showType() отображает тип T вызовом getName() на объекте Class, возвращенным вызовом getClass() на ob. Метод getClass() определен в Object, и потому является членом всех классов. Он возвращает объект Class, соответствующий типу класса объекта, на котором он вызван. Class определяет метод getName(), который возвращает строковое представление имени класса.

Класс GenDemo демонстрирует обобщенный класс Gen. Сначала он создает версию Gen для целых, как показано ниже:

Gen< Integer> iOb;

Посмотрим на это объявление внимательней. Во-первых, отметим, что тип Integer специфицирован в угловых скобках после Gen. В этом случае Integer — это тип-аргумент, который передается в параметре типа Gen, T. Это эффективно создает версию Gen, в которой все ссылки на T транслируются в ссылки на Integer. То есть в данном объявлении ob имеет тип Integer и тип возврата getob() также имеет тип Integer.

Прежде чем двигаться дальше, необходимо заявить, что компилятор Java на самом деле не создает различные версии Gen или любого другого обобщенного класса. Хотя было бы удобно думать в таких терминах, на самом деле подобное не происходит. Вместо этого компилятор удаляет всю обобщенную информацию о типах, выполняя необходимые приведения, чтобы сделать поведение вашего кода таким, будто создана специфическая версия Gen. То есть имеется только одна версия Gen, которая существует в вашей программе. Процесс удаления обобщенной информации о типе называется очисткой (erasure), и мы вернемся к этой теме чуть позднее в настоящей главе. Следующая строка присваивает iOb ссылку на экземпляр Integer-версию класса Gen:

iOb = new Gen< Integer> (88);

Отметим, что когда вызывается конструктор Gen, аргумент типа Integer также указывается. Это необходимо, потому что типом объекта (в данном случае iOb), которому присваивается ссылка, является Gen< Integer>. То есть ссылка, возвращаемая new, также должна иметь тип Gen< Integer>. Если это не так, получается ошибка времени компиляции. Например, следующее присваивание вызовет ошибку компиляции:

iOb = new Gen< Double> (88. 0); // Ошибка!

Поскольку iOb имеет тип Gen< Integer>, он не может быть использован для присваивания ссылки типа Gen< Double>. Эта проверка типа является одним из основных преимуществ обобщений, потому что обеспечивает безопасность типов.

Как указано в комментарии к программе, присваивание

iOb = new Gen< Integer> (88);

, использует автоупаковку для инкапсуляции значения 88, имеющего тип int, в Integer. Это работает, потому что Gen< Integer> создает конструктор, принимающий аргумент Integer. Поскольку ожидается Integer, Java автоматически упаковывает 88 внутрь него. Конечно, присваивание также может быть написано явно, как здесь:

iOb = new Gen< Integer> (new Integer(88));

Однако с этой версией не связано никаких преимуществ.

Программа затем отображает тип ob внутри iOb, который есть Integer. Далее программа получает значение ob в следующей строке:

int v = iOb. getob();

Поскольку возвращаемым типом getob() будет T, который заменяется на Integer при объявлении iOb, то возвращаемым типом getob() также будет Integer, который автоматически распаковывается в int и присваивается переменной v, имеющей тип int. То есть нет никакой необходимости приводить тип возвращаемого значения getob() к Integer. Конечно, использовать автоупаковку не обязательно. Предыдущая строка может быть написана так:

int v = iOb. getob(). intValue();

Однако автоупаковка позволяет сделать код более компактным.

Далее в GenDemo объявляется объект типа Gen< String>:

Gen< String> strOb = new Gen< String> (" Обобщенный тест" );

Поскольку типом-аргументом является String, String подставляется вместо T внутри Gen. Это создает (концептуально) String-версию Gen, что и демонстрируют остальные строки программы.

Когда объявляется экземпляр обобщенного типа, аргумент, переданный в качестве параметра типа, должен быть типом класса. Вы не можете использовать примитивный тип вроде int или char. Например, Gen можно передать любой тип класса в T, но нельзя передать примитивный тип в качестве параметра типа. Таким образом, следующее объявление недопустимо:

Gen< int> strOb = new Gen< int> (53); // Ошибка, нельзя использовать

// примитивные типы

Конечно, невозможность использовать примитивный тип не является серьезным ограничением, так как вы можете применять оболочки типов (как это и делается в предыдущем примере) для инкапсуляции примитивных типов. Более того, механизм автоупаковки и автораспаковки Java делает использование оболочек типов прозрачным.

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

iOb = strOb; // Не верно!

Даже несмотря на то, что iOb и strOb имеют тип Gen< T>, они являются ссылками на разные типы, потому что типы их параметров отличаются. Это часть того способа, благодаря которому обобщения добавляют безопасность типов и предотвращают ошибки.

Теперь вы можете задать себе следующий вопрос: если та же функциональность, которую мы обнаружили в обобщенном классе Gen, может быть достигнута без обобщений, т. е. простым указанием Object в качестве типа данных и применением правильных приведений, в чем же выгода от того, что класс Gen параметризован? Ответ: в том, что обобщения автоматически гарантируют безопасность типов во всех операциях, где задействован Gen. В процессе работы с ним исключается необходимость явного приведения и ручной проверки типов в коде.

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

// NonGen — функциональный эквивалент Gen

// не использующий обобщений.

class NonGen {

Object ob; // ob теперь имеет тип Object

// Передать конструктору ссылку на объект типа Object

NonGen(Object o) {

ob = o;

}

// Вернуть тип Object.

Object getob() {

return ob;

}

// Показать тип ob.

void showType() {

System. out. println(" Типом ob является " +

ob. getClass(). getName());

}}

// Демонстрация необобщенного класса.

class NonGenDemo {

public static void main(String args[]) {

NonGen iOb;

// Создать объект NonGen и сохранить

// Integer в нем. Автоупаковка используется.

iOb = new NonGen(88);

BookNew_JAVA-7. indb 344 02. 06. 2007 1: 07: 12

Глава 14. Обобщения 345

// Показать тип данных, используемый iOb.

iOb. showType();

// Получить значение iOb.

// На этот раз приведение необходимо.

int v = (Integer) iOb. getob();

System. out. println(" значение: " + v);

System. out. println();

// Создать другой объект NonGen и

// сохранить в нем String.

NonGen strOb = new NonGen(" Тест без обобщений" );

// Показать тип данных, используемый strOb.

strOb. showType();

// Получить значение strOb.

// Опять же — приведение необходимо.

String str = (String) strOb. getob();

System. out. println(" Значение: " + str);

// Это компилируется, но концептуально неверно!

iOb = strOb;

v = (Integer) iOb. getob(); // ошибка времени выполнения!

}}

В этой версии программы присутствует несколько интересных моментов. Для начала NonGen заменяет все обращения к типу T на Object. Это дает NonGen возможность хранить объекты любого типа, как это делает и обобщенная версия. Однако это не дает возможности Java-компилятору иметь какую-то реальную информацию о типе данных, в действительности сохраняемых в NonGen, что плохо по двум причинам. Во-первых, для извлечения сохраненных данных требуется явное приведение. Во-вторых, многие ошибки несоответствия типов не могут быть обнаружены до времени выполнения. Рассмотрим каждую из этих проблем поближе.

Обратите внимание на строку:

int v = (Integer) iOb. getob();

Поскольку типом возврата getob() является Object, необходимо привести его к Integer, чтобы позволить выполнить автораспаковку и сохранить значение в v. Если убрать приведение, программа не скомпилируется. В версии с обобщением приведение происходит неявно. В версии без обобщения приведение должно быть явным. Это не только неудобство, но также потенциальный источник ошибок.

Теперь рассмотрим следующую кодовую последовательность в конце программы:

// Это компилируется, но концептуально неверно!

iOb = strOb;

v = (Integer) iOb. getob(); // ошибка времени выполнения!

Здесь strOb присваивается iOb. Однако strOb ссылается на объект, содержащий строку, а не целое число. Это присваивание синтаксически корректно, потому что все ссылки NonGen одинаковы, и любая ссылка NonGen может указывать на любой другой объект типа NonGen. Однако этот оператор семантически неверен, что и отражено в следующей строке. Здесь тип возврата getob() приводится к Integer, и затем делается попытка присвоить это значение v. Проблема в том, что iOb теперь ссылается на объект, который хранит String, а не Integer. К несчастью, без использования обобщений компилятор Java не имеет возможности обнаружить это. Вместо этого возбуждается исключение времени выполнения. Возможность создавать безопасный в отношении типов код, в котором ошибки несоответствия типов перехватываются компилятором — это главная выгода от обобщений. Хотя использование ссылок на Object для создания “псевдо-обобщенного” кода всегда возможно, нужно помнить, что такой код не является безопасным в отношении типов, и злоупотребление им приводит к исключениям времени выполнения.

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

Поделиться:





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



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