Ковариантность контравариантность инвариантность java

Ковариация, контравариантность и инвариантность — что они означают? (Часть 2)

В первой части этого поста я дал краткое объяснение и привел несколько примеров различных систем типов дисперсии. Как и было обещано, в этом посте мы углубимся в эту тему. Мы выясним, что они на самом деле означают в системах типов Java и Kotlin.

Для этого я создам простую структуру наследования, как и раньше.

Мы рассмотрим четыре различных сценария для каждого типа дисперсии, в которых имеет смысл: операторы присваивания, переопределение методов, массивы и общие коллекции.

Ковариация на Яве и Котлине

Оператор присваивания: присваивание переменной является ковариантным по своей природе как в Java, так и в Kotlin. Следовательно, следующие строки Java в фрагменте кода Java являются допустимыми:

GrandParent grandParent = new Parent(); GrandParent anotherGrandParent = new BabyChild(); Parent parent = new BabyChild();

То же самое верно и для сниппета Kotlin:

val grandParent: GrandParent = Parent() val anotherGrandParent: GrandParent = BabyChild() val parent: Parent = BabyChild()

Переопределение метода. В этом случае оба языка работают одинаково. Типы возвращаемого значения ковариантны, а параметры метода неизменны. Рассмотрим этот фрагмент:

Массивы. В этом случае для Java и Kotlin есть разница в подходах. Java идет по пути ковариации, в то время как Kotlin придерживается здесь инвариантности. Рассмотрим следующий фрагмент, который считается допустимым в Java:

// Valid statements GrandParent[] grandParents = new BabyChild[10]; GrandParent[] anotherGrandParents = new Parent[10]; Parent[] parents = new BabyChild[10]; // Side comment: The above snippets can be a pain because of the // potential ArrayStoreException that can happen when one tries to // add a new item to the collection that is incompatible with // the type that the collection was initialised with. The following // line will cause a crash at runtime. grandParents[1] = new Parent(); // It will compile but it will crash at runtime because you can't // cast from a `Parent` to a `BabyChild`.

Однако в Котлине это не так:

// These are valid val grandParents: Array = arrayOfNulls(10) val parents: Array = arrayOfNulls(10) val children: Array = arrayOfNulls(10) // These will NOT compile. Kotlin arrays are invariant. val grandParents: Array = arrayOfNulls(10) val grandParents2: Array = arrayOfNulls(10) val parents: Array = arrayOfNulls(10)

Общие коллекции в Java: Чтобы сделать общие коллекции ковариантными в Java, мы должны явно указать тип дисперсии. Это называется дисперсией использования сайта. Рассмотрим следующий фрагмент:

// This will not compile. The parameterised types of collections // are invariant by default. List willNotCompileParents = new ArrayList(); // Turn on the covariant behaviour through use-site variance. // This list contains (or produces) `Parent` and anything that is a // subtype of `Parent`. List okayParents = new ArrayList();

Теперь вы можете заметить, что есть потенциальное ArrayStoreException (или подобное исключение), которое может произойти с ковариантной универсальной коллекцией. Мы защищены от этого во время компиляции, потому что компилятор не позволит нам вызвать любой метод, который имеет параметризованный тип в качестве аргумента. Однако мы можем вызвать любой метод, который имеет параметризованный тип в качестве возвращаемого типа.

// This will cause a compile error. okayParents.add(new Parent());

В контексте общих коллекций это означает, что список теперь фактически доступен только для чтения. Это связано с тем, что метод «add (…)» в списке принимает параметризованный тип и вызовет ошибку компиляции, если кто-то попытается его вызвать. Мы можем с уверенностью сказать, что список является производителем общих элементов.

Общие коллекции в Kotlin: Ковариационный подход для общих коллекций обрабатывается в Kotlin по-другому. В Kotlin есть функция отклонения на сайте объявления, которая позволяет разработчикам заранее определять ковариантные универсальные типы, это делается во время определения класса.

В отличие от Java, Kotlin делает шаг вперед, компилятор не позволит вам определять методы в вашем классе, которые имеют параметризованный тип в качестве аргумента. Если вы не можете их определить, вы не можете их называть.

Если вы не можете их определить, вы не можете их называть.

Если я добавлю следующий фрагмент в тело класса TestParameter , я не смогу скомпилировать свой код:

// From IntelliJ: «Type parameter T is declared as ‘out’ but occurs // in ‘in’ position in type T», so it won’t compile. fun accept(t: T)

