Кроме стандартного наследования в generics, в Java существует еще один мощный инструмент — wildcard. Он позволяет работать с обобщенными типами еще более гибко, особенно когда нужно обрабатывать методы, принимающие параметры разных типов, или передавать коллекции с разными типами данных. Wildcard помогает избежать ограничений, которые возникают из-за жесткого определения типа в generics, делая код более универсальным и адаптивным. Более углубленные знания в языке программирования JAVA можно получить вместе с академией FoxmindEd.
В предыдущей части статьи мы рассматривали особенности наследования в generics в Java и разбирали такое ограничение типов, как T extends Type, что позволяет контролировать параметры типов во время объявления класса или метода. Однако, такой подход предполагает, что тип будет четко определен еще на этапе объявления. Например, как в нашем классе Container:
@Data
class Container<T extends Animal> {
private T item;
}
Мы уже на этапе создания класса определяем, что класс работает со всеми наследниками Animal. И при создании экземпляра контейнера, мы должны определиться с типом, который он будет принимать:
Container<Dog> dogContainer = new Container<>(new Dog());
Но бывают ситуации, когда жесткое ограничение на определенный тип может мешать. Например, мы хотим, чтобы класс, метод или массив работали с разными объектами. Для таких случаев в Java предусмотрен еще один механизм — wildcard.
Wildcard в generics — это специальный символ <?>, который используется для обозначения неопределенного типа. Он позволяет объявить параметр типа, не конкретизируя, каким именно он будет. Использование wildcard делает ваш код более гибким, позволяя методам и классам работать с широким диапазоном типов, вместо того чтобы заранее ограничивать себя. Они удобны тогда, когда нужно обрабатывать объекты, которые могут принадлежать к разным классам, но вы хотите сохранить гибкость и избежать ошибок типов. Например, если у вас есть метод, работающий с коллекциями, вы можете использовать wildcard, чтобы этот метод мог принимать коллекции разных типов, а не только с одним конкретным типом.
Типы Wildcard в Generics
В Java есть три основных вида wildcard, каждый из которых используется для разных ситуаций и задач. Рассмотрим их более подробно.
Неограниченный Wildcard (<?>)
Неограниченный wildcard используется, когда тип не важен, и метод или класс может работать с любым типом. Это наиболее универсальный вариант wildcard, который позволяет принимать любой тип данных.
Приведу простой пример. У нас есть контейнер, который мы параметризовали как <?>:
@NoArgsConstructor
@Data
class Container<T> {
private T item;
}
Container<?> containerWildcart = new Container<>();
Однако, чем же это отличается от raw type?
Container containerRowType = new Container();
Отличие заключается в том, что в containerWildcart мы не можем положить ничего кроме null, поскольку компилятор не знает, какой именно тип объекта он хранит. Однако вы можете получить объекты из этого контейнера как объекты типа Object. А когда мы пишем просто Container containerRowType = new Container();, это означает, что мы не используем generics вообще. Это обратная совместимость с кодом, написанным до введения generics в Java (до Java 5).
Container<?> containerWildcart = new Container<>();
containerWildcart.setItem(null); // Единственное, что можно добавить - это null
containerWildcart.setItem("Letter to Olga"); // будет ошибка компиляции
Object item = containerWildcart.getItem(); // Мы можем получить объект, но тип будет Object
Container containerRowType = new Container();
// можем класть в Bag все, что угодно
containerRowType.setItem(null);
containerRowType.setItem("Letter to Olga");
Возникает вопрос: если мы не можем добавлять элементы в Container<?>, то в чем смысл использования неограниченного wildcard? На самом деле, этот подход очень полезен, когда нужно создать метод, работающий с различными типами, но не изменяющий их содержимое.
Например:
public void printItems(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
Этот метод может работать с любым списком, независимо от того, является ли он List<String>, List<Integer>, или даже List<CustomClass>. Благодаря использованию <?> компилятор позволяет нам читать из коллекции, но не позволяет добавлять в нее новые элементы (кроме null). Это гарантирует, что метод не изменит данные, хранящиеся в списке, что особенно важно для функций, предназначенных только для чтения.
List<String> stringList = Arrays.asList("Apple", "Banana", "Cherry");
List<Integer> integerList = Arrays.asList(1, 2, 3);
printItems(stringList); // Выведет: Apple, Banana, Cherry
printItems(integerList); // Выведет: 1, 2, 3
Неограниченный wildcard полезен в таких случаях:
- Когда метод должен читать данные из коллекции, но не изменять их.
- Когда нужно создать универсальный интерфейс для работы с разными типами коллекций.
- Когда необходимо гарантировать, что метод не внесет изменений в коллекцию, и тем самым обеспечить защиту данных.
Таким чином, <?> помогает сделать код более гибким, поддерживая различные типы данных, и одновременно защищает от нежелательных изменений в структуре данных, с которыми работает метод.
Ограниченный Wildcard с верхним пределом
Ограниченный wildcard с верхним пределом (<? extends Type>) в Java используется для обозначения, что параметр типа может быть любым подтипом (классом-потомком) конкретного типа Type. Это позволяет создавать более гибкие методы и классы, которые могут обрабатывать не только объекты определенного типа, но и объекты, которые его наследуют.
Этот тип wildcard обычно используется в методах, которые только читают данные из коллекции, но не изменяют ее. Причина заключается в том, что, объявив параметр как <? extends Type>, мы говорим компилятору, что тип может быть любым подтипом Type, но мы не знаем точно, каким именно. Это ограничивает возможность записи новых элементов в такую коллекцию, поскольку компилятор не может гарантировать, что тип элемента, который мы добавляем, будет соответствовать всем возможным подтипам.
Предположим, у нас есть класс Animal и его подклассы Dog и Cat. Если мы хотим создать метод, который обрабатывает список любых животных, но только для чтения, мы можем использовать ограниченный wildcard с верхним пределом.
class Animal {
void sound() {
System.out.println("Some sound...");
}
}
class Dog extends Animal {
void sound() {
System.out.println("Bark");
}
}
class Cat extends Animal {
void sound() {
System.out.println("Meow");
}
}
public class Main {
public static void main(String[] args) {
List<Dog> dogs = Arrays.asList(new Dog(), new Dog());
List<Cat> cats = Arrays.asList(new Cat(), new Cat());
List<Animal> animal = List.of(new Animal());
doSoundAnimals(dogs); // Выведет: Bark Bark
doSoundAnimals(cats); // Выведет: Meow Meow
doSoundAnimals(animal); // Выведет: Some sound...
doSoundAnimals(new ArrayList<String>()); // ошибка компиляции, потому что String не наследует Animal
}
public static void doSoundAnimals(List<? extends Animal> animals) {
for (Animal animal : animals) {
animal.sound(); // Вызывает метод sound() каждого объекта
}
}
public static void addAnimal(List<? extends Animal> animals) {
animals.add(new Cat()); // Ошибка компиляции
}
}
В примере выше метод doSoundAnimals принимает List<? extends Animal>, что означает, что этот метод может работать с List объектов любого типа, который является подтипом Animal, таких как List<Dog> или List<Cat>. В методе мы можем получать объекты из коллекции и вызывать их методы, однако не можем добавлять новые объекты в коллекцию, ведь тип ? неопределен с точки зрения компилятора.
Использование ограниченного wildcard с верхним пределом полезно в следующих случаях:
- Для универсальных методов, которые работают с группами объектов, имеющих общего предка: Если метод только читает данные, вы можете объявить параметр как <? extends Type>, чтобы сделать его более гибким.
- Когда нужно гарантировать безопасность типов во время чтения данных: В таких случаях мы можем быть уверены, что все объекты в коллекции имеют тип, который наследует Type, что позволяет избежать ошибок при вызове методов базового класса.
- Обеспечение поддержки полиморфизма: Это позволяет использовать полиморфизм при работе с различными объектами, имеющими общий родительский класс, без необходимости создавать отдельные методы для каждого подтипа.
Ограниченный Wildcard с нижним пределом
Ограниченный wildcard с нижним пределом (<? super Type>) в Java используется, когда мы хотим добавлять объекты определенного типа или его «потомков» в коллекцию. При этом нам неважно, какие объекты уже есть в этой коллекции, главное, чтобы они были «старшими» или «равными» нужному нам типу. Класс Animal является «старшим» типом для Dog и Cat, что позволяет нам использовать <? super Dog> или <? super Cat> для такой коллекции.
В отличие от wildcard с верхним пределом (<? extends Type>), который позволяет только чтение из коллекции, wildcard с нижней границей (<? super Type>) обеспечивает возможность записи в коллекцию. Это делает его идеальным для ситуаций, когда мы хотим добавлять элементы в коллекцию, но не будем читать или обрабатывать конкретный тип уже хранящихся элементов.
public class Main {
public static void main(String[] args) {
List<Object> items = new ArrayList<>();
List<CharSequence> names = new ArrayList<>();
List<String> usernames = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
// Мы можем передать любой из этих списков
addName(items); // Добавляем имя в список Object
addName(names); // Добавляем имя в список CharSequence
addName(usernames); // Добавляем имя в список String
addName(integers); // Ошибка компиляции, потому что Integer не суперкласс String
}
public static void addName(List<? super String> list) {
list.add("Olga");
list.add("Ivan"); }
}
Мы используем List<? super String>, чтобы иметь возможность передавать в метод списки, которые могут содержать объекты String или их суперклассы, такие как CharSequence или Object. Этот подход полезен, когда мы не хотим ограничиваться только String, но и иметь возможность передать другие совместимые типы (например, CharSequence).
Таким образом, wildcard с нижней границей позволяет нам работать с объектами разных уровней иерархии, делая методы более универсальными и удобными для добавления элементов в коллекции, где не всегда известен точный тип. Обращайся в академию FoxmindEd и регистрируйся на курс JAVA. Ждем тебя!!!
🤔 Остались вопросы о Wildcard в Generics? - смело задавайте ниже! 💬