Java

Java Generics — Bounded Wildcards (ковариантность и контравариантность)

В данной статье мы обсудим инвариантность, ковариантность и контравариантность Java, а также разберемся при чем здесь дженерики и Bounded Wildcards.

Вариантность

Вариантность (variance) показывает как производные типы наследуются в зависимости от наследования между их исходными типами.

Производные типы — это контейнеры, делегаты и прочие классы, которые оперируют другими типами внутри себя. Например, List<Integer> — это производный тип, а Integer — это исходный тип.

Инвариантность дженериков

Дженерики в Java — инвариантны.

Инвариантность — это отсутствие наследования между производными типами. Допустим, если объявлен список List<T>, то он может хранить только элементы типа T и никакие другие.

Это вызывает некоторые проблемы. Например, иногда мы хотим работать с некоторым множеством типов, но не одним типом.

Допустим, мы имеем такую функцию:

    public long sum(List numbers) {
        long sumaccum = 0;
        for (Number number : numbers) {
            sumaccum += number.longValue();
        }
        return sumaccum;
    }

Мы не можем передать в нее список типа List<Integer>, так как дженерики инвариантны. То есть мы не можем делать такое присваивание:

        List ints = new ArrayList<>();
        List numbers = ints;

так как между производными типами отсутствует наследование, хотя оно присутствует в исходных типах: Integer — это подтип Number.

Итак, проблема: инвариантность дженериков сильно ограничивает возможности полиморфизма и нам нужно какое-то решение.

Bounded Wildcards

Я не буду здесь затрагивать Unbounded Wildcard, которые по семантике схожи с Raw типами, а поговорим про Bounded Wildcards.

Bounded Wildcards позволяют нам работать с множеством типом, а не конкретным типом.

Ковариантность

Ковариантность — это сохранение иерархии наследования исходных типов в производных типах в том же порядке.

Java предоставляет ковариантность с помощью конструкции <? extends T, где T — некоторый базовый тип. Такая запись называется wildcard с верхней границей и она обозначает множество типов, состоящее из самого типа T и наследующих его типов.

Разберем на примере. Пусть у нас есть следующая иерархия типов:

Object
  |
Number (верхняя граница)
  |
Integer

Тогда множество <? extends Number> имеет верхнюю границу Number, а значит содержит типы ниже или равно этой границы: Number, Integer, Double и остальные типы, которые наследуют Number.

Итак, зная верхнюю границу типа, мы гарантированно знаем, что, тип элемента — это сама граница или тип, унаследованный от неё.

Мы не знаем точно, какой это тип, однако гарантированно знаем, что мы можем работать с этим элементом как с родителем (Number), так как полиморфизм позволяет работать с ребенком как с родителем, то есть кастить к верхнему типу по иерархии (например Integer скастить к Number).

Вот пример:

        Integer i = 1;
        Number n = i;

Итак, <? extends T> ковариантен. Это означает, что если Integer — подтип Number, то и List<Integer> — подтип List<? extends Number>. Тогда валидно такое присваивание:

        List ints = new ArrayList<>();
        List nums = ints;

Мы видим, что иерархия наследования исходных типов сохранилась. Нам гарантируется, что в списке лежат элементы, чей тип — это Number или унаследованный от Number, то есть мы точно знаем их верхнюю границу.

Теперь мы можем переписать нашу функцию подсчета суммы элементов следующим образом:

    public long sum(List numbers) {
        long sumaccum = 0;
        for (Number number : numbers) {
            sumaccum += number.longValue();
        }
        return sumaccum;
    }

Теперь в нашей функции мы можем работать с любыми типами, которые наследуют Number. Нам не важно, какие именно это типы (Integer, Double и т.п.), нам важно только то, что их верхняя граница — это Number, то есть мы можем работать с ними через интерфейс их базового класса, который является Number.

Однако, мы можем только читать из такого списка, но не добавлять элементы в него, причём запись запрещена на стадии компиляции. Так работает потому, что мы не знаем точного типа элементов в списке, ведь данный список может хранить элементы как типа Integer, так и типа Double. Если бы запись была возможна, то мы могли бы получить Heap Pollution.

Heap Pollution — это ситуация, когда какая-то переменная определённого типа ссылается на объект совсем иного типа. При такой попытке присвоения мы получаем ClassCastException.

Контравариантность

Контравариантность — это обращение иерархии исходных типов на противоположную в производных типах.

Java предоставляет возможности контравариантности с помощью конструкции <? super T>, где T — некоторый базовый тип. Такая запись называется wildcard с нижней границей и она обозначает множество типов, состоящее из самого типа T и его супертипов.

