JavaRush /Java блог /Java Developer /Wildcards в Java Generics
Автор
Артем Divertitto
Senior Android-разработчик в United Tech

Wildcards в Java Generics

Статья из группы Java Developer
Привет! Продолжаем изучать тему дженериков. Ты уже обладаешь солидным багажом знаний о них из предыдущих лекций (об использовании 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 лекции! Зато теперь ты неплохо ориентируешься в теме и сможешь проявить себя на собеседовании :) А сейчас — самое время вернуться к задачам! Успехов в обучении! :)
Комментарии (46)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
tsushko karina Уровень 32
10 января 2024
куда делся класс Dog?в описании классов и иерархии забыли собаку😢
Anonymous #3148644 Уровень 2
12 сентября 2023
Объясните пжл почему нельзя использовать конструкцию: public class MyGenerics<? super Animal> или public class MyGenerics<? extends Animal>?
12 августа 2022
>> Ситуация выглядит не очень хорошо для нас! Получается, нам придется писать отдельные методы для перебора всех видов животных? А если использовать полиморфизм? Вместо:

List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Cat());
cats.add(new Cat());
cats.add(new Cat());
Написать:

List<Animal> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Cat());
cats.add(new Cat());
cats.add(new Cat());
И не нужно писать отдельные методы для перебора всех видов животных
Maksim Tatarintsev Уровень 37
18 июля 2022
Кому не очень понятно про <? extends A> и <? super A>, предлагаю скопировать пример ниже в IDEA и рассмотреть допустимые значения:

import java.util.ArrayList;
import java.util.List;
 
public class Test {
    List list;
    List list1;
    static class C{}
    static class A extends C{}
    static class B extends A{}
    static class D extends B{}
 
    public static void main(String[] args) {
        Test test = new Test();
 
        List<A> listA = new ArrayList<>();
        List<B> listB = new ArrayList<>();
        List<C> listC = new ArrayList<>();
        List<D> listD = new ArrayList<>();
 
        test.setList(listA);
        test.setList(listB);
        test.setList(listD);
        test.setList(listC);
 
        test.setList1(listA);
        test.setList1(listB);
        test.setList1(listD);
        test.setList1(listC);
    }
 
 
    void setList(List<? extends A> spisok){
        this.list = spisok;
    }
    void setList1(List<? super A> spisok){
        this.list1 = spisok;
    }
}
Anonymous #3068853 Уровень 3
28 мая 2022
Вопрос к первому примеру. Перепишем его так:

public class Main {

    public static void main(String[] args) {

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

        List<String> strings = new ArrayList<String>();
        List objects = strings;
        objects.add(new Object());
    }
}
Тут никакой ошибки не возникает, только предупреждения. Почему компилятор не запрещает так делать, чтобы обеспечить типобезопасность?
LuneFox Уровень 41 Expert
26 января 2022

public static void iterateAnimals(Collection<? super Cat> animals) { ... }
Получается, что этим же методом я смогу итерировать даже просто список любых Object. А как сделать, чтобы можно было итерировать только то, что ниже Animal и выше Cat?
Karoshi Уровень 13
18 декабря 2021
Так и что такое wildcard?
Shaman_2010 Уровень 25
12 ноября 2021
Не совсем понятно в чем разница между:

public static void iterateAnimals(Collection<? extends Animal> animals) {
   for(Animal animal: animals) {
       System.out.println("Еще один шаг в цикле пройден!");
   }
}
и

public static <T extends Animal> void iterateAnimals(Collection<T> animals) {
   for(T animal: animals) {
       System.out.println("Еще один шаг в цикле пройден!");
   }
}
Максим Уровень 37
30 марта 2021
Наконец то просветление... вроде
Алексей Серов Уровень 27
23 сентября 2020
То есть он должен принимать на вход Collection<Animal>, Collection<Pet> или Collection<Car>, но не должен работать с Collection<Dog>. Cat...