Инвариантные дженерики работают некорректно

Я прочитал несколько статей о Ковариация, Контравариантность и Инвариантность в 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
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
5
0
75
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ковариация — это возможность передать или указать подтип, когда ожидается супертип. Если ваш класс 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.

https://www.baeldung.com/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 пример.

Хорошо, я не знал, как работает алмазный оператор, это была концептуальная ошибка с моей стороны. Теперь я понимаю, почему код скомпилирован, я ценю ваш ответ andrewJames. Большое спасибо

william delgado 17.05.2022 12:51

Одним из преимуществ 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 появилась функция, называемая типы целей и поли выражение (т. е. выражения, которые появляются внутри контекст), которая гарантирует, что контекст всегда будет учитываться механизмом определения типа.

Другие вопросы по теме