Привет! Продолжаем изучать тему дженериков.
Ты уже обладаешь солидным багажом знаний о них из предыдущих лекций (об использовании 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 лекции! Зато теперь ты неплохо ориентируешься в теме и сможешь проявить себя на собеседовании :)
А сейчас — самое время вернуться к задачам!
Успехов в обучении! :)
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