Далее тип T используется для объявления объекта по имени ob:
Вместо T подставится реальный тип, который будет указан при создании объекта класса Gen. Объект ob будет объектом типа, переданного в параметре типа T. Если в параметре T передать тип String, то экземпляр ob будет иметь тип String.
Рассмотрим конструктор Gen().
Параметр o имеет тип T. Это значит, что реальный тип параметра o определяется типом, переданным параметром типа T при создании объекта класса Gen.
Параметр типа T также может быть использован для указания типа возвращаемого значения метода:
Как использовать обобщённый класс. Можно создать версию класса Gen для целых чисел:
В угловых скобках указан тип Integer, т.е. это аргумент типа, который передаётся в параметре типа T класса Gen. Фактически мы создаём версию класса Gen, в которой все ссылки на тип T становятся ссылками на тип Integer.
Когда мы присваиваем ссылку на экземпляр, то угловые скобки также требуется указывать.
Полная версия записи может быть такой:
Но такая запись избыточна, так как можно использовать автоматическую упаковку значения 77 в нужный формат.
Аналогично, можно было бы использовать вариант без автоупаковки для получения значения:
Обобщения работают только с объектами. Поэтому нельзя использовать в качестве параметра элементарные типы вроде int или char:
Использование обобщений автоматически гарантирует безопасность типов во всех операциях, где они задействованы. Это очень мощный механизм, широко используемый в Java.
Обобщённый класс с двумя параметрами
Можно указать два и более параметров типа через запятую.
Обобщённые методы
Никто не запрещает создавать и методы с параметрами и возвращаемыми значениями в виде обобщений.
Шаблоны аргументов
Шаблон аргументов указывается символом ? и представляет собой неизвестный тип.
По сути, вопрос заменяет Object и мы можем использовать любой класс, который в любом случае будет происходить от Object.
Мы можем ограничить диапазон объектов, указав суперкласс.
Пример обобщённого метода
Переменная типа вводится после модификаторов и перед возвращаемым типом.
Отдельно стоит упомянуть новинку JDK 7, позволяющую сократить код.
Во второй строке используются только угловые скобки, без указания типов.
Помните, что нельзя создать экземпляр типа параметра.
Под Android вам часто придётся использовать списочный массив, который использует обобщещние.
В Java 7, которую можно использовать в проектах для KitKat и выше, существует укороченная запись, когда справа не указывается тип (см. выше). Компилятор сам догадается по левой части выражения.
К наиболее важным новшествам версии языка J2SE 5 можно отнести появление параметризованных (generic) классов и методов, позволяющих использовать более гибкую и в то же время достаточно строгую типизацию, что особенно важно при работе с коллекциями. Применение generic-классов для создания типизированных коллекций будет рассмотрено в главе «Коллекции». Параметризация позволяет создавать классы, интерфейсы и методы, в которых тип обрабатываемых данных задается как параметр.
Приведем пример generic-класса с двумя параметрами:
/*пример # 9 : объявление класса с двумя параметрами :Subject.java*/
publicclass Subject <
public Subject(T2 ids, T1 names) <
Здесь T1, Т2 – фиктивные объектные типы, которые используются при объявлении членов класса и обрабатываемых данных. При создании объекта компилятор заменит все фиктивные типы на реальные и создаст соответствующий им объект. В качестве параметров классов запрещено применять базовые типы.
Объект класса Subject можно создать, например, следующим образом:
new Subject (ch, 71D );
В объявлении sub2 имеет место автоупаковка значения 71D в Double.
Ниже приведен пример параметризованного класса Optional с конструкторами и методами, также инициализация и исследование поведения объектов при задании различных параметров.
/*пример # 10 : создание и использование объектов параметризованного
publicclass Optional <
public Optional(T value) <
publicvoid setValue(T val) <
public String toString() <
return value.getClass().getName() + » » + value;
publicstaticvoid main(String[] args) <
int v1 = ob1.getValue();
String v2 = ob2.getValue();
//ob1 =ob2; //ошибка компиляции – параметризация не ковариантна
Optional ob3 = new Optional();
ob3.setValue(«Java SE 6»);
тип объекта, а не тип параметризации */
В результате выполнения этой программы будет выведено:
java.lang.StringJava SE 6
В рассмотренном примере были созданы объекты типа Optional: ob1 на основе типа Integer и ob2 на основе типа String при помощи различных конструкторов. При компиляции вся информация о generic-типах стирается и заменяется для членов класса и методов заданными типами или типом Object, если параметр не задан, как для объекта ob3. Такая реализация необходима для обеспечения совместимости с кодом, созданным в предыдущих версиях языка.
Чтобы расширить возможности параметризованных членов класса, можно ввести ограничения на используемые типы при помощи следующего объявления класса:
public classOptionalExt <
Такая запись говорит о том, что в качестве типа Т разрешено применять только классы, являющиеся наследниками (суперклассами) класса Tип, и соответственно появляется возможность вызова методов ограничивающих (bound) типов.
Часто возникает необходимость в метод параметризованного класса одного допустимого типа передать объект этого же класса, но параметризованного другим типом. В этом случае при определении метода следует применить метасимвол ?. Метасимвол также может использоваться с ограничением extends для передаваемого типа.
/*пример # 11 : использование метасимвола в параметризованном классе:Mark.java,Runner.java*/
public Mark(T value) <
/*вместо*/ // public boolean sameAny(Mark ob) <
publicboolean sameAny(Mark ob) <
return roundMark() == ob.roundMark();
publicboolean same(Mark ob) <
return getMark() == ob.getMark();
publicstaticvoid main(String[] args) <
Mark md = new Mark (71.4D);//71.5d
Mark mi = new Mark (71);
В результате будет выведено:
Метод sameAny(Markob) может принимать объекты типа Mark, инициализированные любым из допустимых для этого класса типов, в то время как метод с параметром Mark мог бы принимать объекты с инициализацией того же типа, что и вызывающий метод объект.
Для generic-типов существует целый ряд ограничений. Например, невозможно выполнить явный вызов конструктора generic-типа:
так как компилятор не знает, какой конструктор может быть вызван и какой объем памяти должен быть выделен при создании объекта.
По аналогичным причинам generic-поля не могут быть статическими, статические методы не могут иметь generic-параметры или обращаться к generic-полям, например:
Пришел, увидел, обобщил: погружаемся в Java Generics
Java Generics — это одно из самых значительных изменений за всю историю языка Java. «Дженерики», доступные с Java 5, сделали использование Java Collection Framework проще, удобнее и безопаснее. Ошибки, связанные с некорректным использованием типов, теперь обнаруживаются на этапе компиляции. Да и сам язык Java стал еще безопаснее. Несмотря на кажущуюся простоту обобщенных типов, многие разработчики сталкиваются с трудностями при их использовании. В этом посте я расскажу об особенностях работы с Java Generics, чтобы этих трудностей у вас было поменьше. Пригодится, если вы не гуру в дженериках, и поможет избежать много трудностей при погружении в тему.
Работа с коллекциями
Предположим, банку нужно подсчитать сумму сбережений на счетах клиентов. До появления «дженериков» метод вычисления суммы выглядел так:
С появлением Generics необходимость в проверке и приведении типа отпала:
Во второй строчке проверки необходимость тоже отпадала. Если потребуется, приведение типов ( casting ) будет сделано на этапе компиляции.
Принцип подстановки
Тип
Подтип
Number
Integer
List
ArrayList
Collection
List
Iterable
Collection
Примеры отношения тип/подтип
Вот пример использования принципа подстановки в Java:
Ковариантность, контравариантность и инвариантность
Но если мы попытаемся изменить содержимое массива через переменную arr и запишем туда число 42, то получим ArrayStoreException на этапе выполнения программы, поскольку 42 является не строкой, а числом. В этом недостаток ковариантности массивов Java: мы не можем выполнить проверки на этапе компиляции, и что-то может сломаться уже в рантайме.
«Дженерики» инвариантны. Приведем пример:
Wildcards
Всегда ли Generics инварианты? Нет. Приведу примеры:
Это ковариантность. List — подтип List
extends B — символ подстановки с указанием верхней границы super B — символ подстановки с указанием нижней границы где B — представляет собой границу
2. Почему нельзя получить элемент из списка ниже?
The Get and Put Principle или PECS (Producer Extends Consumer Super)
Особенность wildcard с верхней и нижней границей дает дополнительные фичи, связанные с безопасным использованием типов. Из одного типа переменных можно только читать, в другой — только вписывать (исключением является возможность записать null для extends и прочитать Object для super ). Чтобы было легче запомнить, когда какой wildcard использовать, существует принцип PECS — Producer Extends Consumer Super.
и Raw типы
Если мы опустим указание типа, например, как здесь:
Если мы попытаемся вызвать параметризованный метода у Raw типа, то компилятор выдаст нам предупреждение «Unchecked call». Если мы попытаемся выполнить присваивание ссылки на параметризованный тип Raw типу, то компилятор выдаст предупреждение «Unchecked assignment». Игнорирование этих предупреждений, как мы увидим позже, может привести к ошибкам во время выполнения нашего приложения.
Wildcard Capture
Попробуем теперь реализовать метод, выполняющий перестановку элементов списка в обратном порядке.
Более подробно о Wildcard Capture можно прочитать здесь и здесь.
Вывод
Переменные типа
Вот еще пример из класса Enum:
Multiple bounds (множественные ограничения)
Вывод
Переменная типа может быть ограничена только сверху одним или несколькими типами. В случае множественного ограничения левая граница (первое ограничение) используется в процессе затирания (Type Erasure).
Type Erasure
На скриншоте ниже два примера программы:
Разница между ними в том, что слева происходит compile-time error, а справа все компилируется без ошибок. Почему?
Reifiable типы
Почему информация об одних типах доступна, а о других нет? Дело в том, что из-за процесса затирания типов компилятором информация о некоторых типах может быть потеряна. Если она потерялась, то такой тип будет уже не reifiable. То есть она во время выполнения недоступна. Если доступна – соответственно, reifiable.
Решение не делать все обобщенные типы доступными во время выполнения — это одно из наиболее важных и противоречивых проектных решений в системе типов Java. Так сделали, в первую очередь, для совместимости с существующим кодом. За миграционную совместимость пришлось платить — полная доступность системы обобщенных типов во время выполнения невозможна.
И еще одна задачка. Почему в примере ниже нельзя создать параметризованный Exception?
Каждое catch выражение в try-catch проверяет тип полученного исключения во время выполнения программы (что равносильно instanceof), соответственно, тип должен быть Reifiable. Поэтому Throwable и его подтипы не могут быть параметризованы.
Unchecked Warnings
Компиляция нашего приложения может выдать так называемый Unchecked Warning — предупреждение о том, что компилятор не смог корректно определить уровень безопасности использования наших типов. Это не ошибка, а предупреждение, так что его можно пропустить. Но желательно все-так исправить, чтобы избежать проблем в будущем.
Heap Pollution
Как мы упомянули ранее, присваивание ссылки на Raw тип переменной параметризованного типа, приводит к предупреждению «Unchecked assignment». Если мы проигнорируем его, то возможна ситуация под названием » Heap Pollution » (загрязнение кучи). Вот пример:
В строке (1) компилятор предупреждает об «Unchecked assignment».
Рассмотрим еще один пример:
Java разрешает выполнить присваивание в строке (1). Это необходимо для обеспечения обратной совместимости. Но если мы попытаемся выполнить метод add в строке (2), то получим предупреждение Unchecked call — компилятор предупреждает нас о возможной ошибке. В самом деле, мы же пытаемся в список строк добавить целое число.
Reflection
Хотя при компиляции параметризованные типы подвергаются процедуре стирания (type erasure), кое-какую информацию мы можем получить с помощью Reflection.
С появлением Generics класс java.lang.Class стал параметризованным. Рассмотрим вот этот код:
Вывод
Если информация о типе доступна во время выполнения программы, то такой тип называется Reifiable. К Reifiable типам относятся: примитивные типы, непараметризованные типы, параметризованные типы с неограниченным символом подстановки, Raw типы и массивы, элементы которых являются reifiable.
Игнорирование Unchecked Warnings может привести к «загрязнению кучи» и ошибкам во время выполнения программы.
Reflection не позволяет получить информацию о типе объекта, если он не Reifiable. Но Reflection позволяет получить информацию о типе возвращаемого методом значения, о типе аргументов метода и о типе полей класса.
Type Inference
Термин можно перевести как «Вывод типа». Это возможность компилятора определять (выводить) тип из контекста. Вот пример кода:
С появлением даймонд-оператора в Java 7 мы можем не указывать тип у ArrayList :
Предположим у нас есть вот такой класс, который описывает связный список:
Результат обобщенного метода List.nil() может быть выведен из правой части:
Механизм выбора типа компилятором показывает, что аргумент типа для вызова List.nil() действительно String — это работает в JDK 7, все хорошо.
Выглядит разумно, что компилятор также должен иметь возможность вывести тип, когда результат такого вызова обобщенного метода передается другому методу в качестве аргумента, например:
В JDK 7 мы получили бы compile-time error. А в JDK 8 скомпилируется. Это и есть первая часть JEP-101, его первая цель — вывод типа в позиции аргумента. Единственная возможность осуществить это в версиях до JDK 8 — использовать явный аргумент типа при вызове обобщенного метода:
Вторая часть JEP-101 говорит о том, что неплохо бы выводить тип в цепочке вызовов обобщенных методов, например:
Но данная задача не решена до сих пор, и вряд ли в ближайшее время появится такая функция. Возможно, в будущих версиях JDK необходимость в этом исчезнет, но пока нужно указывать аргументы вручную:
После выхода JEP 101 на StackOverflow появилось множество вопросов по теме. Программисты спрашивают, почему код, который выполнялся на 7-й версии, на 8-й выполняется иначе – или вообще не компилируется? Вот пример такого кода:
Посмотрим на байт-код после компиляции на JDK1.8:
А теперь байт-код после компиляции на JDK1.7 – то есть на Java 7:
Чтобы избежать таких проблем, Oracle выпустил руководство по переходу с JDK1.7 на JDK 1.8 в котором описаны проблемы, которые могут возникнуть при переходе на новую версию Java, и то, как эти проблемы можно решить.
Например если вы хотите, чтобы в коде выше после компиляции на Java 8 все работало так же, как и на Java 7, сделайте приведение типа вручную:
Заключение
На этом мой рассказ о Java Generics подходит к концу. Вот другие источники, которые помогут вам в освоении темы:
К наиболее важным новшествам версии языка J2SE 5 можно отнести появление параметризованных (generic) классов и методов, позволяющих использовать более гибкую и в то же время достаточно строгую типизацию, что особенно важно при работе с коллекциями. Применение generic-классов для создания типизированных коллекций будет рассмотрено в главе «Коллекции». Параметризация позволяет создавать классы, интерфейсы и методы, в которых тип обрабатываемых данных задается как параметр.
Приведем пример generic-класса с двумя параметрами:
/*пример # 9 : объявление класса с двумя параметрами :Subject.java*/
publicclass Subject sub =
new Subject (ch, 71D );
В объявлении sub2 имеет место автоупаковка значения 71D в Double.
Ниже приведен пример параметризованного класса Optional с конструкторами и методами, также инициализация и исследование поведения объектов при задании различных параметров.
/*пример # 10 : создание и использование объектов параметризованного
int v1 = ob1.getValue();
String v2 = ob2.getValue();
//ob1 =ob2; //ошибка компиляции – параметризация не ковариантна
Optional ob3 = new Optional();
тип объекта, а не тип параметризации */
В результате выполнения этой программы будет выведено:
java.lang.StringJava SE 6
В рассмотренном примере были созданы объекты типа Optional: ob1 на основе типа Integer и ob2 на основе типа String при помощи различных конструкторов. При компиляции вся информация о generic-типах стирается и заменяется для членов класса и методов заданными типами или типом Object, если параметр не задан, как для объекта ob3. Такая реализация необходима для обеспечения совместимости с кодом, созданным в предыдущих версиях языка.
Чтобы расширить возможности параметризованных членов класса, можно ввести ограничения на используемые типы при помощи следующего объявления класса:
public classOptionalExt ob) ob) ob) ms = new Mark (“7”); //ошибкакомпиляции
Mark md = new Mark (71.4D);//71.5d
Mark mi = new Mark (71);
В результате будет выведено:
Метод sameAny(Markob) может принимать объекты типа Mark, инициализированные любым из допустимых для этого класса типов, в то время как метод с параметром Mark мог бы принимать объекты с инициализацией того же типа, что и вызывающий метод объект.
Для generic-типов существует целый ряд ограничений. Например, невозможно выполнить явный вызов конструктора generic-типа:
class Optional integerMatrix Integer является аргументом типа.
Дженерики работают только с объектами! Следующий код является неправильным:
Т обозначает имя параметра типа. Это имя используется в качестве заполнителя вместо которого в дальнейшем подставляется имя конкретного типа, передаваемого классу Matrix при создании объекта. Это означает, что обозначение Т применяется в классе Matrix всякий раз, когда требуется параметр типа. Всякий раз, когда объявляется параметр типа, он указывается в угловых скобках.
Обобщенные типы отличаются в зависимости от типов-аргументов. Следующий код не допустим:
Обобщенный класс может быть объявлен с любым количеством параметров типа. Например:
2. Ограниченные типы
Указывая параметр типа, можно наложить ограничение сверху в виде верхней границы, где объявляется супер класс, от которого должны быть унаследованы все аргументы типов. С этой целью вместе с параметром указывается ключевое слово extends :
Параметр типа Т может быть заменен только указанным супер классом или его подклассами.
Рассмотрим пример использования ограниченного типа:
В виде ограничения можно накладывать не только тип класса, но и тип интерфейса:
Ограничение может включать в себя как тип класса, так и типы одного или нескольких интерфейсов:
3. Применение метасимвольных аргументов
Представьте, что мы хотим добавить метод для сравнения средних значений массивов в класс Average из примера 3. Причем типы массивов могут быть разные:
Но это не сработает, так как в этом случае метод sameAvg будет принимать аргументы только того же типа, что и существующий объект:
Метасимвольные аргументы могут быть ограничены почти таким же образом, как и параметры типов. Ограничивать метасимвольный аргумент особенно важно при создании обобщенного типа, оперирующего иерархией классов. Например:
4. Параметризованные методы и конструкторы
В методах параметризованного класса можно использовать параметр типа, а следовательно, они становятся параметризованными относительно параметра типа.
Но можно объявить параметризованный метод, в котором непосредственно используется один или несколько параметров типа. Более того, можно объявить параметризованный метод, входящий в не параметризованный класс. Например:
Конструкторы также могут быть обобщенными, даже если их классы таковыми не являются. Например:
5. Параметризованные интерфейсы
В дополнение к обобщенным классам и методам вы можете объявлять параметризованные интерфейсы. Параметризованные интерфейсы специфицируются так же, как и обобщенные классы:
6. Иерархии параметризованных классов
Параметризованные классы могут быть частью иерархии классов так же, как и любые другие не параметризованные классы. То есть параметризованный класс может выступать в качестве супер класса или подкласса.
Ключевое отличие между параметризованными и не параметризованными иерархиями состоит в том, что в параметризованной иерархии любые аргументы типов, необходимые параметризованному супер классу, всеми подклассами должны передаваться по иерархии вверх.
Подкласс параметризованного супер класса необязательно должен быть параметризованным, но в нем все же должны быть, указаны параметры типа, требующиеся его параметризованному супер классу. Подкласс может, если требуется, быть, дополнен и своими параметрами типа. Супер классом для параметризованного класса может быть класс не параметризованный.
7. Использование оператора instanceof c параметризованными классами
8. Ограничения присущие обобщениям
Обобщениям присущи некоторые ограничения. Рассмотрим их:
1. Нельзя создавать экземпляр по параметру типа. Ни обычный объект, ни массив:
2. Нельзя создать массив специфических для типа обобщенных ссылок:
3. Нельзя создавать обобщенные статические переменные и методы. Но объявить статические обобщенные методы со своими параметрами типа все же можно:
Вопрос по дженерикам метода и ограничению переменных типов
Сейчас активно изучаю (или даже разбираю) известное пособие Хорстмена и Корнелла по Java2. В данный момент остановился на параметризации. Дошел до пункта «Обобщенные методы» и следующий за ним «Ограничения переменных типов». И в ближайшем листинге среди прочего кода появляется такая сигнатура:
Я понимаю, что в методе применяется метод compareTo(), принадлежащий интерфейсу Comparable и поэтому мы должны «защитить» наш метод от того, чтобы туда не затесался класс, который не реализует данный интерфейс. Ок, понятно. Но почему бы тогда нельзя было написать:
С другой стороны, возможно имелось в виду, что это не объект класса Pair должен возвращать метод, а объект любого класса, реализующий Comparable? Тогда почему не написать так:
Я не отрицаю, что в пособии вполне может быть указана правильная сигнатура. И если это так, то прошу подробно описать ее. Заранее спасибо за развернутые ответы!
Вы хотите развёрнутых ответов? Их есть у меня! Немного теории по параметризованным методам.
Если метод нестатический, а класс — параметризованный, то параметр типа наследуется из описания класса:
Если метод статический, то унаследовать параметр типа от класса он не может. Это вызвано тем, что параметр типа привязывается к конкретному объекту при его создании, а статический метод не привязан к конкретному объекту, он привязан к классу в целом. В случае статического метода параметр типа нужно указывать непосредственно перед объявлением метода:
В этом случае тип T определяется в момент вызова статического метода по типу передаваемого аргумента.
Ещё один случай, когда может понадобиться явно параметризовать метод — это нестатический метод, параметр типа которого должен отличаться от параметра типа класса:
Если в момент компиляции вычислить тип параметра метода невозможно, необходимо явно указать его при вызове. Такие случаи очень редки, поэтому пример несколько синтетический:
Теперь непосредственно к вашему вопросу. Нельзя просто написать