Скидка 20% на курс GRASP & GOF Design Patterns
Узнать больше
10.12.2024
7 минут чтения

Wildcard в Generics

Кроме стандартного наследования в generics, в Java существует еще один мощный инструмент — wildcard. Он позволяет работать с обобщенными типами еще более гибко, особенно когда нужно обрабатывать методы, принимающие параметры разных типов, или передавать коллекции с разными типами данных. Wildcard помогает избежать ограничений, которые возникают из-за жесткого определения типа в generics, делая код более универсальным и адаптивным. Более углубленные знания в языке программирования JAVA можно получить вместе с академией FoxmindEd.

Хотите освоить профессию Java Developer? Присоединяйтесь к программе «От 0 до Strong Java Junior за 12 месяцев». Воспользуйтесь выгодным предложением от 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 с верхним пределом полезно в следующих случаях:

  1. Для универсальных методов, которые работают с группами объектов, имеющих общего предка: Если метод только читает данные, вы можете объявить параметр как <? extends Type>, чтобы сделать его более гибким.
  2. Когда нужно гарантировать безопасность типов во время чтения данных: В таких случаях мы можем быть уверены, что все объекты в коллекции имеют тип, который наследует Type, что позволяет избежать ошибок при вызове методов базового класса.
  3. Обеспечение поддержки полиморфизма: Это позволяет использовать полиморфизм при работе с различными объектами, имеющими общий родительский класс, без необходимости создавать отдельные методы для каждого подтипа.
Подпишитесь на наш Ютуб-канал! Полезные видео для программистов уже ждут вас! YouTube
Выберите свой курс! Путь к карьере программиста начинается здесь! Посмотреть

Ограниченный 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. Ждем тебя!!!

FAQ
Что такое wildcard в generics?

Wildcard - это символ, который означает неопределенный тип, позволяющий методам и классам работать с различными типами данных, не конкретизируя их.

В чем различие между wildcard и raw type?

Wildcard позволяет работать с объектами, гарантируя безопасность типов (можно только читать данные, кроме null). Raw type лишает компилятора информации о типе, что может приводить к ошибкам.

Когда используют неограниченный wildcard ?

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

Что такое ограниченный wildcard с верхней границей (? extends Type)?

Это тип, который позволяет работать с объектами, которые являются подтипами Type. Используется для чтения данных из коллекций.

Что такое ограниченный wildcard с нижней границей (? super Type)?

Это тип, который позволяет добавлять объекты определенного типа или его подтипов в коллекцию, содержащую суперклассы этого типа.

Когда использовать "? extends Type" и "? super Type"?

"? extends Type" подходит для чтения (данные остаются неизменными). "? super Type" используется, когда нужно добавлять объекты в коллекцию.

🤔 Остались вопросы о Wildcard в Generics? - смело задавайте ниже! 💬

Добавить комментарий

Ваш имейл не будет опубликован. Обязательные поля отмечены *

Сохранить моё имя, имейл и адрес сайта в этом браузере для будущих комментариев