По сути дела, обобщения — это параметризованные типы. Эти типы важны, поскольку позволяют объявлять классы, интерфейсы и методы, где тип данных, которыми они оперируют, указан в виде параметра. Используя обобщения, можно создать единственный класс, который, например, будет автоматически работать с разными типами данных.
Скачать исходники для статьи можно ниже
Классы, интерфейсы или методы, имеющие дело с параметризованными типами, называются обобщениями, обобщенными классами или обобщенными методами.
Обобщения добавили в язык безопасность типов, которой так не хватало. Они также упростили процесс выполнения, поскольку теперь нет необходимости применять явные приведения для транслирования объектов класса Object в реальные типы данных, с которыми выполняются действия. Благодаря обобщениям, все приведения выполняются автоматически и неявно.
Простой пример обобщения. В следующей программе определены два класса. Первый — это обобщенный класс Gen, а второй — GenDemo, класс использующий класс Gen.
// Простой обобщенный класс. // Здесь Т — это параметр типа, // который будет заменен реальным типом // при создании объекта класса Gen. class Gen<T> { Т ob; // объявление объекта типа Т // Передать конструктору ссылку // на объект типа Т. Gen(Т о) { ob = о ; } // Вернуть ob. Т getobO { return ob; } // Показать тип Т. void showType() { System.out.println("Типом T является " + ob.getClass().getName()); } } // Демонстрация обобщенного класса. class GenDemo { public static void main(String args[]) { // Создать объект Gen<Integer> и присвоить // ссылку на iOb. Отметьте применение автоупаковки // для инкапсуляции значения 88 в объект Integer. Gen<Integer> iOb; iOb = new Gen<Integer>(88); // Показать тип данных, используемый iOb. iOb.showType(); // Получить значение iOb. Обратите внимание, // что никакого приведения не нужно, int v = iOb.getobO; System.out.println("значение: " + v); System.out.println(); // Создать объект Gen для String. Gen<String> strOb = new Gen<String> ("Обобщенный тест"); // Показать тип данных, используемый strOb. strOb.showType(); // Получить значение strOb. Опять же // приведение не требуется. String str = strOb.getob(); System.out.println("Значение: " + str); } }
Результат работы этой программы:
Типом Т является java.lang.Integer
Значение: 88
Типом Т является java.lang.String
Значение: Обобщенный тест
Давайте внимательно исследуем эту программу. Обратите внимание на объявление класса Gen в следующей строке.
class Gen<T> {
Здесь Т — имя параметра типа. Это имя используется в качестве заполнителя, куда будет подставлено имя реального типа, переданного классу Gen при создании реальных типов.
То есть параметр типа Т применяется в классе Gen всякий раз, когда
требуется параметр типа. Обратите внимание на то, что тип Т заключен в угловые скобки <>. Этот синтаксис может быть обобщен.
Всякий раз, когда объявляется параметр типа, он указывается в угловых скобках. Поскольку класс Gen применяет параметр типа, класс Gen является обобщенным классом, который называется также параметризованным типом. Далее тип Т используется для объявления объекта по имени ob, как показано ниже:
Т ob; // объявляет объект типа Т
Как упоминалось, Т — это место для подстановки реального типа, который будет указан при создании объекта класса Gen. То есть объект ob будет объектом типа, переданного в параметре типа Т.
Например, если в параметре Т передан тип String, то экземпляр ob будет иметь тип String.
Теперь рассмотрим конструктор Gen ():
Gen(Т о) { ob = о; }
Как видите, параметр о имеет тип Т. Это значит, что реальный тип параметра о определяется типом, переданным параметром типа Т при создании объекта класса Gen.
К тому же, поскольку и параметр о, и переменная-член ob имеют тип Т, они оба получают одинаковый реальный тип при создании объекта класса Gen. Параметр типа Т также может быть использован для указания типа возвращаемого значения метода, как в случае метода getob (), оказанного здесь:
Т getob() { return ob; }
Так как объект ob тоже имеет тип Т, его тип совместим с типом, возвращаемым методом getob().
Метод showType() отображает тип Т вызовом метода getName() объекта класса Class, возвращенным вызовом метода getClass() объекта ob. Метод getClass() определен в классе Object и потому является членом всех классов. Он возвращает объект класса Class, соответствующий типу класса объекта, для которого он вызван. Класс Class определяет метод getName(), который возвращает строковое представление имени
класса.
Класс GenDemo демонстрирует обобщенный класс Gen. Сначала он создает версию класса Gen для целых чисел, как показано ниже:
Gen<Integer> iOb;
Посмотрим на это объявление внимательней. Отметим, что тип Integer указан в угловых скобках после слова Gen. В этом случае Integer — это аргумент типа, который передается в параметре типа Т класса Gen. Это фактически создает версию класса Gen, в которой все ссылки на тип Т транслируются в ссылки на тип Integer.
То есть в данном объявлении объект ob имеет тип Integer, и тип
возвращаемого значения метода getob() также имеет тип Integer.
Следующая строка присваивает объекту iOb ссылку на экземпляр целочисленной версии класса Gen:
iOb = new Gen<Integer>(88);
Отметим, что когда вызывается конструктор Gen(), аргументтипа Integer также указывается. Это необходимо, потому что типом объекта (в данном случае объекта iOb, которому присваивается ссылка, является тип Gen То есть ссылка, возвращаемая оператором new, также должна иметь тип Gen Поскольку объект iOb имеет тип Gen Как указано в комментарии к программе, присваивание использует автоупаковку для инкапсуляции значения 88, имеющего тип int, в объекте класса Integer. Однако с этой версией не связано никаких преимуществ. Программа затем отображает тип объекта ob внутри объекта iOb, которым является Integer. Поскольку возвращаемым типом метода getob() будет Т, который заменяется на Integer при объявлении объекта iOb, то возвращаемым типом метода getob() также будет класс Integer, который автоматически распаковывается в тип int и присваивается переменной v, имеющей тип int. То есть нет никакой необходимости приводить тип возвращаемого Однако автоупаковка позволяет сделать код более компактным.
Далее в классе GenDemo объявляется объект типа Gen Поскольку аргументом типа является String, класс String подставляется вместо параметра Т внутри класса Gen. Это создает (концептуально) строковую версию класса Gen, что и демонстрируют остальные строки программы.
Когда объявляется экземпляр обобщенного типа, аргумент, переданный в качестве параметра типа, должен быть типом класса. Вы не можете использовать элементарный тип вроде int или char.
Например, классу Gen можно передать в параметре Т любой тип класса, но нельзя передать элементарный тип. Таким образом, следующее объявление недопустимо.
Конечно, невозможность использовать элементарный тип не является серьезным ограничением, так как вы можете применять оболочки типов (как это и делается в предыдущем примере) для инкапсуляции элементарных типов. Более того, механизм автоупаковки и автораспаковки Java делает использование оболочек типов прозрачным.
Отличие обобщенных типов в зависимости от аргументов типа.
Ключевой момент в понимании обобщенных типов в том, что ссылка на одну Например, следующая строка, если ее добавить к предыдущей программе, вызовет ошибку и программа не будет откомпилирована:
Даже несмотря на то, что объекты iOb и strOb имеют тип Gen Обобщения повышают безопасность типов.
Теперь вы можете задать себе следующий вопрос: если те же функциональные возможности, которые мы обнаружили в обобщенном классе Gen, могут быть получены без обобщений, т.е. простым указанием класса Object в качестве типа данных и применением правильных приведений, то в чем же выгода от того, что класс Gen параметризован?
Ответ: в том, что обобщения автоматически гарантируют безопасность Чтобы понять выгоды от обобщений, для начала рассмотрим следующую программу, которая создает необобщенный эквивалент класса Gen:
В этой версии программы присутствует несколько интересных моментов. Однако это не позволяет компилятору Java иметь какую-то реальную информацию о типе данных, в действительности сохраняемых в объекте Во-первых, для извлечения сохраненных данных требуется явное приведение. Поскольку возвращаемым типом метода getob() является тип Object, необходимо привести его к типу Integer, чтобы позволить выполнить автораспаковку и сохранить значение в переменной v. Если убрать приведение, программа некомпилируется. Теперь рассмотрим следующую кодовую последовательность в конце программы:
Здесь объект strOb присваивается объекту iOb. Однако объект strOb ссылается на объект, содержащий строку, а не целое число. Это присваивание синтаксически корректно, потому что все ссылки класса NonGen одинаковы и любая ссылка класса NonGen может указывать на любой другой объект типа NonGen.
Однако этот оператор семантически неверен, что и отражено в следующей строке.
Здесь тип возвращаемого значения метода getob() приводится к классу Integer, а затем делается попытка присвоить это значение переменной v.
Проблема в том, что объект iOb теперь ссылается на объект, который хранит тип String, а не Integer.
К несчастью, без использования обобщений компилятор Java не имеет возможности обнаружить это. Вместо этого передается исключение времени выполнения.
Возможность создавать безопасный в отношении типов код, в котором ошибки несоответствия типов перехватываются компилятором — это главное преимущество обобщений. Хотя использование ссылок на тип Object для создания “псевдообобщенного” кода всегда возможно, нужно помнить, что такой код не является безопасным в отношении типов, и злоупотребление им приводит к исключениям времени выполнения.
Если это не так, получается ошибка времени компиляции.
Например, следующее присваивание вызовет ошибку компиляции:
iOb = new Gen<Double>(88.О); // Ошибка!
iOb = new Gen<Integer>(88);
Это работает, потому что тип Gen
iOb = new Gen<Integer>(new Integer(88));
Далее программа получает значение объекта ob в следующей строке.
int v = iOb.getobO;
значения метода getob() к классу Integer. Конечно, использовать автоупаковку не обязательно. Предыдущая строка может быть написана так:
int v = iOb.getob().intValue();
Gen<String> strOb = new Gen<String>(" Обобщенный тест");
Gen<int> intOb = new Gen<int>(53); // Ошибка, нельзя использовать
// элементарные типы
специфическую версию обобщенного типа не совместима с другой версией того же обобщенного типа.
iOb = strOb; //Не верно!
типов во всех операциях, где задействован класс Gen. В процессе работы с ним исключается необходимость явного приведения и ручной проверки типов в коде.
// NonGen — функциональный эквивалент Gen,
// не использующий обобщений,
class NonGen {
Object ob; // ob теперь имеет тип Object
// Передать конструктору ссылку на объект типа Object
NonGen(Object о) {
ob = о ;
}
// Вернуть тип Object.
Object getob() {
return ob;
}
// Показать тип ob.
void showType() {
System.out.println("Типом ob является " + ob.getClass().getName());
}
}
// Демонстрация необобщенного класса,
class NonGenDemo {
public static void main(String args[]) {
// Создать объект NonGen и сохранить
// Integer в нем. Автоупаковка используется.
NonGen iOb;
iOb = new NonGen(88);
// Показать тип данных, используемый iOb.
iOb.showType();
// Получить значение iOb.
// На этот раз приведение необходимо,
int v = (Integer) iOb.getobO;
System.out.println("значение: " + v);
System.out.println();
// Создать другой объект NonGen и
// сохранить в нем String.
NonGen strOb = new NonGen("Тест без обобщений");
// Показать тип данных, используемый strOb.
strOb.showType();
// Получить значение strOb.
// Опять же — приведение необходимо.
String str = (String) strOb.getobО;
System.out.println("Значение: " + str);
// Это компилируется, но концептуально неверно!
iOb = strOb;
v = (Integer) iOb.getobO; // ошибка времени выполнения!
}
}
Для начала класс NonGen заменяет все обращения к типу Т на обращения к типу Object. Это позволяет классу NonGen хранить объекты любого типа, как это делает и обобщенная версия.
класса NonGen, что плохо по двум причинам.
Во-вторых, многие ошибки несоответствия типов не могут быть обнаружены до времени выполнения.
Рассмотрим каждую из этих проблем поближе.
Обратите внимание на эту строку:
int v = (Integer) iOb.getobO;
В версии с обобщением приведение происходит неявно. В версии
без обобщения приведение должно быть явным. Это не только приносит неудобство, но и является потенциальным источником ошибок.
// Это компилируется, но концептуально неверно!
iOb = strOb;
v = (Integer) iOb.getob(); // ошибка времени выполнения!
Обобщения предотвращают подобные вещи. По сути, благодаря обобщениям, ошибки времени выполнения преобразуются в ошибки времени компиляции. Это и есть главное преимущество.