JavaRush /Java блог /Random UA /Wildcards у Java Generics

Wildcards у Java Generics

Стаття з групи Random UA
Вітання! Продовжуємо вивчати тему дженериків. Ти вже маєш солідний багаж знань про них з попередніх лекцій (про використання varargs при роботі з дженериками і про стирання типів ), але одну важливу тему ми поки не розглядали — wildcards . Це дуже важлива фішка дженериків. Так, що ми виділабо для неї окрему лекцію! Втім, нічого складного у wildcards немає, у цьому ти зараз переконаєшся:) Wildcards в generics - 1Давай розглянемо приклад:
public class Main {

   public static void main(String[] args) {

       String str = new String("Test!");
       // нияких проблем
       Object obj = str;

       List<String> strings = new ArrayList<String>();
       // ошибка компиляции!
       List<Object> objects = strings;
   }
}
Що тут відбувається? Ми бачимо дві дуже схожі ситуації. У першій ми намагаємося привести об'єкт Stringдо типу Object. Жодних проблем із цим не виникає, все працює як треба. Але в другій ситуації компілятор видає помилку. Хоча, здавалося б, ми робимо те саме. Просто тепер ми використовуємо колекцію кількох об'єктів. Але чому виникає помилка? Яка, по суті, різниця – наводимо ми один об'єкт Stringдо типу Objectчи 20 об'єктів? Між об'єктом та колекцією об'єктів є важлива відмінність . Якщо клас Bє спадкоємцем класу А, то Collection<B>при цьому не спадкоємець Collection<A>. Саме з цієї причини ми не змогли привести наш List<String>до List<Object>.Stringє спадкоємцем Object, але List<String>не є спадкоємцем List<Object>. Інтуїтивно це не дуже логічно. Чому саме таким принципом керувалися творці мови? Давайте припустимо, що тут компілятор не видавав би нам помилку:
List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
У цьому випадку, ми могли б, наприклад, зробити наступне:
objects.add(new Object());
String s = strings.get(0);
Оскільки компілятор не видав нам помилок і дозволив створити посилання List<Object> objectна колекцію рядків strings, можемо додати stringsне рядок, а просто будь-який об'єкт Object! Таким чином, ми втратабо гарантію того, що в нашій колекції знаходяться лише зазначені в дженериці об'єктиString . Тобто ми втратабо головну перевагу дженериків — типобезпеку. І якщо компілятор дозволив нам все це зробити, значить, ми отримаємо помилку лише під час виконання програми, що завжди набагато гірше, ніж помилка компіляції. Щоб запобігти таким ситуаціям, компілятор видає нам помилку:
// ошибка компиляции
List<Object> objects = strings;
...і нагадує, що List<String>не спадкоємець List<Object>. Це залізне правило роботи дженериків, і його необхідно обов'язково пам'ятати при їх використанні. Поїхали далі. Допустимо, у нас є невелика ієрархія класів:
public class Animal {

   public void feed() {

       System.out.println("Animal.feed()");
   }
}

public class Pet extends Animal {

   public void call() {

       System.out.println("Pet.call()");
   }
}

public class Cat extends Pet {

