Если мы напишем такой код: 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> объясняется тем, что такое приведение чревато полным нарушением безопасности типов внутри коллекции.