Есть способ подавить эту ошибку в IntelliJ, как видно с помощью аннотации @UnsafeVariance в Kotlin’s Collections API. Я бы не рекомендовал это.

// IntelliJ error goes away but this is a code smell. fun accept(t: @UnsafeVariance T)

Для Kotlin также существует резервная функция отклонения на сайте на тот случай, если невозможно использовать функцию отклонение сайта объявления. Например, если вы используете общую коллекцию из API, созданную не вами.

Предполагая, что я работаю с указанным выше классом, и он существует в стороннем API. Я все еще могу сделать общий класс ковариантным, используя следующий фрагмент:

// Notice the use of the `out` keyword and how this allows me to // assign `myVar` to subtype-parameterised instance. val myVar: ThirdPartyGenericClass = ThirdPartyGenericClass(BabyChild())

В этом посте мы рассмотрели ковариацию в Java и Kotlin, для разумности следующий и последний пост в этой серии будет посвящен контравариантности и инвариантности в обоих языках.

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

Источник

An introduction to generic types in Java: covariance and contravariance

An introduction to generic types in Java: covariance and contravariance

An introduction to generic types in Java: covariance and contravariance

Java is a statically typed language, which means you must first declare a variable and its type before using it.

For example: int myInteger = 42;

Generic types

Definition: “A generic type is a generic class or interface that is parameterized over types.”

Essentially, generic types allow you to write a general, generic class (or method) that works with different types, allowing for code re-use.

Rather than specifying obj to be of an int type, or a String type, or any other type, you define the Box class to accept a type parameter < ;T>. Then, you ca n use T to represent that generic type in any part within your class.

Now, enter covariance and contravariance.

Covariance and contravariance

Definition

Variance refers to how subtyping between more complex types relates to subtyping between their components (source).

An easy-to-remember (and extremely informal) definition of covariance and contravariance is:

Arrays

In Java, arrays are covariant, which has 2 implications.

Firstly, an array of type T[] may contain elements of type T and its subtypes.

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

Secondly, an array of type S[] is a subtype of T[] if S is a subtype of T .

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

However, it’s important to remember that: (1) numArr is a reference of reference type Number[] to the “actual object” intArr of “actual type” Integer[] .

Therefore, the following line will compile just fine, but will produce a runtime ArrayStoreException (because of heap pollution):

It produces a runtime exception, because Java knows at runtime that the “actual object” intArr is actually an array of Integer .

Generics

With generic types, Java has no way of knowing at runtime the type information of the type parameters, due to type erasure. Therefore, it cannot protect against heap pollution at runtime.

As such, generics are invariant.

ArrayList intArrList = new ArrayList<>();ArrayList numArrList = intArrList; // Not okArrayList anotherIntArrList = intArrList; // Ok

The type parameters must match exactly, to protect against heap pollution.

Wildcards, covariance, and contravariance

With wildcards, it’s possible for generics to support covariance and contravariance.

Tweaking the previous example, we get this, which works!

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

The question mark “?” refers to a wildcard which represents an unknown type. It can be lower-bounded, which restricts the unknown type to be a specific type or its supertype.

Therefore, in line 2, ? super Integer translates to “any type that is an Integer type or its supertype”.

You could also upper-bound the wildcard, which restricts the unknown type to be a specific type or its subtype, by using ? extends Integer .

Read-only and write-only

Covariance and contravariance produce some interesting outcomes. Covariant types are read-only, while contravariant types are write-only.

Remember that covariant types accept subtypes, so ArrayList can contain any object that is either of a Number type or its subtype.

In this example, line 9 works, because we can be certain that whatever we get from the ArrayList can be upcasted to a Number type (because if it extends Number , by definition, it is a Number ).

But nums.add() doesn’t work, because we cannot be sure of the “actual type” of the object. All we know is that it must be a Number or its subtypes (e.g. Integer, Double, Long, etc.).

With contravariance, the converse is true.

Line 9 works, because we can be certain that whatever the “actual type” of the object is, it must be Integer or its supertype, and thus accept an Integer object.

But line 10 doesn’t work, because we cannot be sure that we will get an Integer . For instance, nums could be referencing an ArrayList of Objects .

Applications

Therefore, since covariant types are read-only and contravariant types are write-only (loosely speaking), we can derive the following rule of thumb: “Producer extends, consumer super”.

A producer-like object that produces objects of type T can be of type parameter , while a consumer-like object that consumes objects of type T can be of type para meter .

Источник

Читайте также:  What is overloading methods java
Оцените статью