Я прочитал несколько статей о Ковариация, Контравариантность и Инвариантность в Java, но я в них запутался.
Я использую Java 11, и у меня есть иерархия классов A => B => C
(означает, что C
является подтипом B
и A
, а B
является подтипом A
) и класс Container
:
class Container<T> {
public final T t;
public Container(T t) {
this.t = t;
}
}
например, если я определяю функцию:
public Container<B> method(Container<B> param){
...
}
вот мое замешательство, почему компилируется третья строка?
method(new Container<>(new A())); // ERROR
method(new Container<>(new B())); // OK
method(new Container<>(new C())); // OK Why ?, I make a correction, this compiles OK
если в Java Generics инвариант.
Когда я определяю что-то вроде этого:
Container<B> conta = new Container<>(new A()); // ERROR, Its OK!
Container<B> contb = new Container<>(new B()); // OK, Its OK!
Container<B> contc = new Container<>(new C()); // Ok, why ? It's not valid, because they are invariant
Ковариация — это возможность передать или указать подтип, когда ожидается супертип. Если ваш класс C расширяет B, то C является дочерним классом B. Эта связь между C и B также называется отношением is-a
, где экземпляр C также является экземпляром B. Поэтому, когда ваша переменная contc
ожидает экземпляр B и вы передаете new C()
, поскольку new C()
является экземпляром C, а экземпляр C is (also)-an
экземпляром B, то компилятор разрешает следующую запись:
Container<B> contc = new Container<>(new C());
И наоборот, когда вы пишете
Container<B> conta = new Container<>(new A());
вы получаете сообщение об ошибке, потому что A является супертип для B, нет связи is-a
между A и B, а скорее между B и A. Это связано с тем, что каждый экземпляр B также является экземпляром A, но не каждый экземпляр A является экземпляром B (чтобы сделать глупый пример, каждый большой палец является пальцем, но не каждый палец является большим пальцем). А является обобщением В; поэтому он не может появиться там, где ожидается экземпляр B.
Здесь есть хорошая статья, расширяющая концепцию ковариации в java.
Примеры вопроса не демонстрируют инвариантность дженериков.
Примером, демонстрирующим это, может быть:
ArrayList<Object> ao = new ArrayList<String>(); // does not compile
(Вы можете ошибочно ожидать, что приведенное выше скомпилируется, потому что String
является подклассом Object
.)
Вопрос показывает нам разные способы создания объектов Container<B>
, некоторые из которых компилируются, а другие нет из-за иерархии наследования A
, B
и C
.
Этот алмазный оператор <>
означает, что созданный контейнер в любом случае имеет тип B
.
Если взять следующий пример:
Container<B> contc = new Container<>(new C()); // compiles
И перепишите его, заполнив ромб C
, вы увидите, что следующее не компилируется:
Container<B> contc = new Container<C>(new C()); // does not compile
Это даст вам ту же ошибку компиляции «несовместимых типов», что и мой ArrayList
пример.
Одним из преимуществ Java 7 является так называемый алмазный оператор<>
.
И это было с нами так долго, что легко забыть, что каждый раз, когда алмаз используется при создании экземпляра универсального класса, компилятор должен делать вывод универсальный тип из контекста.
Если мы определим переменную, которая будет содержать ссылку на список объектов Person
следующим образом:
List<Person> people = new ArrayList<>(); // effectively - ArrayList<Person>()
компилятор будет делать вывод тип экземпляра ArrayList
из типа переменной people
слева.
В Спецификация языка Java выражение new ArrayList<>()
описывается как выражение создания экземпляра класса, и поскольку оно не определяет параметр универсального типа и используется в контекст, его следует классифицировать как поли выражение. Цитата из спецификации:
A class instance creation expression is a poly expression (§15.2) if it uses the diamond form for type arguments to the class, and it appears in an assignment context or an invocation context (§5.2, §5.3).
т.е. когда алмаз<>
используется с экземпляром универсального класса, фактический тип будет зависеть от контекст, в котором он появляется.
Три приведенных ниже утверждения представляют случай так называемого контекст назначения. И все три экземпляра Container
будут считаться типом B
.
Container<B> conta = new Container<>(new A()); // 1 - ERROR because `B t = new A()` is incorrect
Container<B> contb = new Container<>(new B()); // 2 - fine because `B t = new B()` is correct
Container<B> contc = new Container<>(new C()); // 3 - fine because `B t = new C()` is also correct
Поскольку все экземпляры контейнера имеют тип B
и тип параметра, ожидаемый подрядчиком, также будет B
. т.е. может предоставить экземпляр B
или любого из его подтипов. Следовательно, в случае 1
мы получаем ошибку компиляции, между тем 2
и 3
(B
и подтип B
) будут компилироваться корректно.
И это не нарушение инвариантное поведение. Подумайте об этом так: мы можем хранить в List<Number>
экземпляры Integer
, Byte
, Double
и т. д., что не вызовет никаких проблем, поскольку все они могут представлять свой супертип Number
. Но компилятор не позволит присвоить этот список любому списку, отличному от типа List<Number>
, потому что в противном случае невозможно было бы гарантировать, что это присваивание безопасно. И это то, что означает ковариация.
В качестве примера рассмотрим метод установки в классе Container
:
public class Container<T> {
public T t;
public Container(T t) {
this.t = t;
}
public void setT(T t) {
this.t = t;
}
}
Теперь воспользуемся этим:
Container<B> contb = new Container<>(null); // to avoid any confusion initialy `t` will be assigned to `null`
contb.setT(new A()); // compilation error - because expected type is `B` or it's subtype
contb.setT(new B()); // fine
contb.setT(new C()); // fine because C is a subtype of B
Когда мы имеем дело с выражением создания экземпляра класса, используя ромб <>
, который передается методу в качестве аргумента, тип будет выводиться из контекст вызова, как указано в цитате из приведенной выше спецификации.
Поскольку method()
ожидает Container<B>
, все приведенные выше экземпляры будут считаться относящимися к типу B
.
method(new Container<>(new A())); // Error
method(new Container<>(new B())); // OK - because `B t = new B()` is correct
method(new Container<>(new C())); // OK - because `B t = new C()` is also correct
Примечание
Важно отметить, что до Java 8 (то есть с Java 7, потому что мы используем алмаз) выражение new Container<>(new C())
будет интерпретироваться компилятором как автономное выражение (т. е. контекст будет игнорироваться), создавая экземпляр Container<C>
. Это означает, что ваше первоначальное предположение было несколько правильным: с Ява 7 приведенный ниже оператор не будет компилироваться.
Container<B> contc = new Container<>(new C()); // Container<B> = Container<C> - is an illegal assignment
Но в Java 8 появилась функция, называемая типы целей и поли выражение (т. е. выражения, которые появляются внутри контекст), которая гарантирует, что контекст всегда будет учитываться механизмом определения типа.
Хорошо, я не знал, как работает алмазный оператор, это была концептуальная ошибка с моей стороны. Теперь я понимаю, почему код скомпилирован, я ценю ваш ответ andrewJames. Большое спасибо