Почему это использование Generics не генерирует исключение времени выполнения или компиляции?

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

public class SomeMain {

  public static void main(String[] args) {

    Foo<Integer> foo = new Foo<Integer>();
    System.out.println(foo.getFoo()); // Works, prints out "Foo"

  }

  public static class Foo<E>  {
    public E getFoo() {
      return (E) "Foo";
    }
  }
}

С типичным типом возврата я предположил, что возврат в приведенном выше примере будет оцениваться с помощью:

return (Integer) "Foo";  // Inconvertible types !!!

Вместо этого String возвращается и печатается правильно.

Я получаю ошибку компиляции, если я изменяю вызов:

String fooString = foo.getFoo(); // Compile error, incompatible types found
System.out.println(fooString);

Что мне не хватает, чтобы помочь мне понять, что происходит здесь, и почему исходная версия не привела к ошибке компиляции.

Ответ 1

Это связано с тем, что разрешение перегрузки разрешало ваш вызов println для println(Object), поскольку нет println(Integer).

Имейте в виду, что дженерики Java стираются во время выполнения. И удары, подобные (E) "Foo", удаляются и перемещаются на сайт вызова. Иногда это не обязательно, поэтому вещи присылаются в нужный тип только тогда, когда это необходимо.

Другими словами, никакие броски не выполняются внутри getFoo. Спецификация языка поддерживает это:

Раздел 5.5.2 Проверенные броски и непроверенные роли

  • Литой - совершенно непроверенный актер.

    Для такого приведения не выполняется никаких действий во время выполнения.

После стирания getFoo возвращает Object. И это передается в println(Object), что отлично.

Если я вызову этот метод и foo.getFoo, я получу ошибку:

static void f(Integer i) {
    System.out.println(i);
}
// ...
f(foo.getFoo()); // ClassCastException

потому что на этот раз его нужно отбросить.

Ответ 2

System.out.println не имеет перегрузки, которая принимает Integer. Итак, это утверждение:

System.out.println(foo.getFoo());

System.out.println(Object); ,

Чтобы убедиться, что он в противном случае завершился неудачей, попробуйте:

Foo<Integer> foo = new Foo<Integer>();
Integer fooInt = foo.getFoo(); //class cast exception

Следующее не получится таким же образом:

public static void main(String[] args) throws Exception {
    Foo<Integer> foo = new Foo<Integer>();
    print(foo.getFoo()); //Would also fail with a class cast exception
}
static void print(Integer in) {
    System.out.println(in);
}

И это приводит к сбою компиляции по очевидным причинам:

String fooString = foo.getFoo(); //can't work

foo - Foo<Integer>, а foo.getFoo() возвращает Integer и компилятор может выбрать это.

Ответ 3

Я хотел бы добавить, что во время процесса стирания типа компилятор Java заменяет параметр E неограниченного типа E на Object, поэтому класс Foo фактически скомпилирован в:

public static class Foo {
    public Object getFoo() {
        return "Foo";
    }
}

Поэтому допустим следующий код (бросок не нужен):

Object obj = foo.getFoo();
System.out.println(obj);

В то же время следующий фрагмент кода создает ошибку времени компиляции, как и ожидалось:

Foo<Integer> foo = new Foo<Integer>();
String fooString = foo.getFoo(); // you're trying to trick the compiler (unsuccessfully)
           ^
incompatible types: Integer can not be converted to String

И что основная ответственность за дженерики - проверка времени компиляции.

Но есть и другая сторона истории - казни времени. Например, если вы пишете:

Integer value = foo.getFoo();

вы получаете ClassCastException во время выполнения (компилятор Java вставляет checkcast которая проверяет, может ли результат foo.getFoo() быть foo.getFoo() в Integer).