Что такое обобщения, простой пример реализации в Java

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

Используя обобщения, можно, например, создать единственный класс, который будет автоматически обращаться с разнотипными данными.

Классы, интерфейсы или методы, оперирующие параметризованными типами, называются обобщенными.

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

Краткое содержание статьи:

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

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

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

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

Если у вас имеется некоторый опыт программирования на С++, не спешите делать поспешные выводы о том, как обобщения действуют в Java.

Начнем с простого примера обобщенного класса

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

Ниже приведен результат, выводимый данной программой.

Внимательно проанализируем эту программу. Обратите внимание на объявле­ние класса Gen в следующей строке кода:

где Т обозначает имя параметра типа. Это имя используется в качестве заполнителя вместо которого в дальнейшем подставляется имя конкретного типа, передаваемого классу Gen при создании объекта.

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

Всякий раз, когда объявляется параметр типа, он указывается в угловых скобках. В классе Gen применяется параметр типа, и поэтому он является обобщенным классом, относящимся к так называемому параметризованному типу.

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

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

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

Рассмотрим далее конструктор Gen(). Его код приведен ниже:

Как видите, параметр о имеет тип Т . Это означает, что конкретный тип параметра о определяется с помощью параметра типа Т, передаваемого при создании объекта класса Gen.

А поскольку параметр о и переменная экземпляра ob относятся к типу Т, то они получают одинаковый конкретный тип при создании объекта класса Gen.

Параметр типа Т может быть также использован для указания типа, возвращаемого методом, как показано ниже на примере метода getob(). Объект ob также относится к типу Т, поэтому его тип совместим с типом, возвращаемым методом getob().

Метод showType() отображает тип Т, вызывая метод getName() для объекта типа Class, возвращаемого в результате вызова метода getClass() для объекта ob.

Метод getClass() определен в классе Object, и поэтому он является членом всех классов. Этот метод возвращает объект типа Class, соответствующий типу того класса объекта, для которого он вызывается.

В классе Class определяется метод getName(), возвращающий строковое представление имени класса.

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

Проанализируем это объявление внимательнее. Обратите внимание на то, что тип Integer указан в угловых скобках после слова Gen.

В данном случае Integer — это аргумент типа, который передается в качестве параметра типа Т из класса Gen. Это объявление фактически означает создание версии класса Gen, где все ссылки на тип Т преобразуются в ссылки на тип Integer

Таким образом, в данном объявле­нии объект ob относится к типу Integer и метод getob() возвращает тип Integer.

Прежде чем продолжить дальше, следует сказать, что компилятор Java на са­мом деле не создает разные версии класса Gen или любого другого обобщенно класса.

Теоретически это было бы удобно, но на практике дело обстоит иначе. Вместо этого компилятор удаляет все сведения об обобщенных типах, выполняя необходимые операции приведения типов, чтобы сделать поведение прикладного кода таким, как будто создана конкретная версия класса Gen.

Таким образом, име­ется только одна версия класса Gen, которая существует в прикладной программе. Процесс удаления обобщенной информации об обобщенных типах называется стиранием.

В следующей строке кода переменной iOb присваивается ссылка на экземпляр целочисленной версии класса Gen:

Обратите внимание на то, что, когда вызывается конструктор Gen(), аргумент типа Integer также указывается.

Это необходимо потому, что объект (в данном случае — iOb), которому присваивается ссылка, относится к типу Gen<Integer>.

Следовательно, ссылка, возвращаемая оператором new, также должна относиться к типу Gen<Integer>. В противном случае во время компиляции возникает ошибка.

Например, следующее присваивание вызовет ошибку во время компиляции:

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

Такая проверка типа является одним из основных преимуществ обобщений, потому что она обеспечи­вает типовую безопасность.

В версии JDK 7 появилась возможность упо­треблять сокращенный синтаксис для создания экземпляра обобщенного класса.

Как следует из комментариев к данной программе, в приведенном ниже при­сваивании выполняется автоупаковка для инкапсуляции значения 88 типа int в объекте типа Integer.

Такое присваивание допустимо, поскольку обобщение Gen<Integer> создает конструктор, принимающий аргумент типа Integer. А поскольку предполагается объект типа Integer, то значение 88 автоматически упаковывается в этом объекте.

Разумеется, присваивание может быть написано и явным образом, как показано ниже, но такой его вариант не дает никаких преимуществ.

Затем в данной программе отображается тип объекта ob переменной iOb ( в данном случае — тип Integer ) . А далее получается значение объекта ob в следу­ющей строке:

Метод getob() возвращает обобщенный тип Т, который был заменен на тип Integer при объявлении переменной экземпляра iOb.

Поэтому метод getob() также возвращает тип Integer, который автоматически распаковывается в тип int и присваивается переменной v типа int. Следовательно, тип, возвращаемый методом getob(), нет никакой нужды приводить к типу Integer.

Безусловно, выполнять автоупаковку необязательно, переписав предыдущую строку кода так, как показано ниже. Но автоупаковка позволяет сделать код более компактным.

Далее в классе GenDemo объявляется объект типа Gen<String> следующим об­разом:

В качестве аргумента типа в данном случае указывается тип String, подставля­емый вместо параметра типа Т в обобщенном классе Gen.

Это, по существу, приво­дит к созданию строковой версии класса Gen, что и демонстрируется в остальной части рассматриваемой здесь программы.

Обобщения действуют только со ссылочными типами

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

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

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

Обобщенные типы различаются по аргументам типа

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

Так, если ввести следующую строку кода в предыдущую про­грамму, то при ее компиляции возникнет ошибка:

Несмотря на то что переменные экземпляра iOb и strOb относятся к типу Gen<T>, они являются ссылкам и на разные типы объектов, потому что их пара­метры типов отличаются.

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

Интересное видео для Java программистов по теме: