По существу, обобщения — это параметризованные типы. Такие типы важны, поскольку они позволяют объявлять классы, интерфейсы и методы, где тип данных, которыми они оперируют, указан в виде параметра.
Используя обобщения, можно, например, создать единственный класс, который будет автоматически обращаться с разнотипными данными.
Классы, интерфейсы или методы, оперирующие параметризованными типами, называются обобщенными.
Следует заметить, что в Java всегда предоставлялась возможность создавать в той или иной степени обобщенные классы, интерфейсы и методы, оперирующие ссылками типа Object. А поскольку класс Object служит суперклассом для всех остальных классов, то он позволяет обращаться к объекту любого типа.
Краткое содержание статьи:
- Простой пример реализации обощенного класса
- Обобщения действуют только со ссылочными типами
- Обобщенные типы различаются по аргументам типа
- Интересное видео по теме
Следовательно, в старом коде ссылки типа Object использовались в обобщенных классах, интерфейсах или методах с целью оперировать разнотипными объектами. Но дело в том, что они не могли обеспечить типовую безопасность.
Именно обобщения внесли в язык типовую безопасность типов, которой так недоставало прежде. Они также упростили процесс выполнения, поскольку теперь нет нужды в явном приведении типов для преобразования объектов типа Object в конкретные типы обрабатываемых данных.
Благодаря обобщениям все операции приведения типов выполняются автоматически и неявно. Таким образом, обобщения расширили возможности повторного использования кода, позволив делать это легко и безопасно.
Программирующим на С++ следует иметь в виду, что обобщения и шаблоны в С++ это не одно и то же, хотя они и похожи. У этих двух подходов к обобщенным типам есть ряд принципиальных отличий.
Если у вас имеется некоторый опыт программирования на С++, не спешите делать поспешные выводы о том, как обобщения действуют в Java.
Начнем с простого примера обобщенного класса
В приведенной ниже программе определяются два класса. Первый из них — обобщенный класс Gen, второй — демонстрационный класс GenDemo, в котором используется обобщенный класс Gen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
// Простой обобщенный класс. // Здаесь T обозначает параметр типа, // который будет заменен реальным типом // при создаии объекта типа Gen class Gen<T> { T ob; // объявить объект типа T // передать конструктору ссылку на объект типа T Gen(T o) { ob = o; } // возвратить объект ob T getob() { return ob; } // показать типа T void showType() { System.out.println("Типом T является " + ob.getClass().getName()); } } // продемонстрируем применение обобщенного класса class GenDemo { public static void main(String args[]) { // Создать ссылку типа Gen для целых чисел 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 для символьным сток Gen<String> strOb = new Gen<String>("Текст обобщений"); // показать тип данных, хранящихся в переменной strOb strOb.showType(); // получить значение переменной strOb. И в этом случае // приведение типов не требуется String str = strOb.getob(); System.out.println("Значение: " + str); } } |
Ниже приведен результат, выводимый данной программой.
1 2 3 4 5 |
Типом T является java.lang.Integer Значение: 88 Типом T является java.lang.String Значение: Текст обобщений |
Внимательно проанализируем эту программу. Обратите внимание на объявление класса Gen в следующей строке кода:
1 |
class Gen<T> { |
где Т обозначает имя параметра типа. Это имя используется в качестве заполнителя вместо которого в дальнейшем подставляется имя конкретного типа, передаваемого классу Gen при создании объекта.
Это означает, что обозначение Т применяется классе Gen всякий раз, когда требуется параметр типа. Обратите внимание на то, что обозначение Т заключено в угловые скобки ( <> ) . Этот синтаксис может быть обобщен.
Всякий раз, когда объявляется параметр типа, он указывается в угловых скобках. В классе Gen применяется параметр типа, и поэтому он является обобщенным классом, относящимся к так называемому параметризованному типу.
Далее тип Т используется для объявления объекта ob:
1 |
T ob; // объявить объект типа T |
Как упоминалось выше, параметр типа Т — это место для подстановки конкретного типа, который указывается в дальнейшем при создании объекта класса Gen.
Это означает, что объект ob станет объектом того типа, который будет передан в качестве параметра типа Т. Так, если передать тип String в качестве параметра типа Т, то такой экземпляр объекта ob будет иметь тип String.
Рассмотрим далее конструктор Gen(). Его код приведен ниже:
1 2 3 |
Gen (T o) { ob = o; } |
Как видите, параметр о имеет тип Т . Это означает, что конкретный тип параметра о определяется с помощью параметра типа Т, передаваемого при создании объекта класса Gen.
А поскольку параметр о и переменная экземпляра ob относятся к типу Т, то они получают одинаковый конкретный тип при создании объекта класса Gen.
Параметр типа Т может быть также использован для указания типа, возвращаемого методом, как показано ниже на примере метода getob(). Объект ob также относится к типу Т, поэтому его тип совместим с типом, возвращаемым методом getob().
1 2 3 |
T getob() { return ob; } |
Метод showType() отображает тип Т, вызывая метод getName() для объекта типа Class, возвращаемого в результате вызова метода getClass() для объекта ob.
Метод getClass() определен в классе Object, и поэтому он является членом всех классов. Этот метод возвращает объект типа Class, соответствующий типу того класса объекта, для которого он вызывается.
В классе Class определяется метод getName(), возвращающий строковое представление имени класса.
Класс GenDemo служит для демонстрации обобщенного класса Gen. Сначала в нем создается версия класса Gen для целых чисел, как показано ниже.
1 |
Gen<Integer> iOb; |
Проанализируем это объявление внимательнее. Обратите внимание на то, что тип Integer указан в угловых скобках после слова Gen.
В данном случае Integer — это аргумент типа, который передается в качестве параметра типа Т из класса Gen. Это объявление фактически означает создание версии класса Gen, где все ссылки на тип Т преобразуются в ссылки на тип Integer
Таким образом, в данном объявлении объект ob относится к типу Integer и метод getob() возвращает тип Integer.
Прежде чем продолжить дальше, следует сказать, что компилятор Java на самом деле не создает разные версии класса Gen или любого другого обобщенно класса.
Теоретически это было бы удобно, но на практике дело обстоит иначе. Вместо этого компилятор удаляет все сведения об обобщенных типах, выполняя необходимые операции приведения типов, чтобы сделать поведение прикладного кода таким, как будто создана конкретная версия класса Gen.
Таким образом, имеется только одна версия класса Gen, которая существует в прикладной программе. Процесс удаления обобщенной информации об обобщенных типах называется стиранием.
В следующей строке кода переменной iOb присваивается ссылка на экземпляр целочисленной версии класса Gen:
1 |
iOb = new Gen<Integer>(88); |
Обратите внимание на то, что, когда вызывается конструктор Gen(), аргумент типа Integer также указывается.
Это необходимо потому, что объект (в данном случае — iOb), которому присваивается ссылка, относится к типу Gen<Integer>.
Следовательно, ссылка, возвращаемая оператором new, также должна относиться к типу Gen<Integer>. В противном случае во время компиляции возникает ошибка.
Например, следующее присваивание вызовет ошибку во время компиляции:
1 |
iOb = new Gen<Double>(88.3); // Ошибка! |
Переменная iOb относится к типу Gen<Integer>, поэтому она не может быть использована для присваивания ссылки типа Gen<Double>.
Такая проверка типа является одним из основных преимуществ обобщений, потому что она обеспечивает типовую безопасность.
В версии JDK 7 появилась возможность употреблять сокращенный синтаксис для создания экземпляра обобщенного класса.
Как следует из комментариев к данной программе, в приведенном ниже присваивании выполняется автоупаковка для инкапсуляции значения 88 типа int в объекте типа Integer.
1 |
iOb = new Gen<Integer>(88); |
Такое присваивание допустимо, поскольку обобщение Gen<Integer> создает конструктор, принимающий аргумент типа Integer. А поскольку предполагается объект типа Integer, то значение 88 автоматически упаковывается в этом объекте.
Разумеется, присваивание может быть написано и явным образом, как показано ниже, но такой его вариант не дает никаких преимуществ.
1 |
iOb = new Gen<Integer>(new Integer(88)); |
Затем в данной программе отображается тип объекта ob переменной iOb ( в данном случае — тип Integer ) . А далее получается значение объекта ob в следующей строке:
1 |
int v = iOb.getob(); |
Метод getob() возвращает обобщенный тип Т, который был заменен на тип Integer при объявлении переменной экземпляра iOb.
Поэтому метод getob() также возвращает тип Integer, который автоматически распаковывается в тип int и присваивается переменной v типа int. Следовательно, тип, возвращаемый методом getob(), нет никакой нужды приводить к типу Integer.
Безусловно, выполнять автоупаковку необязательно, переписав предыдущую строку кода так, как показано ниже. Но автоупаковка позволяет сделать код более компактным.
1 |
int v = iOb.getob().intValue(); |
Далее в классе GenDemo объявляется объект типа Gen<String> следующим образом:
1 |
Get<String> strOb = new Gen<String>("Текст обобщений"); |
В качестве аргумента типа в данном случае указывается тип String, подставляемый вместо параметра типа Т в обобщенном классе Gen.
Это, по существу, приводит к созданию строковой версии класса Gen, что и демонстрируется в остальной части рассматриваемой здесь программы.
Обобщения действуют только со ссылочными типами
Когда объявляется экземпляр обобщенного типа, аргумент, передаваемый в качестве параметра типа, должен относиться к ссылочному типу, но ни в коем случае не к примитивному типу наподобие int или char.
Например, в качестве параметра Т классу Gen можно передать тип любого класса, но нельзя передать примитивный тип. Таким образом, следующее объявление недопустимо:
1 |
Gen<int> intOb = new Gen<int>(10000); // Ошибка! Использовать примитивные типы нельзя |
Безусловно, отсутствие возможности использовать примитивный тип не является серьезным ограничением, поскольку можно применять оболочки типов данных (как это делалось в предыдущем примере программы) для инкапсуляции примитивных типов. Более того, механизм автоупаковки и автораспаковки в Java делает прозрачным применение оболочек типов данных.
Обобщенные типы различаются по аргументам типа
В отношении обобщенных типов самое главное понять, что ссылка на одну конкретную версию обобщенного типа несовместима с другой версией того же самого обобщенного типа.
Так, если ввести следующую строку кода в предыдущую программу, то при ее компиляции возникнет ошибка:
1 |
iOb = strOb; // Неверно! |
Несмотря на то что переменные экземпляра iOb и strOb относятся к типу Gen<T>, они являются ссылкам и на разные типы объектов, потому что их параметры типов отличаются.
Этим, в частности, обобщения обеспечивают типовую безопасность, предотвращая ошибки подобного рода.