На этом занятии
рассмотрим возможность обобщения интерфейсов в языке Java. Для простоты,
как всегда, будем описывать класс Point для
представления точки на плоскости. И для начала объявим обобщенный интерфейс с
двумя методами:
enum TypeCoord {
COORD_X, COORD_Y;
}
interface GeomInteface<T> {
void setCoord(T x, T y);
T getCoord(TypeCoord type);
}
Синтаксис записи
обобщенных интерфейсов абсолютно такой же, как и у классов. Далее, следуют два
абстрактных метода: setCoord() – для записи координат точки; getCoord()
– для получения координаты точки в зависимости от значения перечисления (COORD_X – для получения
значения x; COORD_Y – для получения
значения y).
Теперь, запишем
класс Point, который будет
применять этот интерфейс:
class Point<TT> implements GeomInteface<TT> {
private TT x, y;
public void setCoord(TT x, TT y) {
this.x = x;
this.y = y;
}
public TT getCoord(TypeCoord type) {
return (type == TypeCoord.COORD_X) ? x : y;
}
}
Я здесь
специально обобщенный тип у класса Point обозначил через TT, чтобы отличать
его от типа T интерфейса. И
отсюда хорошо видно, что когда выполняется применение интерфейса GeomInteface, его обобщенный
тип T принимает то же
значение, что и тип TT класса Point. В результате,
тип интерфейса и класса всегда идентичен. Далее, внутри класса мы используем
тип TT и определяем
реализации двух методов.
Все, вот так мы
с вами объявили обобщенный интерфейс и применили его в обобщенном классе.
Теперь, в методе main() можно создать объект этого класса и вызвать его
методы:
public class Main {
public static void main(String[] args) {
Point<Float> pt = new Point<Float>();
pt.setCoord(10f, 20f);
System.out.println( pt.getCoord(TypeCoord.COORD_X));
System.out.println( pt.getCoord(TypeCoord.COORD_Y));
}
}
Или, вместо типа
Point<Float> можно
прописать тип интерфейса:
GeomInteface<Float> pt = new Point<Float>();
И все будет
работать абсолютно также.
Конечно, мы
можем применять обобщенный интерфейс и в обычном классе, например, сделать так:
class Point implements GeomInteface<Integer> {
private Integer x, y;
public void setCoord(Integer x, Integer y) {
this.x = x;
this.y = y;
}
public Integer getCoord(TypeCoord type) {
return (type == TypeCoord.COORD_X) ? x : y;
}
}
Но тогда у
интерфейса необходимо в угловых скобках явно указать требуемый тип данных. Вот
так, довольно просто и очевидно реализуются обобщенные интерфейсы.
Обобщения при наследовании классов
Наконец,
обобщения можно использовать и в механизме наследования классов. Работает все
достаточно просто и очевидным образом. Для простоты понимания и изложения
материала, я создам простую иерархию из одного обобщенного класса PointProp и двух дочерних
классов: Point2D и Point3D.
Сначала объявим
обобщенный базовый класс PointProp:
class PointProp<T> {
T id, color;
}
А, затем, на его
основе два дочерних:
class Point2D<T2D> extends PointProp<T2D> {
T2D x, y;
Point2D(T2D x, T2D y) {
this.x = x;
this.y = y;
}
}
class Point3D<T3D> extends PointProp<T3D> {
T3D x, y, z;
Point3D(T3D x, T3D y, T3D z) {
this.x = x;
this.y = y;
this.z = z;
}
}
Смотрите, я
здесь опять же специально задал типы T2D и T3D разные у
дочерних классов и у базового класса, чтобы было понятнее, как все применяется.
Синтаксис наследования вполне очевиден: мы прописываем базовый класс и в
угловых скобках указываем требуемый тип данных. Далее, можем создать экземпляры
дочерних объектов, следующим образом:
public class Main {
public static void main(String[] args) {
Point2D<Integer> p1 = new Point2D<>(1, 2);
Point3D<Float> p2 = new Point3D<>(10f, 20f, 30f);
}
}
Как видите,
здесь реализуется обычный механизм наследования, только дополнительно
указываются типы данных. При необходимости, мы можем наследоваться от
обобщенного базового класса, описывая обычные классы, например, так:
class Point2D extends PointProp<Integer> { ... }
class Point3D extends PointProp<Float> { ... }
Или, наоборот,
использовать обычный базовый класс, объявляя обобщенные дочерние:
class Point2D<T2D> extends PointProp { ... }
class Point3D<T3D> extends PointProp { ... }
То есть,
возможны различные комбинации.
Оператор instanceof с обобщенными классами
Оператор instanceof можно применять
с обобщенными классами (как вы помните, этот оператор возвращает true, если ссылка
является экземпляром указанного класса и false – в противном
случае). Давайте посмотрим на примере как это делается. Предположим, мы
объявляем две ссылки типа PointProp на разные дочерние объекты:
PointProp<Integer> p1 = new Point2D<>(1, 2);
PointProp<Float> p2 = new Point3D<>(10f, 20f, 30f);
И, затем, хотим
определить: к какому дочернему классу относится та или иная ссылка. Для этого
воспользуемся оператором instanceof, следующим образом:
if( p1 instanceof Point2D<?>)
System.out.println("p1 является объектом класса Point2D");
Обратите
внимание, как записан класс Point2D. Здесь в
угловых скобках мы указываем метасимвол (знак вопроса), так как ссылка p1 может
реализовывать самый разный тип данных, поэтому обобщенный класс в операторе instanceof
обязательно нужно прописывать с метасимволом. Указывать конкретный тип не
допустимо. Следующие строчки приведут к ошибке:
if( p1 instanceof Point2D<Integer>)
System.out.println("p1 является объектом класса Point2D");
То есть, с
помощью оператора instanceof можно лишь определить принадлежность к тому или
иному дочернему классу без указания конкретного обобщенного типа.
Приведение типов обобщенных классов
Однако, всегда
следует помнить, что, например, типы
Point2D<Integer>
и
Point2D<Float>
это разные типы
данных. И, в частности, привести тип ссылки p1 к Point2D<Integer> возможно:
((Point2D<Integer>)p1).x = 5; // ошибки нет
Но к типу Point2D<Float> уже нельзя
из-за конфликта типов:
((Point2D<Float>)p1).x = 5f; // ошибка: несоответствие типов
Вот так в Java реализуются
обобщенные интерфейсы и наследование обобщенных классов.