Статьи

Дисперсия в Java

На днях я наткнулся на этот пост, описывающий то, что автор рассматривает как плюсы и минусы 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 ).

Вывод: дисперсия обеспечивает умеренные сетевые преимущества в моем ежедневном программировании, особенно когда требуется совместимость с подтипами (что является обычным явлением в ООП).

  1. Укрощение подстановочных знаков: сочетание различий в определении и использовании сайта (John Altidor, et. и др.
  2. Насколько я понимаю, разница между использованием сайта использования и определения сайта заключается в том, что последний требует, чтобы дисперсия была закодирована в самом общем типе (подумайте о необходимости объявить MyGenericType<? extends Number> ), что вынуждает разработчика API вытеснять все варианты использования. C # определяет дисперсию на сайте определения. С другой стороны, разница между сайтами использования не имеет этого ограничения — разработчик API может просто объявить свой API как универсальный и позволить пользователю определять дисперсию для своих вариантов использования. Недостатком инвариантности сайта использования являются «скрытые» сюрпризы, описанные выше, все они проистекают из «концептуальной сложности, […] предвосхищения универсальности в точках аллюзии» (см. « Приручение символов подстановки» выше).
  3. Принцип наименьшего удивления — Википедия. Я смутно помню упоминание где-то о дизайнерах Java, следующих этому принципу, но сейчас я не могу его найти.
  4. Joined объединяет несколько Text с. Объявление инварианта итерируемого Text делает этот конструктор непригодным для подтипов Text . 2
  5. дженерики java дисперсии
  6. Поделиться Tweet +1

Опубликовано на Java Code Geeks с разрешения Джорджа Аристи, партнера нашей программы JCG. Смотреть оригинальную статью здесь: Дисперсия в Java

Мнения, высказанные участниками Java Code Geeks, являются их собственными.