На днях я наткнулся на этот пост, описывающий то, что автор рассматривает как плюсы и минусы Go после 8 месяцев опыта. Я в основном согласен после полной работы с Go в течение сопоставимого периода
Несмотря на эту преамбулу, это пост о Variance в Java , где моя цель — освежить мое понимание того, что такое Variance и некоторые нюансы его реализации в Java.
( Подсказка: вам нужно знать это для получения сертификата OCJP . )
Я напишу свои мысли на эту тему для Go в следующем посте.
Что такое дисперсия?
В статье Википедии о дисперсии говорится:
Дисперсия относится к тому, как подтип между более сложными типами относится к подтипам между их компонентами.
«Более сложные типы» здесь относятся к структурам более высокого уровня, таким как контейнеры и функции. Таким образом, дисперсия касается совместимости назначений между контейнерами и функциями, состоящими из параметров, которые связаны через иерархию типов . Это позволяет безопасно интегрировать параметрический и подтипный полиморфизм 1 . Например. могу ли я присвоить результат функции, которая возвращает список кошек, переменной типа «список животных»? Можно ли передать список автомобилей Audi методу, который принимает список автомобилей? Могу ли я вставить волка в этот список животных?
В Java дисперсия определяется на сайте использования 2 .
4 вида дисперсии
Перефразируя статью в вики, конструктор типов:
- Ковариант, если он принимает подтипы, но не супертипы
- Контравариантный, если он принимает супертипы, но не подтипы
- Бивариант, если он принимает как супертипы, так и подтипы
- Инвариант, если не принимает ни супертипы, ни подтипы
(Очевидно, что объявленный параметр типа принимается во всех случаях.)
Инвариантность в Java
Сайт использования не должен иметь открытых границ для параметра типа.
Если A является супертипом B , то GenericType<A> не является супертипом GenericType<B> и наоборот.
Это означает, что эти два типа не имеют никакого отношения друг к другу, и ни один из них не может быть заменен на другой при любых обстоятельствах.
Инвариантные контейнеры
В Java инварианты, вероятно, являются первыми примерами обобщений, с которыми вы столкнетесь, и являются наиболее интуитивно понятными. Методы параметра типа можно использовать, как и следовало ожидать. Все методы параметра типа доступны.
Их нельзя обменять
|
1
2
3
|
// Type hierarchy: Person :> Joe :> JoeJrList<Person> p = new ArrayList<Joe>(); // COMPILE ERROR (a bit counterintuitive, but remember List<Person> is invariant)List<Joe> j = new ArrayList<Person>(); // COMPILE ERROR |
Вы можете добавить объекты к ним:
|
1
2
3
4
5
|
// Type hierarchy: Person :> Joe :> JoeJrList<Person> p = new ArrayList<>();p.add(new Person()); // okp.add(new Joe()); // okp.add(new JoeJr()); // ok |
Вы можете читать объекты из них:
|
1
2
3
4
|
// Type hierarchy: Person :> Joe :> JoeJrList<Joe> joes = new ArrayList<>();Joe j = joes.get(0); // okPerson p = joes.get(0); // ok |
Ковариантность на Яве
Сайт использования должен иметь открытую нижнюю границу для параметра типа.
Если B является подтипом A , то GenericType<B> является подтипом GenericType<? extends A> GenericType<? extends A> .
Массивы в Java всегда были ковариантными
До того как дженерики были представлены в Java 1.5 , массивы были единственными доступными дженериковыми контейнерами. Они всегда были ковариантными, например. Integer[] является подтипом Object[] . Компилятор позволяет вам передать Integer[] методу, который принимает Object[] . Если метод вставляет супертип Integer , ArrayStoreException генерируется во время выполнения . Правила ковариантных обобщенных типов реализуют эту проверку во время компиляции , не допуская ошибки, которая когда-либо случалась.
|
1
2
3
4
5
6
7
8
9
|
public static void main(String... args) { Number[] numbers = new Number[]{1, 2, 3, 4, 5}; trick(numbers);}private static void trick(Object[] objects) { objects[0] = new Float(123); // ok objects[1] = new Object(); // ArrayStoreException thrown at runtime} |
Ковариантные контейнеры
Java допускает создание подтипов (ковариантных) универсальных типов, но накладывает ограничения на то, что может «перетекать в эти универсальные типы и выходить из них» в соответствии с Принципом наименьшего удивления 3 . Другими словами, методы с возвращаемыми значениями параметра типа доступны, в то время как методы с входными аргументами параметра типа недоступны.
Вы можете обменять супертип на подтип:
|
1
2
3
4
|
// Type hierarchy: Person :> Joe :> JoeJrList<? extends Joe> = new ArrayList<Joe>(); // okList<? extends Joe> = new ArrayList<JoeJr>(); // okList<? extends Joe> = new ArrayList<Person>(); // COMPILE ERROR |
Чтение из них интуитивно понятно:
|
1
2
3
4
5
|
// Type hierarchy: Person :> Joe :> JoeJrList<? extends Joe> joes = new ArrayList<>();Joe j = joes.get(0); // okPerson p = joes.get(0); // okJoeJr jr = joes.get(0); // compile error (you don't know what subtype of Joe is in the list) |
Писать им запрещено (нелогично) для защиты от ловушек с массивами, описанными выше . Например. в приведенном ниже примере кода вызывающий / владелец List<Joe> был бы удивлен, если бы другой метод с ковариантным аргументом List<? extends Person> List<? extends Person> добавил Jill .
|
1
2
3
4
5
6
|
// Type hierarchy: Person > Joe > JoeJrList<? extends Joe> joes = new ArrayList<>();joes.add(new Joe()); // compile error (you don't know what subtype of Joe is in the list)joes.add(new JoeJr()); // compile error (ditto)joes.add(new Person()); // compile error (intuitive)joes.add(new Object()); // compile error (intuitive) |
Контравариантность в Java
Сайт использования должен иметь открытую верхнюю границу для параметра типа.
Если A является супертипом B , то GenericType<A> является супертипом GenericType<? super B> GenericType<? super B>
Контравариантные контейнеры
Контравариантные контейнеры ведут себя нелогично: в отличие от ковариантных контейнеров, доступ к методам с возвращаемыми значениями параметра типа недоступен, в то время как методы с входными аргументами параметра типа доступны:
Вы можете обменять подтип на супертип:
|
1
2
3
4
|
// Type hierarchy: Person > Joe > JoeJrList<? super Joe> joes = new ArrayList<Joe>(); // okList<? super Joe> joes = new ArrayList<Person>(); // okList<? super Joe> joes = new ArrayList<JoeJr>(); // COMPILE ERROR |
Невозможно захватить определенный тип при чтении из них:
|
1
2
3
4
5
|
// Type hierarchy: Person > Joe > JoeJrList<? super Joe> joes = new ArrayList<>();Joe j = joes.get(0); // compile error (could be Object or Person)Person p = joes.get(0); // compile error (ditto)Object o = joes.get(0); // allowed because everything IS-A Object in Java |
Вы можете добавить подтипы «нижней границы»:
|
1
2
3
|
// Type hierarchy: Person > Joe > JoeJrList<? super Joe> joes = new ArrayList<>();joes.add(new JoeJr()); // allowed |
Но вы не можете добавить супертипы:
|
1
2
3
4
|
// Type hierarchy: Person > Joe > JoeJrList<? super Joe> joes = new ArrayList<>();joes.add(new Person()); // compile error (again, could be a list of Object or Person or Joe)joes.add(new Object()); // compile error (ditto) |
Бивариантность в Java
Сайт использования должен объявить неограниченный подстановочный знак в параметре типа.
Универсальный тип с неограниченным подстановочным знаком является супертипом всех ограниченных вариаций одного и того же универсального типа. Например. GenericType<?> Является супертипом GenericType<String> . Поскольку неограниченный тип является корнем иерархии типов, из этого следует, что из его параметрических типов он может обращаться только к методам, унаследованным от java.lang.Object .
Думайте о GenericType<?> Как GenericType<Object> .
Дисперсия структур с параметрами типа N
А как насчет более сложных типов, таких как функции? Применяются те же принципы, у вас просто есть больше параметров типа для рассмотрения:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
// Type hierarchy: Person > Joe > JoeJr// InvarianceFunction<Person, Joe> personToJoe = null;Function<Joe, JoeJr> joeToJoeJr = null;personToJoe = joeToJoeJr; // COMPILE ERROR (personToJoe is invariant)// CovarianceFunction<? extends Person, ? extends Joe> personToJoe = null; // covariantFunction<Joe, JoeJr> joeToJoeJr = null;personToJoe = joeToJoeJr; // ok// ContravarianceFunction<? super Joe, ? super JoeJr> joeToJoeJr = null; // contravariantFunction<? super Person, ? super Joe> personToJoe = null;joeToJoeJr = personToJoe; // ok |
Дисперсия и Наследование
Java позволяет переопределять методы с ковариантными типами возврата и типами исключений:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
interface Person { Person get(); void fail() throws Exception;}interface Joe extends Person { JoeJr get(); void fail() throws IOException;}class JoeImpl implements Joe { public JoeJr get() {} // overridden public void fail() throws IOException {} // overridden} |
Но попытка переопределить методы с ковариантными аргументами приводит только к перегрузке:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
interface Person { void add(Person p);}interface Joe extends Person { void add(Joe j);}class JoeImpl implements Joe { public void add(Person p) {} // overloaded public void add(Joe j) {} // overloaded } |
Последние мысли
Разница вносит дополнительную сложность в Java. В то время как правила типизации вокруг дисперсии просты для понимания, правила, касающиеся доступности методов параметра типа, являются нелогичными. Понимание их не просто «очевидно» — оно требует паузы, чтобы обдумать логические последствия.
Тем не менее, мой ежедневный опыт заключается в том, что нюансы, как правило, остаются в стороне:
- Я не могу вспомнить случай, когда мне пришлось объявлять контравариантный аргумент, и я редко сталкиваюсь с ними (хотя они существуют ).
- Ковариантные аргументы кажутся немного более распространенными ( пример 4 ), но их легче рассуждать (к счастью).
Ковариантность является ее самым сильным достоинством, учитывая, что подтипирование является фундаментальной техникой объектно-ориентированного программирования (пример: см. Примечание 4 ).
Вывод: дисперсия обеспечивает умеренные сетевые преимущества в моем ежедневном программировании, особенно когда требуется совместимость с подтипами (что является обычным явлением в ООП).
- Укрощение подстановочных знаков: сочетание различий в определении и использовании сайта (John Altidor, et. и др. ↩
- Насколько я понимаю, разница между использованием сайта использования и определения сайта заключается в том, что последний требует, чтобы дисперсия была закодирована в самом общем типе (подумайте о необходимости объявить
MyGenericType<? extends Number>), что вынуждает разработчика API вытеснять все варианты использования. C # определяет дисперсию на сайте определения. С другой стороны, разница между сайтами использования не имеет этого ограничения — разработчик API может просто объявить свой API как универсальный и позволить пользователю определять дисперсию для своих вариантов использования. Недостатком инвариантности сайта использования являются «скрытые» сюрпризы, описанные выше, все они проистекают из «концептуальной сложности, […] предвосхищения универсальности в точках аллюзии» (см. « Приручение символов подстановки» выше). ↩ - Принцип наименьшего удивления — Википедия. Я смутно помню упоминание где-то о дизайнерах Java, следующих этому принципу, но сейчас я не могу его найти. ↩
-
Joinedобъединяет несколькоTextс. Объявление инварианта итерируемогоTextделает этот конструктор непригодным для подтиповText. ↩ ↩ 2 - дженерики java дисперсии
- Поделиться Tweet +1
|
Опубликовано на Java Code Geeks с разрешения Джорджа Аристи, партнера нашей программы JCG. Смотреть оригинальную статью здесь: Дисперсия в Java Мнения, высказанные участниками Java Code Geeks, являются их собственными. |