Ограничения типов, метасимвольные аргументы, обобщенные методы и конструкторы

Продолжаем тему Generics (обобщения) и начнем с вопроса ограничений обобщенных типов. О чем здесь речь? Представьте, что нам нужно реализовать метод с некоторым типом данных T, который бы возвращал максимальное из двух значений:

class Point<T> {
    public T x, y;
 
    Point(T x, T y) {
        this.x = x;
        this.y = y;
    }
 
    double getMax() {
        double xd = x.doubleValue();
        double yd = y.doubleValue();
 
        return (xd < yd) ? yd : xd;
    }
}

Здесь я воспользовался классом Point, который мы создали на предыдущем занятии и добавил метод getMax(), возвращающий максимальную координату. Но если попытаться скомпилировать эту программу, то возникнет ошибка, так как не все типы данных поддерживают метод doubleValue(). Этот метод существует у типов, наследуемых от класса Number. Поэтому, если бы мы указали компилятору, что в качестве T будем использовать только числовые типы: Integer, Short, Double, Float, то проблем с вызовом doubleValue() не было бы, т.к. все эти типы реализуют этот метод. Так вот, чтобы сделать такой трюк и ограничить T числовыми типами, следует использовать такую запись:

class Point<T extends Number> { ... }

Здесь мы говорим, что в качестве T можно передавать любой тип данных, у которого базовый класс является Number. А это, как раз, все числовые типы. Теперь, при компиляции программы никаких ошибок не появляется благодаря введенному ограничению на типы данных.

Создадим в методе main объект класса Point и вызовем метод getMax():

public class Main {
    public static void main(String[] args) {
        Point<Integer> pt = new Point<Integer>(1, 2);
 
        double max = pt.getMax();
        System.out.println( max );
    }
}

В консоли увидим значение 2.0. Обратите внимание, несмотря на то, что указан тип Integer, метод doubleValue() для него возвратит вещественное значение, т.е. целое число будет приведено к типу double и возвращено этим методом. Это в данном случае удобно, т.к. double можно использовать как универсальный тип представления разных действительных чисел.

А что будет, если мы попробуем создать объект с нечисловым типом, например, строковым:

Point<String> ptS = new Point<String>("1", "2");

В этом случае возникнет ошибка в момент компиляции программы, т.к. тип String не наследуется от класса Number и не подходит под наши ограничения.

В качестве ограничений можно использовать и обобщенные классы с явным указанием типа, например, так:

class Numbers<T> { }
 
class Point<T extends Numbers<Integer>> { ... }

В этом случае в качестве типов можно использовать любые типы данных, унаследованных от класса Number и с указанным в нем типом Integer.

Можно делать и еще более сильные ограничения, когда помимо класса указываются интерфейсы, которые он должен реализовывать. В качестве отвлеченного примера, объявим два пустых интерфейса:

interface I1 {}
interface I2 {}

И, затем, у типа класса Point укажем их после имени базового класса:

class Point<T extends Number & I1, I2> { ... }

В этом случае можно использовать любые числовые типы, реализующие эти два интерфейса. Конечно, у нас нет таких типов данных, поэтому указание Integer приведет к ошибке при компиляции.

Также можно указывать только интерфейсы без базового класса:

class Point<T extends I1, I2> { ... }

В этом случае можно использовать любые тип, реализующие эти два интерфейса. Думаю, принцип реализации ограничений понятен.

Метасимвольные аргументы

Иногда использование обобщений может приводить к неожиданным результатам. Предположим, мы хотим в классе Point реализовать метод сравнения двух координат:

class Point<T extends Number> {
    public T x, y;
 
...
 
    boolean equalsPoint(Point<T> pt) {
        return (this.x.doubleValue() == pt.x.doubleValue() &&
                this.y.doubleValue() == pt.y.doubleValue());
    }
...
}

И, далее в методе main() создаем два объекта и сравниваем их с помощью нашего метода equalsPoint():

public class Main {
    public static void main(String[] args) {
        Point<Integer> pt = new Point<Integer>(1, 2);
        Point<Double> pt2 = new Point<Double>(1.0, 2.0);
 
        System.out.println( pt.equalsPoint(pt2) );
    }
}

Но при компиляции возникнет ошибка в строчке

pt.equalsPoint(pt2)

Дело в том, что при вызове метода pt.equalsPoint() в качестве типа T будет подставлен класс Integer и ожидается аргумент типа Point<Integer>, а мы передаем аргумент с типом Point<Double>. Получается, что метод equalsPoint() можно использовать с теми же типами данных, что и объект pt и у нас перестает работать механизм обобщения. Как поправить эту ситуацию? Для этого в Java существует специальный метасимвольный аргумент, который задается с помощью символа ‘?’. И если вместо T записать знак вопроса:

boolean equalsPoint(Point<?> pt) { ... }

то проблема будет решена. Теперь, метод equalsPoint() принимает любой тип данных класса Point.

При необходимости, мы также можем вводить ограничения на метасимвольные аргументы, например, так:

boolean equalsPoint(Point<? extends Number> pt) { ... }

или, добавляя интерфейсы. То есть, ограничения прописываются и работают абсолютно также, как и с обобщенными типами T.

Обобщенные методы

Иногда требуется объявлять не обобщенный класс, а один или несколько обобщенных методов внутри обычного класса. Например, мы пишем класс Math и хотим определить метод, который бы определял нахождение определенного значения в массиве. Для этой цели хорошо подходит следующий статический обобщенный метод:

class Math {
    public static <T> boolean isIn(T val, T[] ar) {
        for(T v: ar)
            if(v.equals(val)) return true;
 
        return false;
    }
}

Смотрите, мы здесь перед типом метода указываем обобщенный тип T, и далее используем его при определении аргументов. Затем, в методе main() можно вызвать этот метод следующим образом:

        Short ar[] = {1,2,3,4};
        Short val = 4;
        boolean flIn = Math.isIn(val, ar);
        System.out.println( flIn );

Или, с явным указанием обобщенного типа:

boolean flIn = Math.<Short>isIn(val, ar);

Тогда в качестве аргументов можно передавать только экземпляры классов Short.

Обобщенные конструкторы

Наряду с методами в классах можно прописывать и обобщенные конструкторы. Объявляются они по аналогии с обобщенными методами, следующим образом:

class Digit {
    public double value;
 
    <T extends Number>Digit(T value) {
        this.value = value.doubleValue();
    }
}

Обратите внимание, сам класс Digit не является обобщенным, только его конструктор. Далее, в методе main() можно его использовать с любыми типами числовых данных: Integer, Float, Short и т.д.

public class Main {
    public static void main(String[] args) {
        Digit d1 = new Digit(10);
        Digit d2 = new Digit(10.5);
        Digit d3 = new Digit(10.5f);
 
        System.out.println(d1.value + " " + d2.value + " " + d3.value);
    }
}

Вот так в языке Java можно накладывать ограничения на используемые типы данных, использовать метасимвольные аргументы и обобщенные конструкторы и методы.

Видео по теме