   public void meow() {

       System.out.println("Cat.meow()");
   }
}
На чолі ієрархії стоять просто Тварини: від них успадковуються Домашні Тварини. Домашні Тварини діляться на 2 типи - Собаки та Кішки. А тепер уяви, що нам потрібно створити простий метод iterateAnimals(). Метод повинен приймати колекцію будь-яких тварин ( Animal, Pet, Cat, Dog), перебирати всі елементи, і щоразу виводити що-небудь у консоль. Давай спробуємо написати такий метод:
public static void iterateAnimals(Collection<Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
Здавалося б, завдання вирішено! Однак, як ми нещодавно з'ясували, List<Cat>чи List<Dog>не List<Pet>є спадкоємцями List<Animal>! Тому при спробі викликати метод iterateAnimals()зі списком котиків ми отримаємо помилку компілятора:
import java.util.*;

public class Main3 {


   public static void iterateAnimals(Collection<Animal> animals) {

       for(Animal animal: animals) {

           System.out.println("Еще один шаг в цикле пройден!");
       }
   }

   public static void main(String[] args) {


       List<Cat> cats = new ArrayList<>();
       cats.add(new Cat());
       cats.add(new Cat());
       cats.add(new Cat());
       cats.add(new Cat());

       //ошибка компилятора!
       iterateAnimals(cats);
   }
}
Ситуація виглядає не дуже добре для нас! Виходить нам доведеться писати окремі методи для перебору всіх видів тварин? Насправді ні, не доведеться :) І в цьому нам якраз допоможуть wildcards ! Ми вирішимо задачу в рамках одного простого методу, використовуючи таку конструкцію:
public static void iterateAnimals(Collection<? extends Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
Це і є wildcard. Точніше, це перший із кількох типів wildcard - " extends " (інша назва - Upper Bounded Wildcards ). Про що нам каже ця конструкція? Це означає, що спосіб приймає на вхід колекцію об'єктів класу Animalабо будь-якого класу-спадкоємця Animal (? extends Animal). Іншими словами, метод може прийняти на вхід колекцію Animal, Pet, Dogабо Cat- не має значення. Давай переконаємося, що це працює:
public static void main(String[] args) {

   List<Animal> animals = new ArrayList<>();
   animals.add(new Animal());
   animals.add(new Animal());

   List<Pet> pets = new ArrayList<>();
   pets.add(new Pet());
   pets.add(new Pet());

   List<Cat> cats = new ArrayList<>();
   cats.add(new Cat());
   cats.add(new Cat());

   List<Dog> dogs = new ArrayList<>();
   dogs.add(new Dog());
   dogs.add(new Dog());

   iterateAnimals(animals);
   iterateAnimals(pets);
   iterateAnimals(cats);
   iterateAnimals(dogs);
}
Виведення в консоль:

Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Ми створабо 4 колекції і 8 об'єктів, і в консолі рівно 8 записів. Все чудово працює! :) Wildcard дозволив нам легко вмістити потрібну логіку з прив'язкою до конкретних типів в один спосіб. Ми позбавабося необхідності писати окремий метод для кожного виду тварин. Уяви, скільки методів у нас було б, якби наш додаток використовувався в зоопарку чи ветеринарній клініці:) А тепер давай розглянемо іншу ситуацію. Наша ієрархія успадкування залишиться незмінною: клас верхнього рівня Animal, трохи нижче - клас свійських тварин Pet, а на наступному рівні - Catі Dog. Тепер тобі потрібно переписати метод iretateAnimals()таким чином, щоб він міг працювати з будь-яким типом тварин, окрім собак . Тобто він має приймати на вхідCollection<Animal>, Collection<Pet>або Collection<Cat>, але не повинен працювати з Collection<Dog>. Як ми можемо цього досягти? Здається, перед нами знову замаячила перспектива писати окремий метод для кожного типу. Як інакше пояснити компілятору нашу логіку? А зробити це можна просто! Тут нам знову прийдуть на допомогу wildcards. Але цього разу ми скористаємося іншим типом - " super " (інша назва - Lower Bounded Wildcards ).
public static void iterateAnimals(Collection<? super Cat> animals) {

   for(int i = 0; i < animals.size(); i++) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
Тут принцип схожий. Конструкція <? super Cat>говорить компілятору, що спосіб iterateAnimals()може приймати на вхід колекцію об'єктів класу Catчи іншого класу-предка Cat. Під цей опис у нашому випадку підходять сам клас Cat, його предок - Petsі предок предка - Animal. Клас Dogне вписується в це обмеження, тому спроба використовувати метод зі списком List<Dog>призведе до помилки компіляції:
public static void main(String[] args) {

   List<Animal> animals = new ArrayList<>();
   animals.add(new Animal());
   animals.add(new Animal());

   List<Pet> pets = new ArrayList<>();
   pets.add(new Pet());
   pets.add(new Pet());

   List<Cat> cats = new ArrayList<>();
   cats.add(new Cat());
   cats.add(new Cat());

   List<Dog> dogs = new ArrayList<>();
   dogs.add(new Dog());
   dogs.add(new Dog());

   iterateAnimals(animals);
   iterateAnimals(pets);
   iterateAnimals(cats);

   //ошибка компиляции!
   iterateAnimals(dogs);
}
Наше завдання вирішене, і знову wildcards виявабося вкрай корисними :) На цьому лекція добігла кінця. Тепер ти бачиш, наскільки важлива тема дженериків при вивченні джаву — у нас пішло на неї 4 лекції! Зате тепер ти непогано орієнтуєшся в темі і зможеш проявити себе на співбесіді :) А зараз - саме час повернутися до завдань! Успіхів у навчанні! :)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