В языке Java обобщения играют важную роль в обеспечении безопасности типов и создании гибкого кода. Однако, при работе с наследованием и обобщениями могут возникать сложности. Именно здесь на помощь приходят wildcards (подстановочные знаки или метасимволы), предоставляя мощный инструмент для работы с неизвестными типами.
В контексте обобщений wildcard или подстановочный знак представляет представляет неизвестный тип и обозначается с помощью вопросительного знака ?. Он позволяет создавать более гибкий функционал, который может работать с различными типами данных.
Существует три вида подстановочных знаков для разных целей:
Неограниченный Wildcard (?)
Wildcard, ограниченный "сверху" (ограниченный базовым классом) (? extends Type)
Wildcard, ограниченный "снизу" (ограниченный производным классом) (? super Type)
Рассмотрим эти типы подстановочных знаков и сценарии их использования на примере следующего класса Person:
class Person<T>{
private T id;
private String name;
T getId(){ return id; }
void setId(T id){ this.id = id; }
String getName() { return name; }
Person(T id, String name){
this.id = id;
this.name = name;
}
}
Тип Person является обобщенный и типизирован параметром T, который предоставляет тип для поля id.
Но прежде всего посмотрим на ситуацию, где подстановочные знаки могут помочь:
public class Program{
public static void main(String[] args) {
Person<Object> bob = new Person<>(2.0, "Bob");
Person<Integer> tom = new Person<>(1, "Tom");
Person<String> sam = new Person<>("F-456", "Sam");
printPersonInfo(bob);
printPersonInfo(tom); // ! ОШибка: tom - НЕ представляет тип Person<Object>
printPersonInfo(sam); // ! ОШибка: sam - НЕ представляет тип Person<Object>
}
static void printPersonInfo(Person<Object> person) {
System.out.print("Name: " + person.getName());
Object id = person.getId();
System.out.println("; Id: " + id);
}
}
При попытке компиляции этой программы мы столкнемся с ошибками. Почему? Здесь метод printPersonInfo() в качестве параметра принимает объект типа Person<Object>. Типизация типом Object казалось бы охватит все возможные ситуации
для поля id, в том числе когда это поле представляет типы Integer и String (как наследника класса Object). Однако в реальности мы сможем передать в этот метод объекты Person,
которые типизирированы типами, отличными от Object:
Person<Integer> tom = new Person<>(1, "Tom");
Person<String> sam = new Person<>("F-456", "Sam");
printPersonInfo(tom); // ! ОШибка: tom - НЕ представляет тип Person<Object>
printPersonInfo(sam); // ! ОШибка: sam - НЕ представляет тип Person<Object>
И подстановочные символы wildcard как раз позволяют решить эту проблему.
Неограниченный wildcard (Unbounded Wildcard) используется, когда необходимо работать с обобщенным типом, где конкретный тип неизвестен или не важен. Такой подстановочный знак фактически обозначает "любой тип". Такой тип подстановочных знаков может быть полезен, когда логика вашего метода не зависит от конкретного типа, используемого в обобщении.
Рассмотрим следующий пример:
public class Program{
public static void main(String[] args) {
// Пример использования:
Person<Integer> tom = new Person<>(123, "Tom");
Person<String> bob = new Person<>("A-456", "Bob");
printPersonInfo(tom); // Name: Tom; Id: 123
System.out.println();
printPersonInfo(bob); // Name: Bob; Id: A-456
}
static void printPersonInfo(Person<?> person) {
// Мы можем безопасно вызывать методы, не связанные с дженериком
System.out.print("Name: " + person.getName());
// Мы можем получить id, но компилятор будет рассматривать его как Object
Object id = person.getId();
System.out.println("; Id: " + id);
}
}
В данном случае определен метод printPersonInfo, который просто выводит информацию о человеке. Обратите внимание на тип его параметра:
Person<?> person
То есть параметр person представляет класс Person, но каким типом типизирован этот класс, неважно. Поскольку здесь нам не важен тип его поля id - мы просто выводим его на консоль. В этом случае
используется строковое представление поля id. Поэтому в этот метод мы можем передать объекты класса Person, типизированные различными типами, например, Integer или String:
Person<Integer> tom = new Person<>(123, "Tom");
Person<String> bob = new Person<>("A-456", "Bob");
printPersonInfo(tom); // Name: Tom; Id: 123
printPersonInfo(bob); // Name: Bob; Id: A-456
Но поскольку компилятор здесь не знает, какой именно тип ожидает объект Person<?>, мы не можем выполнять ряд операций с полем id, например, установить его с помощью метода setId():
public class Program{
public static void main(String[] args) {
// Пример использования:
Person<Integer> tom = new Person<>(123, "Tom");
changePerson(tom, 25);
}
static void changePerson(Person<?> person, Object id) {
person.setId(id); // ! ошибка
}
}
Здесь при попытке установить id через вызов person.setId(id) мы полуим ошибку во время компиляции.
Wildcard, ограниченный "сверху" или ограниченный базовым классом, означает "любой тип, который представляет данный базовый класс или его производный класс. Задается в виде следующего выражения:
? extends Type
То есть тип должен представлять производный от Type тип или сам Type
Данный тип подстановочных знаков применяется, когда вам нужно получать значение обобщения и работать с ним как с базовым типом.
Почему называется ограничением "сверху"? Потому что в иерархии типов базовые типы располагаются вверху. Соответственно тип Type задает крайний вверху базовый класс. Классы, которые расположены выше Type (базовые классы по отношению к Type, например, Object) отсекаются.
Например, определим метод, который работает только с теми объектами Person, у которых поле id представляет число, например, Integer, Double.
Отличительная особенность встроенных числовых типов в Java в том, что они наследуются от базового класса Number. Поэтому мы можем использовать тип Number в качестве ограничения сверху:
public class Program{
public static void main(String[] args) {
Person<Integer> tom = new Person<>(10, "Tom");
Person<Double> bob = new Person<>(25.5, "Bob");
processNumericId(tom); // Id as double: 10.0
processNumericId(bob); // Id as double: 25.5
}
static void processNumericId(Person<? extends Number> person) {
// Мы можем безопасно "прочитать" id как Number
Number id = person.getId();
System.out.println("Id as double: " + id.doubleValue());
}
}
Здесь метод processNumericId() принимает объект Person, который типизирован типом NUmber или его наследниками:
void processNumericId(Person<? extends Number> person)
Благодаря этому компилятор внутри метода будет рассматривать поле id (которое имеет тип T) как поле типа Number. Поэтому мы сможем внутри метода работать с id как с объектом типа Number, например,
вызвать его метод doubleValue() для получения id в виде double.
Однако мы не можем написать следующим образом:
public static void main(String[] args) {
Person<String> sam = new Person<>("A-456", "Sam");
Person<Object> alice = new Person<>(123, "Alice");
processNumericId(sam); // ! Ошибка
processNumericId(alice); // ! Ошибка
}
В первом случае (processNumericId(sam)) объект Person типизирован типом String, а String не входит в иерархию типа Number. Поэтому получаем ошибку.
Во втором случае (processNumericId(alice)) объект Person типизирован типом Object. Однако в иерархии типов тип Object располагается выше типа Number
(является его базовым классом). Поэтому также получаем ошибку.
Также мы не можем установить id, даже если будем использовать объект Number:
static void setNumericId(Person<? extends Number> person, Number id) {
person.setId(id); // ! Ошибка
}
Компилятор не знает, что именно ожидает объект — Integer или Double, поэтому не позволит установить какое-либо конкретное значение.
Wildcard, ограниченный "снизу" или ограниченный производным типом, означает "любой тип, который представляет данный тип или его базовый тип. Под базовым типом понимается любой тип,
который расположен выше по иерархии (например, для типа Integer - это Number (прямой базовый тип) и Object (непрямой базовый тип)). Задается в виде следующего выражения:
? super Type
То есть тип должен представлять сам Type, либо его базовый тип (супертип). Поэтому все типы-наследники от самого Type в данном случае отсекаются.
Допустим, нам нужен метод для обновления id, который представлять тип Integer, либо его базовый класс (Number, Object). В этом случае мы можем установить ограничение снизу в виде типа Integer:
public class Program{
public static void main(String[] args) {
Person<Integer> pInt = new Person<>(1, "Tom");
Person<Number> pNum = new Person<>(2.0, "Bob");
Person<Object> pObj = new Person<>("F-456", "Sam");
setIntegerId(pInt, 100); // OK
setIntegerId(pNum, 200); // OK
setIntegerId(pObj, 300); // OK
// проверка
System.out.println("pInt Id: " + pInt.getId()); // pInt Id: 100
System.out.println("pNum Id: " + pNum.getId()); // pNum Id: 200
System.out.println("pObj Id: " + pObj.getId()); // pObj Id: 300
}
static void setIntegerId(Person<? super Integer> person, int newId) {
// Мы можем безопасно "записать" Integer,
// так как любой супертип Integer сможет его принять.
person.setId(newId);
}
}
Здесь определен метод setIntegerId(), который получает объект Person и новое значение id для установки. Такой метод сможет работать с типами Person<Integer>, Person<Number> и Person<Object>.
Однако при чтении (например, при вызове getId()) мы можем быть уверены только в том, что получим Object, так как неизвестно, какой именно супертип используется (Integer, Number или Object):
public class Program{
public static void main(String[] args) {
Person<Integer> pInt = new Person<>(1, "Tom");
Person<Number> pNum = new Person<>(2.0, "Bob");
Person<Object> pObj = new Person<>("F-456", "Sam");
printPersonId(pInt);
printPersonId(pNum);
printPersonId(pObj);
}
static void printPersonId(Person<? super Integer> person) {
// Здесь id - это Object
Object id = person.getId();
// Integer id = person.getId(); // так мы не можем написать
System.out.println(id);
}
}
Принцип PECS (Producer Extends, Consumer Super) представляет простое правило помогает запомнить, когда какой wildcard использовать:
Producer extends: объект поставляет данные (операции чтения, как в getId()). В этом случае случае используется ? extends Type
Consumer super: объект потребляет данные (операции записи, как в setId()). В этом случае случае используется ? super Type