Ограничения типов, метасимвольные аргументы, обобщенные методы и конструкторы
Продолжаем тему Generics (обобщения) и начнем с вопроса ограничений обобщенных типов. О чем здесь речь? Представьте, что нам нужно реализовать метод с некоторым типом данных T, который бы возвращал максимальное из двух значений:
class PointT> { 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 PointT extends Number> { . }
Здесь мы говорим, что в качестве T можно передавать любой тип данных, у которого базовый класс является Number. А это, как раз, все числовые типы. Теперь, при компиляции программы никаких ошибок не появляется благодаря введенному ограничению на типы данных.
Создадим в методе main объект класса Point и вызовем метод getMax():
public class Main { public static void main(String[] args) { PointInteger> pt = new PointInteger>(1, 2); double max = pt.getMax(); System.out.println( max ); } }
В консоли увидим значение 2.0. Обратите внимание, несмотря на то, что указан тип Integer, метод doubleValue() для него возвратит вещественное значение, т.е. целое число будет приведено к типу double и возвращено этим методом. Это в данном случае удобно, т.к. double можно использовать как универсальный тип представления разных действительных чисел.
А что будет, если мы попробуем создать объект с нечисловым типом, например, строковым:
PointString> ptS = new PointString>("1", "2");
В этом случае возникнет ошибка в момент компиляции программы, т.к. тип String не наследуется от класса Number и не подходит под наши ограничения.
В качестве ограничений можно использовать и обобщенные классы с явным указанием типа, например, так:
class NumbersT> { } class PointT extends NumbersInteger>> { . }
В этом случае в качестве типов можно использовать любые типы данных, унаследованных от класса Number и с указанным в нем типом Integer.
Можно делать и еще более сильные ограничения, когда помимо класса указываются интерфейсы, которые он должен реализовывать. В качестве отвлеченного примера, объявим два пустых интерфейса:
interface I1 {} interface I2 {}
И, затем, у типа класса Point укажем их после имени базового класса:
В этом случае можно использовать любые числовые типы, реализующие эти два интерфейса. Конечно, у нас нет таких типов данных, поэтому указание Integer приведет к ошибке при компиляции.
Также можно указывать только интерфейсы без базового класса:
class PointT extends I1, I2> { . }
В этом случае можно использовать любые тип, реализующие эти два интерфейса. Думаю, принцип реализации ограничений понятен.
Метасимвольные аргументы
Иногда использование обобщений может приводить к неожиданным результатам. Предположим, мы хотим в классе Point реализовать метод сравнения двух координат:
class PointT extends Number> { public T x, y; . boolean equalsPoint(PointT> 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) { PointInteger> pt = new PointInteger>(1, 2); PointDouble> pt2 = new PointDouble>(1.0, 2.0); System.out.println( pt.equalsPoint(pt2) ); } }
Но при компиляции возникнет ошибка в строчке
Дело в том, что при вызове метода pt.equalsPoint() в качестве типа T будет подставлен класс Integer и ожидается аргумент типа Point, а мы передаем аргумент с типом Point. Получается, что метод 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 );
Или, с явным указанием обобщенного типа:
Тогда в качестве аргументов можно передавать только экземпляры классов 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 можно накладывать ограничения на используемые типы данных, использовать метасимвольные аргументы и обобщенные конструкторы и методы.
Видео по теме
#11 Концепция объектно-ориентированного программирования (ООП)
#12 Классы и создание объектов классов
#13 Конструкторы, ключевое слово this, инициализаторы
#14 Методы класса, сеттеры и геттеры, public, private, protected
#15 Пакеты, модификаторы конструкторов и классов
#16 Ключевые слова static и final
#17 Внутренние и вложенные классы
#18 Как делается наследование классов
#19 Ключевое слово super, оператор instanceof
#20 Модификаторы private и protected, переопределение методов, полиморфизм
#21 Абстрактные классы и методы
#22 Интерфейсы — объявление и применение
#23 Интерфейсы — приватные, статические и дефолтные методы, наследование интерфейсов
#24 Анонимные внутренние классы
#26 Обобщения классов (Generics)
#27 Ограничения типов, метасимвольные аргументы, обобщенные методы и конструкторы
#28 Обобщенные интерфейсы, наследование обобщенных классов
© 2023 Частичное или полное копирование информации с данного сайта для распространения на других ресурсах, в том числе и бумажных, строго запрещено. Все тексты и изображения являются собственностью сайта
Bounded Type Parameters
There may be times when you want to restrict the types that can be used as type arguments in a parameterized type. For example, a method that operates on numbers might only want to accept instances of Number or its subclasses. This is what bounded type parameters are for.
To declare a bounded type parameter, list the type parameter’s name, followed by the extends keyword, followed by its upper bound, which in this example is Number . Note that, in this context, extends is used in a general sense to mean either «extends» (as in classes) or «implements» (as in interfaces).
public class Box < private T t; public void set(T t) < this.t = t; >public T get() < return t; >public extends Number> void inspect(U u) < System.out.println("T: " + t.getClass().getName()); System.out.println("U: " + u.getClass().getName()); >public static void main(String[] args) < BoxintegerBox = new Box(); integerBox.set(new Integer(10)); integerBox.inspect("some text"); // error: this is still String! > >
By modifying our generic method to include this bounded type parameter, compilation will now fail, since our invocation of inspect still includes a String :
Box.java:21: inspect(U) in Box cannot be applied to (java.lang.String) integerBox.inspect("10"); ^ 1 error
In addition to limiting the types you can use to instantiate a generic type, bounded type parameters allow you to invoke methods defined in the bounds:
public class NaturalNumber < private T n; public NaturalNumber(T n) < this.n = n; >public boolean isEven() < return n.intValue() % 2 == 0; > // . >
The isEven method invokes the intValue method defined in the Integer class through n.
Multiple Bounds
The preceding example illustrates the use of a type parameter with a single bound, but a type parameter can have multiple bounds:
A type variable with multiple bounds is a subtype of all the types listed in the bound. If one of the bounds is a class, it must be specified first. For example:
Class A < /* . */ >interface B < /* . */ >interface C < /* . */ >class D < /* . */ >
If bound A is not specified first, you get a compile-time error:
class D < /* . */ >// compile-time error