Привіт! Продовжуємо вивчати тему дженеріків.
Ти вже маєш солідний багаж знань про них з попередніх лекцій (про використання varargs при роботі з дженеріками і про стиряння типів), але одну важливу тему ми поки що не розглядали — wildcards.
Це дуже важлива фішка дженеріків. Настільки, що ми виділили для цього окрему лекцію! Втім, нічого складного у wildcards немає, в цьому ти зараз переконаєшся :)
Давай розглянемо приклад:
Давай розглянемо приклад:
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 лекції! Зате тепер ти непогано орієнтуєшся у темі й зможеш проявити себе на співбесіді :)
А зараз — саме час повернутися до задач!
Успіхів у навчанні! :)
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