Почему нельзя привести List<String> к List<Object>. Особенности полиморфизма в обощениях (generics)

Если мы напишем такой код:
String str = "Hello";Object obj = str;
то код откомпилируется и выполнится без проблем. Если же мы напишем следующее:
List<String> strs = new ArrayList<>();List<Object> objs = strs;
то код даже не соберётся и компилятор сообщит нам, что "error: incompatible types: List<String> cannot be converted to List<Object>"
Такое поведение может показаться контринтуитивным (так и есть), но, в действительности, машина ведёт себя правильно и абсолютно логично с точки зрения обеспечения безопасности типов.
Давайте уберём проверку типов компилятором, воспользовавшись так называемым "сырым" типом списка, то есть создадим переменную List objs вместо List<Object> objs. Тогда наш код откомпилируется без проблем:
List<String> strs = new ArrayList<>();List objs = strs;
Дополним его следующим образом:
List<String> strs = new ArrayList<>();List objs = strs;
objs.add(1);String firstValue = strs.get(0);
Последняя строка выбросит исключение: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String.
Действительно, если бы List<String> был приводим к List<Object>, то выполни мы такое преобразование, мы затем могли бы в списки, объявленные как списки строк, добавлять любые объекты. Такую ситуацию никак нельзя назвать безопасной с точки зрения типов.

Использование списка предков для работы со списком потомков


Тем не менее, мы можем пользоваться списками предков, для обработки потомков следующим образом:
public static void main(String[] args) { List<Integer> integers = new ArrayList<>(); List<Double> doubles = new ArrayList<>();
numListProcessor(integers); numListProcessor(doubles);}
public static void numListProcessor(List<? extends Number> nums) { System.out.println(nums.size());}
Как видите, объявление параметра nums типом List<? extends Number> позволило передавать в метод и List<Integer>, и List<Double>. Давайте посмотрим, что мы можем сделать с nums в методе numListProcessor():
public static void main(String[] args) { List<Integer> integers = new ArrayList<>(); integers.add(0);
numListProcessor(integers);}
public static void numListProcessor(List<? extends Number> nums) { System.out.println(nums.size()); Number num = nums.get(0); System.out.println(num.doubleValue()); nums.add(new Integer(0));}
Последняя строка не откомпилируется.
Из кода метода numListProcessor() видно:
  • Во-первых, использование nums.size() показало, что мы можем пользоваться переменной nums как любым другим List'ом, что неудивительно.
  • Во-вторых, мы можем извлечь из списка элемент, но он будет автоматически приведён к типу Number и с ним можно бдет делать только то, что можно делать с Number'ами. В нашем примере мы вызвали на этом Number'е метод doubleValue(), который ожидаемо вернёт 0.0.
  • В-третьих, мы не можем добавлять в список Integer'ы. Внутри метода numListProcessor о параметре nums известно только то, что в нём лежат типы, наследующие Number, но неизвестно, какие именно. В одном месте мы вызовем этот метод, передав список Integer'ов, в другом -- передав список Double'ов. Очевидно, что передавать new Integer(0) в метод add небезопасно, так как List<? extends Number> может на деле оказаться List<Double>.

Более того, не откомпилируется даже следующая версия метода:
public static void numListProcessor(List<? extends Number> nums) { Number num = new Integer(0); nums.add(num);}
Также не откомпилируется последняя строка.
Запись List<? extends Number> означает, что список содержит каких-то конкретных наследников Number'а, а не абстрактные Number'ы. А наследники могут быть разными и мы заранее не знаем, какие именно. Поэтому компилятор не допустит такого нарушения безопасности типов.
Таким образом, поведение компилятора полностью подчинено задаче обеспечения безопасности типов и он с этой задачей прекрасно справляется. Невозможность приведения List<String> к List<Object> объясняется тем, что такое приведение чревато полным нарушением безопасности типов внутри коллекции.