Для примера вернемся к иерархии типов из прошлого примера:

Object
  |
Number
  |
Integer (нижняя граница)

Тогда множество <? super Integer> имеет нижнюю границу Integer и включает в себя типы выше или равно этой границы: Integer, Number, Object.

Итак, зная нижнюю границу исходного типа, мы гарантированно знаем, что исходный тип по иерархии стоит выше или равно этой границы, но не наследует границу. Опять же, мы не знаем точно сам исходный тип, однако нам это и не требуется. Все, что важно — это то, что исходный тип стоит по иерархии выше или равно границы.

Это дает нам возможность добавлять в производный тип элементы типа Integer или ниже его. Допустим, к иерархии добавляется еще один тип (в Java Integer определен как финальный, но сделаем допущение):

Object
  |
Number
  |
Integer (нижняя граница)
  |
SubtypeInteger

Тогда мы можем добавить в список типа List<? super Integer> элементы как типа Integer, так и SubtypeInteger. Но мы не можем добавить в этот список элемент типа Number, так как не знаем точный тип элементов в списке. Так могут лежать элементы как типа Object, так и типа Integer. И если тип Number кастится к Object, то скастить к Integer уже нельзя. Но мы можем добавлять элементы типа Integer и унаследованные от него, так как они могут безопасно скаститься к типу выше по иерархии.

Итак, мы помним, что Integer — это подтип Number. Но в силу контравариантности верно, что List<Number> — это подтип List<? super Integer>. Это означает, что такое присваивание валидно:

        List nums = new ArrayList<>();
        List ints = nums;

Так как List<? super Integer> — это родитель, то мы имеем возможность скастить List<Number> к родителю. Действительно, если список nums содержит элементы типа Number, то они безопасно помещаются в список элементов, где исходный тип должен по иерархии стоять выше Integer.

Мы можем только писать в такой список, но не читать. Мы не имеем возможности читать из такого списка, так как, опять же, не знаем точный тип элементов в массиве. Там могут лежать элементы как типа Integer, так и Object, поэтому мы не знаем, какой тип указать при чтении. Если бы чтение было возможным, то в ран-тайме мы могли бы получить ситуацию Heap Pollution с соответствующим выпадением ClassCastException. Например, усли укажем тип ссылки Integer, но указывать она будет на элемент типа Object, то выпадет ClassCastException, поэтому чтение запрещено на стадии компиляции.

PECS

Bounded Wildcards следует использовать на входных параметрах функций для более гибкого API (как мы видели в примере с подсчетом суммы).

Существует простое правило PECS (Producer extends and Consumer super) для использования нужного типа bounded wildcard и более безопасного использования типов.

Если коллекция предназначена только для чтения, то её следует объявлять как List<? extends T>. Из такой коллекции мы можем читать элементы типа T, но не можем писать в неё и, соответственно, тем самым избегаем Heap Pollution. Здесь мы видим первую часть правила: Producer extends: то есть коллекция только производит элементы, но не принимает их.

Если же коллекция предназначена только для записи, то её следует объявлять как List<? super T>. В такую коллекцию мы можем только писать элементы типа T или унаследованные от него, но не можем читать из неё, и тем самым избегаем Heap Pollution. Здесь мы видим вторую часть правила: Consumer super: коллекция только принимает элементы, но не производит их.

Если же коллекция предназначена и для чтения, и для записи, то никакого ограничения накладывать не надо. Просто объявите тип списка как List<T>.

Java Generics — Bounded Wildcards (ковариантность и контравариантность): 15 комментариев

  1. Прекрасная статья. Единственное, почему-то в примерах типы написаны без generic-параметров:
    List ints = new ArrayList();
    List nums = ints;
    Было бы удобнее, если бы было
    List ints = new ArrayList();
    List nums = ints;

  2. Добрый день, коллега!

    Хотите узнать Всех контрагентов Ваших конкурентов?!
    Мы вам в этом поможем. Напишите нам по контактам ниже и получите информацию о том Кто, Что, Когда и по какой Цене покупал или продавал конкурирующей компании.
    Тем самым вы открываете для себя уникальную возможность сделать более выгодное предложение своим будущим контрагентам или получить лучшего Поставщика.

    Наглядный пример отправим по запросу.

    На эту почту отвечать не нужно.
    Пишите по контактам ниже:

    Email: konsaltplus24@gmail.com
    Telegram: @Consalting24

    P.S.: Увеличьте свои продажи за счёт информации о контрагентах ваших конкурентов!

Добавить комментарий

Ваш адрес email не будет опубликован.