На днях я наткнулся на этот пост, описывающий то, что автор рассматривает как плюсы и минусы 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 :> JoeJr List<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 :> JoeJr List<Person> p = new ArrayList<>(); p.add( new Person()); // ok p.add( new Joe()); // ok p.add( new JoeJr()); // ok |
Вы можете читать объекты из них:
1
2
3
4
|
// Type hierarchy: Person :> Joe :> JoeJr List<Joe> joes = new ArrayList<>(); Joe j = joes.get( 0 ); // ok Person 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 :> JoeJr List<? extends Joe> = new ArrayList<Joe>(); // ok List<? extends Joe> = new ArrayList<JoeJr>(); // ok List<? extends Joe> = new ArrayList<Person>(); // COMPILE ERROR |
Чтение из них интуитивно понятно:
1
2
3
4
5
|
// Type hierarchy: Person :> Joe :> JoeJr List<? extends Joe> joes = new ArrayList<>(); Joe j = joes.get( 0 ); // ok Person p = joes.get( 0 ); // ok JoeJr 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 > JoeJr List<? 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 > JoeJr List<? super Joe> joes = new ArrayList<Joe>(); // ok List<? super Joe> joes = new ArrayList<Person>(); // ok List<? super Joe> joes = new ArrayList<JoeJr>(); // COMPILE ERROR |
Невозможно захватить определенный тип при чтении из них:
1
2
3
4
5
|
// Type hierarchy: Person > Joe > JoeJr List<? 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 > JoeJr List<? super Joe> joes = new ArrayList<>(); joes.add( new JoeJr()); // allowed |
Но вы не можете добавить супертипы:
1
2
3
4
|
// Type hierarchy: Person > Joe > JoeJr List<? 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 // Invariance Function<Person, Joe> personToJoe = null ; Function<Joe, JoeJr> joeToJoeJr = null ; personToJoe = joeToJoeJr; // COMPILE ERROR (personToJoe is invariant) // Covariance Function<? extends Person, ? extends Joe> personToJoe = null ; // covariant Function<Joe, JoeJr> joeToJoeJr = null ; personToJoe = joeToJoeJr; // ok // Contravariance Function<? super Joe, ? super JoeJr> joeToJoeJr = null ; // contravariant Function<? 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, являются их собственными. |