Крім стандартного наслідування в 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 корисний у таких випадках:
- Коли метод має читати дані з колекції, але не змінювати їх.
- Коли потрібно створити універсальний інтерфейс для роботи з різними типами колекцій.
- Коли необхідно гарантувати, що метод не внесе змін у колекцію, і тим самим забезпечити захист даних.
Таким чином, <?> допомагає зробити код гнучкішим, підтримуючи різноманітні типи даних, і водночас захищає від небажаних змін у структурі даних, з якими працює метод.
курси Junior саме для вас.
Обмежений 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? - Сміливо задавайте нижче! 💬