Обобщенные интерфейсы, наследование обобщенных классов

На этом занятии рассмотрим возможность обобщения интерфейсов в языке 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 реализуются обобщенные интерфейсы и наследование обобщенных классов.

Видео по теме