Разница для <? super/extends String> в описании метода и переменной

Дано:

import java.util.*;

public class Hancock {
    //insert code here
        list.add("foo");
    }
}

Какие два фрагмента кода, вставленные независимо в строке 5, будут компилироваться без предупреждений? (Выберите два)

A. public void addString(List list) {
B. public void addString(List<String> list) {
C. public void addString(List<? super String> list) {
D. public void addString(List<? extends String> list) {

Правильными ответами являются B и C.

Ответы A и B для меня совершенно ясны. Для ответов C и D я знаю, каким образом происходит наследование, однако я не могу понять, почему ответ D не компилируется в Eclipse, пока все остальные (A с warrning о generic, B и C без warrings).

Ошибка в Eclipse для ответа D The method add(capture#1-of ? extends String) in the type List<capture#1-of ? extends String> is not applicable for the arguments (String).

С другой стороны, это компиляция:

public void addString() {
    List<? extends String> list1 = new ArrayList<String>();
    List<? super String> list2 = new ArrayList<String>();
}

Почему? Почему <? super String> не компилируется в объявлении метода, пока он компилируется в объявлении переменной.

Я знаю, что String является окончательным классом и не может быть расширен каким-либо другим классом, но это не объясняет мне, что здесь происходит.

Ответ 1

Сначала давайте посмотрим ответ C:

public void addString(List<? super String> list) {
    list.add("foo");
}

В объявлении этого метода указано, что вам будет разрешено передавать объекты List, которые параметризуются каким-то супер классом String, например String или Object. Итак:

  • Если вы пройдете List<String>, list.add("foo") будет абсолютно корректным.
  • Если вы пройдете List<Object>, то list.add("foo") будет абсолютно корректным, потому что "foo" - это String (и вы можете добавить String в List<Object>).

Это означает, что правильный ответ C.


Теперь посмотрим ответ D.

Если у вас есть объявление метода, например:

public void addString(List<? extends String> list) {

}

это означает, что вы сможете передать List объекты, параметризованные подтипом неизвестного String. Итак, когда вы выполняете list.add("foo");, компилятор не будет знать, имеет ли предоставленный объект тип, который соответствует неизвестному подтипу String, и поэтому вызывает ошибку времени компиляции.


Если у вас есть:

public void addString() {
    List<? extends String> list1 = new ArrayList<String>();
    List<? super String> list2 = new ArrayList<String>();
}

Этот фрагмент компилируется отлично, потому что list1 определяется как содержать List объекты, которые имеют неизвестный подтип String, включая сам String, поэтому он действителен. Проблема в том, что вы не сможете ничего добавить, кроме null.

Что касается list2, переменная может содержать объекты List, которые параметризуются некоторым супертипом String, включая сам String.


Дополнительная информация:

Ответ 2

Во-первых, дженерикам все равно, что String является окончательным. Они работают одинаково для конечных и не заключительных классов.

С учетом этого должно быть понятно, почему D не разрешено - если это так, вы можете сделать это:

void test() {
    List<Integer> integers = new ArrayList<Integer>();
    addADouble(integers);
    int a = integers.get(0); // ????
}

void addADouble(List<? extends Number> list) {
    list.add(new Double(5.0));
}

List<? extends Number> - это "Список чего-то, что расширяет число, но вы точно не знаете, что такое" Список ". - это может быть List<Double> или List<Integer>, или List<Number>, или List<YourCustomSubclassOfNumber>, поэтому вы не можете ничего добавить к нему, потому что вы не знаете, правильный ли он.