Что можно типизировать дженериком java
Обобщение типа данных, generic
Начиная с Java 5 появились новые возможности для программирования, к которым следует отнести поддержку обобщенного программирования, названная в Java generic. Эта возможность позволяет создавать более статически типизированный код. Соответственно, программы становятся более надежными и проще в отладке.
generic являются аналогией с конструкцией «Шаблонов»(template) в С++. Ожидалось, что дженерики Java будут похожи на шаблоны C++. На деле оказалось, что различия между generic’ами Java и шаблонами С++ довольно велики. В основном generic в Java получился проще, чем их C++-аналог, однако он не является упрощенной версией шаблонов C++ и имеют ряд значительных отличий. Так, в языке появилось несколько новых концепций, касающихся generic’ов – это маски и ограничения.
Рассмотрим 2 примера без использования и с использованием generic. Пример без использования generic с приведением типа (java casting):
В данном примере программист знает тип данных, размещамый в List’e. Тем не менее, необходимо обратить особое внимание на приведение типа («java casting»). Компилятор может лишь гарантировать, что метод next() вернёт Object, но чтобы обеспечить присвоение переменной типа Integer правильным и безопасным, требуется java casting. Приведение типа не исключает возможности появления ошибки «Runtime Error» из-за невнимательности разработчика.
Возникает вопрос: «Как с этим бороться? Каким образом зарезервировать List для определенного типа данных?». Данную проблему решают дженерики generic. В следующем примере используется generic без приведения типов.
В примере вместо приведения к Integer, был определен тип списка List. В этом заключается существенное отличие, и компилятор может проверить данный тип на корректность во время компиляции во всем коде. Эффект от generic особенно проявляется в крупных проектах: он улучшает читаемость и надежность кода в целом.
Свойства Generics
Объявление generic-класса
Объявить generic-класс совсем несложно. Пример такого объявления :
Пример использования generic-класса GenericSample :
Проблемы реализации generic
1. Wildcard
Рассмотрим процедуру dump, которой в качестве параметров передается Collection для вывода значений в консоль.
Проблема состоит в том что данная реализация кода не эффективна, так как Collection не является полностью родительской коллекцией всех остальных коллекций, грубо говоря Collection имеет ограничения. Для решения этой проблемы используется Wildcard («?»), который не имеет ограничения в использовании, то есть имеет соответствие с любым типом, и в этом его плюсы. И теперь, мы можем вызвать это с любым типом коллекции.
2. Bounded Wildcard
Рассмотрим процедуру draw, которая рисует фигуры, наследующие свойства родителя Shape. Допустим у Shape есть наследник Circle, и его необходимо «изобразить».
Использование позволяет использовать тип Cycle и всех его предков вполоть до Object.
3. Generic метод
Определим процедуру addAll, которая в качестве параметров получает массив данных Object[] и переносит его в коллекцию Collection
Ошибки, возникающие в последних строках связаны с тем, что нельзя просто вставить Object в коллекции неизвестного типа. Способ решения этой проблемы является использование «generic метода«. Для этого перед методом нужно объявить и использовать его.
Но все равно после выполнение останется ошибка в третьей строчке :
Допустим имеется функция, которая находит ближайший объект к точке Glyph из заданной коллекции. Glyph – это базовый тип, и может иметься неограниченное количество потомков этого типа. Также может иметься неограниченное количество коллекций, хранящих элементы, тип которых соответствует одному из этих потомков. Хотелось бы, чтобы функция могла работать со всеми подобными коллекциями, и возвращала элемент, тип которого совпадал бы с типом элемента коллекции, а не приводился к Glyph. Следующий пример не очень удачный:
Функция выглядит неплохо, но, тем не менее, не лишена недостатков. Получается так, что функции можно передать коллекцию любого типа. Это усложняет реализацию функции, порождая необходимость проверки типа элемента. Будет гораздо лучше написать так:
Теперь все встает на свои места, и в функцию можно передать только коллекцию, элементы которой реализуют интерфейс Glyph. generic сделал свое дело, код получился более типобезопасным.
4. Generic-классы
Наследование можно применять и для параметров generic-классов:
Как в методах, так и в классах можно задать более одного базового интерфейса, который должен реализовывать generic-параметр. Это делается при помощи следующего синтаксиса:
В данном примере generic-параметр должен реализовывать не только интерфейс Glyph, но и MoveableGlyph. Ограничений на количество интерфейсов, которые должен реализовывать переданный тип, нет. Но в класс можно передать только один, т.к. в Java нет множественного наследования. Типы в этом списке могут быть generic-типами, но ни один конкретный интерфейс не может появляться в списке более одного раза, даже с разными параметрами:
5. Bounded type argument
Метод копирования из одной коллекции в другую
6. Lower bounded wildcard
Метод нахождения максимума в коллекции
6. Wildcard Capture
Реализация метода Swap в List
Ограничения generic
Невозможно создать массив generic’ов :
Невозможно создать массив generic-классов :
Преобразование типов
В Generics также можно манипулировать с информацией, хранящийся в переменных.
Наследование исключений в generic’ах
Возможность использовать параметр generic-класса или метода в throws позволяет при описании абстрактного метода не ограничивать разработчика, использующего класс или интерфейс, конкретным типом исключения. Но использовать тип, заданный в качестве параметра, в catch-выражениях нельзя.
Необходимо добавить, что тип, переданный в качестве параметра, должен обязательно быть наследником Throwable.
Таким образом, generic-и в Java получились проще и внесли несколько интересных концепций, таких как маски (wildcard) и ограничения, которые, добавили удобство при работе и помогли решить проблемы. Но, как и любое усложнение языка, эти нововведения затрудняют его понимание и изучение. Появление generic-ов сделало язык Java более выразительным и строгим; такие изменения только на пользу.
Дженерики (Java, обучающая статья)
Предисловие
За основу данной статьи была взята информация из 6-ой главы книги «Oracle Certified Professional Java SE 7 Programmers Exams 1Z0-804 and 1Z0-805». Она была немного изменена (кое-где обрезана, а кое-где дополнена с помощью Google и Википедии). Здесь показаны далеко не все нюансы дженериков — для более подробной информации следует обратиться к официальной документации. Приятного прочтения.
Введение
Обобщённое программирование — это такой подход к описанию данных и алгоритмов, который позволяет их использовать с различными типами данных без изменения их описания. В Java, начиная с версии J2SE 5.0, добавлены средства обобщённого программирования, синтаксически основанные на C++. Ниже будут рассматриваться generics (дженерики) или > — подмножество обобщённого программирования.
Допустим мы ничего не знаем о дженериках и нам необходимо реализовать специфический вывод на консоль информации об объектах различного типа (с использованием фигурных скобок).
Ниже пример реализации:
В вышеприведённом коде была допущена ошибка, из-за которой на консоли мы увидим следующее:
Теперь на время забудем об этом примере и попробуем реализовать тот же функционал с использованием дженериков (и повторим ту же ошибку):
Самое существенное отличие (для меня) в том, что при ошибке, аналогичной предыдущей, проблемный код не скомпилируется:
Думаю, многие согласятся, что ошибка компиляции «лучше» ошибки времени выполнения, т.к. чисто теоретически скомпилированный код с ошибкой может попасть туда, куда ему лучше бы и не попадать. Это очевидное достоинство дженериков. Теперь подробнее рассмотрим конструкции, относящиеся к дженерикам в этом примере. Для того, чтобы код скомпилировался, достаточно заменить строку
Посмотрим на декларацию BoxPrinter:
После имени класса в угловых скобках » » указано имя типа «Т», которое может использоваться внутри класса. Фактически Т – это тип, который должен быть определён позже (при создании объекта класса).
Внутри класса первое использование T в объявлении поля:
Здесь объявляется переменная дженерик-типа (generic type), т.о. её тип будет указан позже, при создании объекта класса BoxPrinter.
В main()-методе происходит следующее объявление:
Здесь указывается, что Т имеет тип Integer. Грубо говоря, для объекта value1 все поля Т-типа его класса BoxPrinter становятся полями типа Integer (private Integer val;).
Ещё одно место, где используется T:
Как и в декларации val с типом Т, вы говорите, что аргумент для конструктора BoxPrinter имеет тип T. Позже в main()-методе, когда будет вызван конструктор в new, указывается, что Т имеет тип Integer:
Теперь, внутри конструктора BoxPrinter, arg и val должны быть одного типа, так как оба имеют тип T. Например следующее изменение конструктора:
приведёт к ошибке компиляции.
Последнее место использования Т в классе – метод getValue():
Тут вроде тоже всё ясно – этот метод для соответствующего объекта будет возвращать значение того типа, который будет задан при его (объекта) создании.
При создании дженерик-классов мы не ограничены одним лишь типом (Т) – их может быть несколько:
Нет ограничений и на количество переменных с использующих такой тип:
Алмазный синтаксис (Diamond syntax)
Вернёмся немного назад к примеру со строкой кода:
Если типы не будут совпадать:
То мы получим ошибку при компиляции:
Немного лениво каждый раз заполнять типы и при этом можно ошибиться. Чтобы упростить жизнь программистам в Java 7 был введён алмазный синтаксис (diamond syntax), в котором можно опустить параметры типа. Т.е. можно предоставить компилятору определение типов при создании объекта. Вид упрощённого объявления:
Следует обратить внимание, что возможны ошибки связанные с отсутствием «<>» при использовании алмазного синтаксиса
В случае с примером кода выше мы просто получим предупреждение от компилятора, Поскольку Pair является дженерик-типом и были забыты «<>» или явное задание параметров, компилятор рассматривает его в качестве простого типа (raw type) с Pair принимающим два параметра типа объекта. Хотя такое поведение не вызывает никаких проблем в данном сегменте кода, это может привести к ошибке. Здесь необходимо пояснение понятия простого типа.
Посмотрим на вот этот фрагмент кода:
Теперь посмотрим на вот этот:
По результатам выполнения оба фрагмента аналогичны, но у них разная идея. В первом случае мы имеем место с простым типом, во вторым – с дженериком. Теперь сломаем это дело – заменим в обоих случаях
Для простого типа получим ошибку времени выполнения (java.lang.ClassCastException), а для второго – ошибку компиляции. В общем, это очень похоже на 2 самых первых примера. Если в двух словах, то при использовании простых типов, вы теряете преимущество безопасности типов, предоставляемое дженериками.
Универсальные методы (Generic methods)
По аналогии с универсальными классами (дженерик-классами), можно создавать универсальные методы (дженерик-методы), то есть методы, которые принимают общие типы параметров. Универсальные методы не надо путать с методами в дженерик-классе. Универсальные методы удобны, когда одна и та же функциональность должна применяться к различным типам. (Например, есть многочисленные общие методы в классе java.util.Collections.)
Рассмотрим реализацию такого метода:
Нам в первую очередь интересно это:
» » размещено после ключевых слов «public» и «static», а затем следуют тип возвращаемого значения, имя метода и его параметры. Такое объявление отлично от объявления универсальных классов, где универсальный параметр указывается после имени класса. Тело метода вполне обычное – в цикле все элементы списка устанавливаются в одно значение (val). Ну и в main()-методе происходит вызов нашего универсального метода:
Стоит обратить внимание на то, что здесь не задан явно тип параметра. Для IntList – это Integer и 100 тоже упаковывается в Integer. Компилятор ставит в соответствие типу Т – Integer.
А сейчас вопрос – какая (-ие) из нижеприведённых строк откомпилируется без проблем?
Ответ с пояснением:
Первый вариант неправильный, т.к. нельзя создавать объект интерфейса.
Во втором случае мы создаем объект типа ArrayList и ссылку на него базового для ArrayList класса. И там, и там дженерик-тип одинаковый – всё правильно.
В третьем и четвёртом случае будет иметь ошибка компиляции, т.к. дженерик-типы должны быть одинаковыми (связи наследования здесь никак не учитываются).
Условие одинаковости дженерик-типов может показаться не совсем логичным. В частности хотелось бы использовать конструкцию под номером 3. Почему же это не допускается?
Будем думать от обратного – допустим 3-ий вариант возможен. Рассмотрим такой код:
Wildcards (Маски)
Сейчас будут рассмотрены Wildcard Parameters (wildcards). Этот термин в разных источниках переводится по-разному: метасимвольные аргументы, подстановочные символы, групповые символы, шаблоны, маски и т.д. В данной статье я буду использовать «маску», просто потому, что в ней меньше букв…
Как было написано выше вот такая строка кода не скомпилируется:
Но есть возможность похожей реализации:
Под маской мы будем понимать вот эту штуку – » «.
А сейчас пример кода использующего маску и пригодного к компиляции:
Метод printList принимает список, для которого в сигнатуре использована маска:
И этот метод работает для списков с различными типами данных (в примере Integer и String).
Однако вот это не скомпилируется:
И ещё один маленький пример:
Тут не возникнет проблем компиляции. Однако нехорошо, что переменная numList хранит список со строками. Допустим нам нужно так объявить эту переменную, чтобы она хранила только списки чисел. Решение есть:
Данный код не скомпилируется, а всё из-за того, что с помощью маски мы задали ограничение. Переменная numList может хранить ссылку только на список, содержащий элементы унаследованные от Number, а всё из-за объявления: List numList. Тут мы видим, как маске задаётся ограничение – теперь numList предназначен для списка с ограниченным количеством типов. Double как и Integer наследуется от Number, поэтому код приведённый ниже скомпилируется.
То, что было описано выше называется ограниченными масками (Bounded wildcards). Применение таких конструкций может быть весьма красивым и полезным. Допустим нам необходимо посчитать сумму чисел различного типа, которые хранятся в одном списке:
Double-тип был использован для переменной result т.к. он без проблем взаимодействует с другими числовыми типами (т.е. не будет проблем с приведением типов).
На этом все. Надеюсь, данная статья была полезной.
Если Вам понравилась статья, проголосуйте за нее
Голосов: 175 Голосовать
Что можно типизировать дженериком java
Обобщения или generics (обобщенные типы и методы) позволяют нам уйти от жесткого определения используемых типов. Рассмотрим проблему, в которой они нам могут понадобиться.
Допустим, мы определяем класс для представления банковского счета. К примеру, он мог бы выглядеть следующим образом:
И на первый взгляд мы можем решить данную проблему следующим образом: задать id как поле типа Object, который является универсальным и базовым суперклассом для всех остальных типов:
Проблема может показаться искуственной, так как в данном случае мы видим, что в конструктор передается строка, поэтому мы вряд ли будем пытаться преобразовывать ее к типу int. Однако в процессе разработки мы можем не знать, какой именно тип представляет значение в id, и при попытке получить число в данном случае мы столкнемся с исключением java.lang.ClassCastException.
Писать для каждого отдельного типа свою версию класса Account тоже не является хорошим решением, так как в этом случае мы вынуждены повторяться.
Эти проблемы были призваны устранить обобщения или generics. Обобщения позволяют не указывать конкретный тип, который будет использоваться. Поэтому определим класс Account как обобщенный:
После объявления класса мы можем применить универсальный параметр T : так далее в классе объявляется переменная этого типа, которой затем присваивается значение в конструкторе.
Используем данный класс:
Например, первый объект будет использовать тип String, то есть вместо T будет подставляться String:
В этом случае в качестве первого параметра в конструктор передается строка.
А второй объект использует тип int (Integer):
Обобщенные интерфейсы
Интерфейсы, как и классы, также могут быть обобщенными. Создадим обобщенный интерфейс Accountable и используем его в программе:
При реализации подобного интерфейса есть две стратегии. В данном случае реализована первая стратегия, когда при реализации для универсального параметра интерфейса задается конкретный тип, как например, в данном случае это тип String. Тогда класс, реализующий интерфейс, жестко привязан к этому типу.
Вторая стратегия представляет определение обобщенного класса, который также использует тот же универсальный параметр:
Обобщенные методы
Кроме обобщенных типов можно также создавать обобщенные методы, которые точно также будут использовать универсальные параметры. Например:
Особенностью обобщенного метода является использование универсального параметра в объявлении метода после всех модификаторов и перед типом возвращаемого значения.
Затем внутри метода все значения типа T будут представлять данный универсальный параметр.
При вызове подобного метода перед его именем в угловых скобках указывается, какой тип будет передаваться на место универсального параметра:
Использование нескольких универсальных параметров
Мы можем также задать сразу несколько универсальных параметров:
Обобщенные конструкторы
Конструкторы как и методы также могут быть обобщенными. В этом случае перед конструктором также указываются в угловых скобках универсальные параметры:
В данном случае конструктор принимает параметр id, который представляет тип T. В конструкторе его значение превращается в строку и сохраняется в локальную переменную.
Пришел, увидел, обобщил: погружаемся в 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