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