В языке Java можно описывать
классы без привязки к конкретному типу данных. Например, мы собираемся записать
класс Point для
представления точки на плоскости с координатами x, y. И здесь перед
программистом сразу встает вопрос: какой тип данных определить у этих координат?
Если в будущем предполагаются только целочисленные величины x, y, то наверное,
подойдет тип int. Если же будут
использоваться вещественные значения, то тип float или double. Конечно, можно
взять тип double и хранить с его
помощью и целочисленные и вещественные значения. Но это не лучший путь, так как
вещественная арифметика выполняется заметно дольше целочисленной. И для
целочисленных координат лучше использовать именно целочисленный тип. Для
разрешения такой ситуации в Java и была введена
возможность создавать классы с обобщенными типами.
В нашем примере
с классом Point его обобщенное
описание можно объявить следующим образом:
class Point<T> {
public T x, y;
}
Смотрите, здесь
после имени класса в угловых скобках стоит буква T – это имя
обобщенного типа. На момент записи класса мы пока не знаем что будет
подставлено вместо T. Здесь может быть и тип Integer и тип Double и тип String и любой другой
тип языка Java (кроме
примитивных: int, float, double и т.п). После
этого поля x, y будут
соответствовать этому типу данных. Но когда этот тип становится определенным? В
момент создания экземпляра класса Point. То есть, в
методе main() мы должны
теперь указывать не только имя класса, но и в угловых скобках тип данных,
например:
Point<Integer> pt = new Point<Integer>();
И, затем,
использовать целочисленные координаты:
pt.x = 10;
pt.y = 20;
System.out.println(pt.x + " " + pt.y);
Если потом
потребуется создать объект класса Point с вещественными
полями, то достаточно будет выполнить конструкцию:
Point<Double> ptD = new Point<Double>();
Причем ссылка ptD будет иметь уже
другой тип, чем ссылка pt. И, например, вот такая строчка
приведет к ошибке:
pt = ptD; // ошибка: разные типы ссылок
В итоге объекты Point<Integer> и Point<Double> это,
фактически, разные объекты с разными типами данных, но построенные по одному
шаблону класса Point. В этом и заключается смысл обобщения.
У вас может
возникнуть вопрос: а почему в качестве обобщенного типа используется буква T? Можно ли
вместо нее записать другую букву или слово? Да, можно. Например, вот такое
объявление класса Point будет аналогично предыдущему:
class Point<Tt> {
public Tt x, y;
}
Но, в практике
программирования принято записывать просто одну букву T.
Начиная с версии
JDK7 после
оператора new и имени класса
можно записывать только угловые скобки без указания типа(ов), например, так:
Point<Integer> pt = new Point<>();
Point<Double> ptD = new Point<>();
Это несколько
более удобная запись и ее часто можно встретить в текстах программ.
Давайте теперь
добавим в класс Point конструктор. Он будет выглядеть так:
class Point<T> {
public T x, y;
Point(T x, T y) {
this.x = x;
this.y = y;
}
}
Смотрите, здесь
мы также указываем обобщенный тип T у его аргументов.
Соответственно, когда будут создаваться объекты этого класса, вместо T будет
подставляться соответствующий тип:
public class Main {
public static void main(String[] args) {
Point<Integer> pt = new Point<Integer>(1, 2);
Point<Double> ptD = new Point<Double>(10.5, 20.8);
System.out.println(pt.x + " " + pt.y);
System.out.println(ptD.x + " " + ptD.y);
}
}
По аналогии
можно добавить методы, которые возвращали бы координаты точки:
T getCoordX() { return x; }
T getCoordY() { return y; }
Но вернуть сразу
массив координат не получится, следующие строчки приведут к ошибке:
T[] getCoords() {
return new T[] {x, y};
}
Дело в том, что Java запрещает
создавать массив на основе обобщенного типа. Программа просто не может быть
скомпилирована в байт-код, так как нельзя реализовать универсально оператор new с неким
обобщенным типом. Но как же тогда быть, если нам нужно реализовать подобную
операцию? Очень просто. Вспомним, что все классы на вершине иерархии имеют
класс Object. Именно его
можно указать в качестве общего типа:
И, затем, в
методе main вызвать этот
метод, например, так:
Так можно выйти
из этой ситуации. И глядя на эти строчки, может возникнуть мысль: а зачем
вообще придумали эти обобщения, когда мы могли бы использовать класс Object в качестве
общего типа данных? Да, так тоже можно делать, но это менее удобно. Во-первых,
нам пришлось бы постоянно выполнять приведение типов. И, во-вторых, не было бы
контроля соответствия типов данных, который предоставляют обобщения. Благодаря
этому в программе с обобщениями программист совершает меньше ошибок, чем с
типом данных Object.
Класс с двумя обобщенными типами
Я думаю, общая
идея использования обобщений понятна. Давайте сделаем следующий шажок и
посмотрим как можно объявлять класс с двумя обобщенными типами. Чтобы не
усложнять нашу реализацию, в класс Point добавим еще одно
поле с именем id и типом V:
class Point<T, V> {
public V id;
public T x, y;
Point(T x, T y) {
this.x = x;
this.y = y;
}
V getId() { return id; }
T getCoordX() { return x; }
T getCoordY() { return y; }
}
Как видите, все
достаточно очевидно. Мы через запятую в угловых скобках указали еще один тип V и используем
его для определения поля id. Также у нас имеется метод getId(), который
возвращает это поле. Теперь, при создании объектов этого класса, нам нужно
указывать два типа:
public class Main {
public static void main(String[] args) {
Point<Integer, Integer> pt = new Point<>(1, 2);
Point<Double, String> ptD = new Point<>(10.5, 20.8);
pt.id = 1;
ptD.id = "point_1";
}
}
В первом случае
мы используем целочисленный тип для id, а во втором – строковый.
Соответственно, ниже полю id присваиваем 1, а для второго объекта –
строку "point_1". По
аналогии можно записывать множество разных обобщенных типов при определении
класса.
Итак, на этом
занятии мы сделали введение в обобщения (Generics) классов. На
последующих продолжим эту тему и увидим, как делаются ограничения на обобщенные
типы данных, создаются обобщенные интерфейсы и методы, как работают обобщения в
механизме наследования.