Обобщения в Java

На протяжении всей истории развития языка Java, он претерпевал изменения. Иногда изменения носили косметический характер, иногда это было просто исправление уязвимостей, а иногда переход на новую версию языка знаменовал поистине значительные, а в некоторых случаях и революционные изменения, одним из таких изменений стали обобщения (generics). Обобщения в Java были представлены в версии 5.0 это было результатом реализации самых первых требований к спецификации Java, которые были сформулированы еще в 1999 году. Они позволили создавать более безопасный и легче читаемый код, который не перегружен переменными типа Object и приведением классов. Вы вероятно уже сталкивались с обобщениями в своих программах сами того не осознавая:

В качестве обобщенного типа в данном случае выступает тип String, который указан в треугольных скобках. Начиная с версии Java 7 SE, обобщенный тип можно опускать в объявлении конструктора класса:

Выше мы упоминали про причины появления обобщений в Java, давайте теперь рассмотрим их более подробно на примере:

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

Так еще нам это не помогло – во время выполнения программы было сгенерировано исключение ClassCastException (переменные с типом StringBuilder нельзя привести к переменным с типом String). Конечно, можно было бы заставить код работать так как нам нужно, добавить дополнительные проверки, комментарии для других программистов и т.д. и выводить в консоль элементы списка, но можно сделать все гораздо проще:

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

Теперь код компилируется без каких-либо проблем и выводит в консоль «Обобщения в Java». Если Вы внимательно посмотрите на код, то увидите, что метод showString() претерпел изменения в частности: в параметрах метода мы явно указали что будем использовать список с переменными строкового типа, что избавило нас от необходимости использовать приведение типов. Прежде чем мы углубимся в обобщенное программирование на Java и приступим к созданию обобщенных классов, я хотел бы вас предостеречь – обобщенное программирование намного сложнее, чем могло показаться из примеров выше и прежде чем использовать его в своем коде, надо иметь о нем очень хорошее и ясное представление. Как мы увидим далее опрометчивое использование обобщенных классов может привести к большому количеству не самых очевидных ошибок и «методом тыка» решить их не получится. Если ваш боевой настрой не упал и вы все еще рветесь постигнуть все нюансы этого невероятно мощного инструмента языка Java, тогда не будем задерживаться 🙂



Обобщенные классы.

Обобщенный класс – это класс с одной или более переменной типа. Для того, чтобы класс стал обобщенным достаточно указать тип обобщения в угловых скобках после названия класса:

Обобщения в Java

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

Класс Music условно реализует музыку и в данный момент не представляет особого интереса, второй класс – Player, реализует наш mp3 плеер, как Вы можете заметить после названия класса в угловых скобках мы объявляем использование обобщенного типа . В классе присутствуют два метода и одна переменная обобщенного типа T song. Метод addSong() мы будем использовать для добавления музыки в плейлист, метод playSong() — для проигрывания музыки, обратите внимание, что метод возвращает обобщенный тип T.

Как видите? музыка добавляется в плейлист нашего проигрывателя и все работает. Обратите внимание на создание объекта класса Player, где мы явно указали какой тип объектов в нем мы будем использовать:

Любые другие объекты кроме класса Music использовать будет нельзя:

Обобщенные классы не ограничены в использовании одного обобщенного параметра:

Нашему классу плеера был добавлен второй обобщенный параметр, который отвечает за жанр музыки, а при создании экземпляра класса Player мы объявили, что вторым параметром будет переменная с типом String:

Обобщенные интерфейсы.

Так же, как и классы интерфейсы могут использовать обобщенные параметры.

Существует несколько вариантов того, как класс может реализовать этот интерфейс. Первый вариант – это конкретизировать обобщенный тип в классе реализующим интерфейс:

Второй вариант – это продолжить использовать обобщения в классе реализующим интерфейс:

И последний способ, он же не самый лучший – это написать код в «старом стиле», т.е. не использовать обобщения в принципе:

При компиляции этого кода вы скорее всего получите предупреждение, но код скомпилируется и будет работать.

Обобщенные методы.

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

В качестве параметра метод использует обобщенный тип T. Возвращает метод обобщенный класс Player. Прежде чем использовать обобщенный параметр его необходимо указать перед возвращаемым типом, что мы и сделали . Пренебрежение последним описанным действием может привести к плачевным результатам:

В объявлении метода не был указан обобщенный тип в угловых скобках, что и привело к ошибке компиляции. Проделаем работу над ошибками:

На этот раз все компилируется прекрасно.



Ограничения на переменные типа.

До этого момента все было достаточно просто и очевидно, вот только из-за этой простоты обобщения могли вам показаться не очень полезными. Настало время постигнуть всю мощь обобщений и вместе с этим приобрести кучу проблем. Не редки ситуации, когда класс или метод нуждается в наложении ограничений на переменные типа.

Ограничивающий параметр (жесткие системы обобщенных типов) – это обобщенный тип, который определяет ограничения, накладываемые на обобщение.

В метод check() мы можем передать только объекты классов реализующих интерфейс А. Обратите внимание, что в объявлении используется ключевое слово extends вместо implements! Как вы знаете в Java отсутствует множественное наследование в явном виде, но мы вольны классом реализовывать столько интерфейсов, сколько посчитаем нужным, так же и в обобщениях ограничивающим параметром можно указать реализацию двух интерфейсов разделив их знаком &:

Теперь метод check() принимает в качестве аргументов объект класса реализующего интерфейсы A и B. Ограничения реализуются не только интерфейсами:

На этот раз класс должен реализовывать интерфейс B и расширять класс A. В последнем примере есть один нюанс – если в качестве ограничителя вы вводите какой-либо класс (в нашем примере класс A), то он должен стоять на первом месте:

Универсальный обобщенный тип или неограниченный подстановочный тип (кому как нравится, лично мне нравится больше первый вариант) – это неизвестный обобщенный тип, представленный знаком вопроса. Варианты его использования представлены в таблице:

Обобщения в Java

Давайте теперь рассмотрим каждый из вариантов более подробно.

Универсальные без ограничений.


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

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

Несмотря на то, что класс String является подклассом Object, компилятор всё равно не дает запустить данный код. Это может показаться запутанным, так оно и есть. Давайте рассмотрим другой пример:

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

На этот раз в методе printList() вместо списка с типом Object мы воспользовались списком с универсальным ограничителем ?, который подразумевает под собой любой тип и тип String полностью соответствует этому условию 🙂

Универсальные с восходящим ограничением (ограничение на подтип).


Предположим, нам надо создать список, который бы позволял хранить в себе переменные подклассов Number (иначе говоря любые числа), из предыдущей главы мы уже разобрались почему так сделать нельзя:

Как и в прошлый раз, на помощь нам придут универсальные ограничители:

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

Есть одна интересная особенность первых двух рассмотренных ограничителей. При их использовании, список становится логически неизменяемым.

Проблема кроется в том, что Java не знает какой тип кроется за <? extends A>, это может быть A или B или класс, который еще не написан. Иначе говоря, переменные класса A не могут быть добавлены в List<B>, а переменные класса B не могут быть добавлены в List<A>, с точки зрения Java оба сценария возможны, поэтому подобные операции пресекаются компилятором на стадии компиляции. Давайте посмотрим, как универсальные ограничители работают с интерфейсами:

Самое важное, на что хотелось бы обратить ваше внимание, так это на метод showLetters(), при объявлении универсальных ограничителей всегда используется ключевое слово extends, не важно с чем вы работаете с классом или интерфейсом.

Универсальные с нисходящим ограничением (ограничение на супер тип).


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

В методе addString() используется ограничитель с нисходящим ограничением , означающий, что мы можем использовать любые классы, которые являются родительскими классу String, так же включая класс String. Почему в методе addString() нельзя воспользоваться другими ограничителями? Рассмотрим этот вопрос подробнее:

  • при использовании универсального ограничителя, метод попросту не скомпилируется, потому что наш список автоматически становится неизменяемым (immutable)
  • метод так же не скомпилируется, по той же самой причине, что и выше, при использовании ограничителей с восходящим ограничением список становится неизменяемым
  • c ограничением только на класс Object в наш метод мы не сможем передать список из строк



Обобщения и JVM.

JVM не работает с обобщениями, все объекты принадлежат обычным классам. Всегда при определении обобщенного типа, автоматически создается соответствующий ему базовый тип. Имя этого типа совпадает с именем обобщенного типа с удаленными параметрами типа. Переменные типа стираются и заменяются ограничивающими типами (или типом Object, если переменная не имеет ограничений). Рассмотрим пример как это работает:

А вот как выглядит базовый тип:

Если T – неограниченная переменная, то она попросту заменяется на Object и мы получаем совершенно обычный класс. С ограничивающими типами дела обстоят интереснее:

Базовый тип:

А если в обобщенном классе поменять местами A и B?

Базовый тип:

А переменная t1 где требуется будет приведена компилятором к типу A. По этой самой причине рекомендуется отмечающий интерфейсы (интерфейсы без методов) указывать в конце списка ограничений.
Давайте посмотрим, как JVM работает с обобщенными выражениями:

Когда в программе вызывается обобщенный метод, компилятор вводит операции привидения типов при стирании возвращаемого типа. При вызове метода a.get() в результате стирания возвращается тип Object, далее компилятор автоматически приводит тип Object к типу класса B.

Ограничения при работе с обращениями.

Что нельзя делать при работе с обобщениями:

  1. Вызывать конструктор иными словами создавать экземпляры переменных типа. Вызов new T() недопустим, потому что во время выполнения программы этот вызов будет заменен на new Object().
  2. Создавать массив из параметризованного типа:

    Что интересно, несмотря на то, что инициализировать массив нельзя, объявить его можно без каких-либо проблем:

  3. Вызвать метод instanceof, потому что все запросы типов во время выполнения дают только базовый тип:

    В примере метод instanceof проверяет является ли переменная obj экземпляром Test любого типа, что конечно же нелогично и недопустимо.

  4. Использовать примитивные типы в качестве обобщений. Иначе говоря, не существует объекта типа Test .
  5. Создавать переменные типа в статическом контексте обобщенных классов:

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

    Кроме того, переменную типа нельзя использовать в блоке catch!

Site Footer