Типы

Java - это язык со статической типизацией, что означает, что вы должны сначала объявить переменную и ее тип, прежде чем использовать ее.

Например: int myInteger = 42;

Введите общие типы.

Универсальные типы

Определение: Универсальный тип - это универсальный класс или интерфейс, параметризованный по типам.

По сути, универсальные типы позволяют вам написать общий универсальный класс (или метод), который работает с разными типами, что позволяет повторно использовать код.

Вместо того, чтобы указывать obj как тип int, тип String или любой другой тип, вы определяете класс Box для принятия параметра типа <T>. Затем вы можете использовать T для представления этого универсального типа в любой части вашего класса.

Теперь введите ковариацию и контравариантность.

Ковариация и контравариантность

Определение

Дисперсия относится к тому, как выделение подтипов между более сложными типами связано с подтипами между их компонентами (источником).

Легко запоминающееся (и крайне неформальное) определение ковариации и контравариантности:

  • Ковариация: принять подтипы
  • Контравариантность: принимать супертипы

Массивы

В Java массивы ковариантны, что имеет два следствия.

Во-первых, массив типа T[] может содержать элементы типа T и его подтипы.

Number[] nums = new Number[5];
nums[0] = new Integer(1); // Ok
nums[1] = new Double(2.0); // Ok

Во-вторых, массив типа S[] является подтипом T[], если S является подтипом T.

Integer[] intArr = new Integer[5];
Number[] numArr = intArr; // Ok

Однако важно помнить, что: (1) numArr - это ссылка ссылочного типа Number[] на «фактический объект» intArr «фактического типа» Integer[].

Следовательно, следующая строка будет компилироваться нормально, но создаст среду выполнения ArrayStoreException (из-за загрязнения кучи):

numArr[0] = 1.23; // Not ok

Это создает исключительную ситуацию во время выполнения, потому что во время выполнения Java знает, что «фактический объект» intArr на самом деле является массивом Integer.

Дженерики

С универсальными типами Java не имеет возможности узнать во время выполнения информацию о типе параметров типа из-за стирания типа. Следовательно, он не может защитить от загрязнения кучи во время выполнения.

Таким образом, универсальные шаблоны неизменны.

ArrayList<Integer> intArrList = new ArrayList<>();
ArrayList<Number> numArrList = intArrList; // Not ok
ArrayList<Integer> anotherIntArrList = intArrList; // Ok

Параметры типа должны точно совпадать для защиты от загрязнения кучи.

Но введите подстановочные знаки.

Подстановочные знаки, ковариация и контравариантность

С помощью подстановочных знаков универсальные шаблоны могут поддерживать ковариацию и контравариантность.

Изменяя предыдущий пример, мы получаем вот что, и оно работает!

ArrayList<Integer> intArrList = new ArrayList<>();
ArrayList<? super Integer> numArrList = intArrList; // Ok

Знак вопроса "?" относится к подстановочному знаку, представляющему неизвестный тип. Он может быть ограниченным снизу, что ограничивает неизвестный тип определенным типом или его супертипом.

Следовательно, в строке 2 ? super Integer переводится как «любой тип, являющийся целочисленным типом или его супертипом».

Вы также можете ограничить верхний предел подстановочного знака, который ограничивает неизвестный тип определенным типом или его подтипом, используя ? extends Integer.

Только для чтения и только для записи

Ковариация и контравариантность дают интересные результаты. Ковариантные типы доступны только для чтения, а контравариантные - только для записи.

Помните, что ковариантные типы принимают подтипы, поэтому ArrayList<? extends Number> может содержать любой объект типа Number или его подтипа.

В этом примере работает строка 9, потому что мы можем быть уверены, что все, что мы получаем из ArrayList, можно преобразовать в тип Number (потому что, если он расширяет Number, по определению, это Number).

Но nums.add() не работает, потому что мы не можем быть уверены в «фактическом типе» объекта. Все, что мы знаем, это то, что это должно быть Number или его подтипы (например, Integer, Double, Long и т. Д.).

В случае контравариантности верно обратное.

Строка 9 работает, потому что мы можем быть уверены, что каким бы ни был «фактический тип» объекта, он должен быть Integer или его супертипом и, таким образом, принимать объект Integer.

Но строка 10 не работает, потому что мы не можем быть уверены, что получим Integer. Например, nums может ссылаться на список массивов Objects.

Приложения

Следовательно, поскольку ковариантные типы доступны только для чтения, а контравариантные типы доступны только для записи (грубо говоря), мы можем вывести следующее практическое правило: «Производитель расширяет, потребитель супер».

Объект, подобный производителю, который создает объекты типа T, может иметь параметр типа <? extends T>, в то время как объект, подобный потребителю, который потребляет объекты типа T, может иметь параметр типа <? super T>.