Skip to content

Latest commit

 

History

History
173 lines (115 loc) · 15.5 KB

JavaTypeVariance.MD

File metadata and controls

173 lines (115 loc) · 15.5 KB

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

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

Содержание:


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

Вариантность (variance) показывает как производные типы переносят наследование между их исходными типами. Например, если B - подтип A, то должен ли список List<B> быть подтипом List<A>? Или как соотносятся функции, одна из которых возвращает A, а другая - B?

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

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

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

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

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

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

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

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

    List<Integer> ints = new ArrayList<>();
    List<Number> numbers = ints; // compile error

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

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

Bounded Wildcards

Bounded Wildcards позволяют нам работать с множеством типом. Я не буду здесь затрагивать Unbounded Wildcard (?), которые по семантике схожи с raw типами, а поговорим про Bounded Wildcards - ? extends T и ? super T.

Ковариантность. Upper Bounded Wildcards

Ковариантность (covariance) - это сохранение иерархии наследования исходных типов в производных типах в том же порядке. Например, если класс Cat наследуется от класса Animal, и конструктор типов списков ковариантен, то и список List<Cat> будет потомком List<Animal>.

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

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

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<Integer> ints = new ArrayList<>();
    List<? extends Number> nums = ints;

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

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

    public long sum(List<? extends Number> 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.

Контравариантность. Lower Bounded Wildcards

Контравариантность (contravariance) - это обращение иерархии исходных типов на противоположную в производных типах. Например, если класс Cat наследуется от класса Animal, и конструктор типов функций контрвариантен, то функция Animal -> String является потомком функции Cat -> String.

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

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

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<Number> nums = new ArrayList<>();
    List<? super Integer> 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 <em>extends</em>: то есть коллекция только производит элементы, но не принимает их.

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

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

Полезные ресурсы