Продолжаем тему 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
можно
накладывать ограничения на используемые типы данных, использовать
метасимвольные аргументы и обобщенные конструкторы и методы.