1

Подскажите пожалуйста почему я не могу сделать следующее

 public class A implements IA {
static void test(List<IA> ia) {

}

public static void main(String[] args) {
    List<A> aList = new ArrayList<>();
    test(aList);
}

}

Метод тест принимает в качестве параметра список объектов типа IA. Почему я не могу передать в метод список List<A>?

Alex
  • 122
  • Ваш вопрос смотивирован появлением какой-либо ошибки? Или вопрос о теоретической передаче списка экземпляров класса A? – Вася Воронцов Jun 17 '20 at 19:55
  • https://ru.stackoverflow.com/questions/361807/%D0%98%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-wildcard-%D0%B2-generics-java – IR42 Jun 18 '20 at 02:44
  • Дело в том что вместо List у меня были массивы (так задумано было преподавателями которые меня типо учат на бесплатных курсах). С массивами выше описанная ситуация работает как надо, а когда пришло время перейти на коллекции, я был удивлен что не могу сделать тоже самое, приходиться изменять много кода. Выше приведен просто маленький примерчик, код из своей учебной программы я не стал скидывать, так как на маленьком примере легче объяснить, что я хочу узнать. – Alex Jun 18 '20 at 02:53

3 Answers3

1

Ответ на самом деле прост - дженерики в Java являются инвариантными. Чтобы ответить на свой вопрос, вам достаточно загуглить что означают следующие термины: инвариантность, контравариантность, ковариантность.

Bakuard
  • 972
0

Не такой это простой вопрос, особенно для начинающих разработчиков. Никакое чтиво про "преобразование" типов, полагаю, вам не поможет, потому что очень скоро вы упретесь в то, что между супер классом и классом - наследником отношение is a, т.е. класс-наследник является супер классом, что снова поставит вас в тот же тупик, в котором вы находитесь. К сожалению, объяснит все подробности происходящего в рамках форума невозможно, однако, если не углубляться в детали, то прояснит ситуацию вполне реально.

Весь фокус в том, что вы имеете дело не просто с наследованием, а с дженериками. И понимание происходящего зависит от того, насколько хорошо вы понимаете работу дженериков (а это довольно непросто). Вы должны понимать, что между классом А и интерфейсом IA действительно отношение is a, но вы имеете дело не с этими классами, а с коллекциями, типизированными указанными классом и интерфейсом, а это совсем не одно и тоже. Теперь определение того, является ли коллекция, объявленная в сигнатуре метода, подходящей по типу для коллекции, которую мы пытаемся передать в этот метод, возложено именно на дженерики(коллекции)!

Для понимания происходящего (реализации полиморфизма) вам следует знать, что для дженериков существует своя система наследования. Рассмотрите этот пример:

import java.util.ArrayList;
import java.util.List;

public class A implements IA {

static void test(List&lt;? extends IA&gt; ia) {

}

public static void main(String[] args) {
    List&lt;IA&gt; aList = new ArrayList&lt;&gt;();
    aList.add(new A());
    aList.add(new B());
    test(aList);
}

}

class B implements IA {

    public static void main(String[] args) {
    List&lt;B&gt; aList = new ArrayList&lt;&gt;();
    aList.add(new B());
    test(aList);
}

}

В случае с классом А нам необходимо создать единую коллекцию для наполнения ее объектами различных классов-наследников. Именно это происходит в методе main класса А. В этом случае коллекция типизируется интерфейсом. Если бы мы типизировали нашу коллекцию конкретным классом, например А, то не смогли бы добавить туда экземпляры класса В, а именно это нам необходимо в данном примере.

В случае с классом В мы хотим иметь коллекцию, типизированную только классом В (возможно для В существуют методы, которых нет в интерфейсе и на этом уровне - в методе main класса В , работая с коллекцией, мы вызываем именно эти специфические для В методы). В этом случае мы не сможем добавить в коллекцию экземпляры класса А.

Что касается метода test, нам достаточно изменить дженерик коллекции лист на List<? extends IA>, теперь этот метод в состоянии принять и коллекцию, типизированную интерфейсом IA, и коллекцию, типизированную наследниками указанного интерфейса - классами A,B.

В том случае, когда вы объявляете метод с сигнатурой test(List<IA> ia) вы указываете компилятору, что хотите принимать исключительно коллекцию, типизированную интерфейсом. Таким образом, вам предоставляется контроль над тем, что именно принимает ваш метод: исключительно коллекции , типизированные интерфейсом, или как коллекции, типизированные интерфейсом, так и коллекции , типизированные наследниками данного интерфейса.

Если есть вопросы - задавайте

IR42
  • 4,262
  • Вопрос есть. А зачем собственно всё так усложнять? Создавать отдельный интерфейс только ради такой цели? Подобные вещи сильно усложняют и замусоривают код. Если нужно чтобы список передавал объекты любых классов, можно сделать намного проще static void test(List<?> obj) либо static void test(List obj) и объявить лист как List aList = new ArrayList<>(); – Sh4rx Jun 17 '20 at 21:52
  • И самое главное, быть может вы приведете пример где это используется кроме как в учебных и познавательных целях? – Sh4rx Jun 17 '20 at 22:02
  • 1
    @Sh4rx все приведенные вами примеры сводятся к передаче ничем нетипизированной коллекции, т.е. по сути List aList . Если вы напишите в аргументах метода такую коллекцию, то у всех экземпляров ее класса вы сможете вызвать только методы Object (без костылей в виде проверки типов и кастования, разумеется). В нашем случае с таким аргументом List<? extends IA> ia я в методе могу вызвать все методы, объявленные в интерфейсе IA (помимо методов Object, разумеется), а это открывает совсем другие возможности и полиморфизм работает именно здесь. – Дмитрий Jun 17 '20 at 22:18
  • @Sh4rx именно поэтому мой пример не сводится к передаче нетипизированной коллекции. Для сравнения (если уйти от коллекций) это тоже самое, что метод, принимающий переменную типа интерфейса и переменную типа Object. Совсем не одно и тоже. Ну а то, что вы считаете, что дженерики используются только в учебных целях и это "замусоривает код", могу только посочувствовать и посоветовать обратиться к Javascript, Там нет строгой типизации и ничего "замусоривать" ваш код не будет.Но только код, размером 10000 строк станет катастрофой без этого "замусоривания". А дженерики-сложная и очень мощная штука. – Дмитрий Jun 17 '20 at 22:24
  • @Sh4rx Что касается примеров... Если вам недостаточно того, что все коллекции , функциональные интерфейсы, как следствие, все лямбды, большая часть спринговых интерфейсов используют дженерики, то примеры вы найдете без труда, если посмотрите мои ответы. Например здесь для типизации парсера, а также написания универсальных сервисов (аналогично сервис лееру в спрингеовом преокте) : https://ru.stackoverflow.com/questions/1134927/%d0%b8%d0%bc%d0%bf%d0%be%d1%80%d1%82-%d0%b8%d0%b7-%d1%84%d0%b0%d0%b9%d0%bb%d0%b0-csv-%d0%b2-sqlite/1134987#1134987 – Дмитрий Jun 17 '20 at 22:46
  • Странно, но вы видимо меня совсем не поняли, дело тут не в дженериках, а в конкретно вашем примере. Но возможно вы сами догадаетесь, и ответите на вопрос, какие проблемы в будущем несёт подобный код, и какие принципы он нарушает? – Sh4rx Jun 17 '20 at 22:50
  • 1
    @Sh4rx какой именно код? это пример, объясняющий, как написать дженерик, обеспечивающий использование полиморфизма. цель-показать в максимально коротком примере, как работает дженерик (с учетом вопроса автора). вы пишите, что мой код static void test(List<? extends IA> ia) тождественен вашему static void test(List obj), а это совсем не так. а какие проблемы несет даный код, точнее метод static void test(List<? extends IA> ia) ? – Дмитрий Jun 17 '20 at 23:00
  • Дмитрий, мне вовсе не хочется спорить и развозить кашу в комментариях, да и по правилам сообщества это неприемлимо. Я просто оставлю это здесь, думаю будет интересно и полезно как вам так и автору вопроса (хотя я лично не уверен, что автора вопроса интересовали именно дженерики в данном случае) https://habr.com/ru/company/sberbank/blog/416413/ – Sh4rx Jun 17 '20 at 23:10
  • @Sh4rx я читал эту статью.во что превращается код после компиляции-отдельная тема.негатив можно найти во всем.дженерик-только инструмент,его тоже можно использовать неправильно,это приведет к проблемам,как и с любым другим инструментом.посмотрите последний пример в вашей статье,где параметизированный дженериком метод возвращает null,результат которого пробрасывается в перегруженый метод,заставляя по null определит сигнатуру нужного метода?!это за гранью добра и зла.я говорил о целесообразности использования test(List<?extendsIA> ia)(втч в отличии от test(Listobj)).тут проблем я не вижу – Дмитрий Jun 17 '20 at 23:33
  • вы немного перескочили, не забегая в Java 8+ там идёт речь о wildcard. И вот моё уточнение было именно о read/write возможностях оных. Там есть вывод, в каких случаях это стоит использовать, а в каких нет. И там же приведен пример с <?> и реализацией записи в список. Но я скажу так, данные примеры имеют место быть, но для новичков я думаю это слишком сложно. Более простые аналогии на самом деле реализовать можно и без этих "фич". Классический пример - отдельный класс а-ля Utils в который можно вынести методы общего назначения, а уже при надобности сделать перегрузку с дженериками. – Sh4rx Jun 18 '20 at 14:25
  • @ Sh4rx я прочитал про wildcard.мы не используем raw в нашем примере.у нас есть только <? exdands IA>.в этом случае проблемы будут только при попытке записать в такую коллекцию,ну ок,по условию задачи это и не нужно (и не нужно в принципе).кроме того, об этом нас немедленно предупредит компилятор,посему не вижу проблем,ведь дженерик для этого и нужен,как и вся типизация(позволяет обнаружить проблемы на этапе компиляции, а не в рантайме).что касается альтернативной реализации,думаю, это возможно,хотя это уже и не предмет вопроса,да и,скорее всего,лучше так, чем тянуть в утиль.и да, это сложно – Дмитрий Jun 18 '20 at 16:21
-1

Проблема в том что вы пытаетесь передать объект класса A в метод который принимает на вход объект интерфейса IA. Из-за этого возникает несовместимость типов. Рекомендую вам почитать про преобразование типов. В частности восходящее и нисходящее преобразование. На самом деле проблем может быть много, к примеру интерфейс может быть приватным и находится в другом классе, либо в другом package, либо могут быть другие синтаксические ошибки при которых вы попросту не сможете от него наследоваться.

(Попытаюсь объяснить проще, у всех классов по-дефолту один родитель - класс Object. У интерфейсов нету общего базового класса-родителя, и интерфейсы могут наследоваться (extends) только от других интерфейсов, а классы только от одного другого класса, но классы могут имплементировать (implements) несколько интерфейсов. На примере:

class A extends (class) B - Можно

сlass A extends (class) A, B - Множественное наследование классов запрещено

class A extends (interface) B - Нельзя, классы наследуются только от классов

class A implements (interface) B, C - Можно

interface A extends (class) B - Запрещено, поскольку класс изначально содержит в себе детали реализации (являясь потомком Object)

interface A extends (interface) A, B - наследоваться от нескольких интерфейсов можно.

interface A implements (interface/class) B - интерфейсы не могут имплементировать другие интерфейсы и тем более классы.

Дабы вам было понятнее почему так происходит, попытаюсь объяснить простыми словами. Классы придуманы для описания объектов и могут содержать в себе детали реализации. Интерфейсы придуманы для описания поведений, к примеру есть у нас класс собака - Dog. Этот класс описывает характеристики (цвет, рост, порода, выполняемые действия (методы внутри класса)). И есть интерфейсы - Move, Bark, Eat - которые описывают поведение (двигаться, гавкать, кушать) которое могут имплементировать другие классы. Из этого так же логически возникает факт того, что нужно хорошо подумать, прежде чем имплементировать какой-либо интерфейс. Поскольку к примеру класс Dog может имплементировать поведение гавкать, но если мы сделаем тоже с классом Cat (кошка) то это уже будет немного странно, не правда ли?

Советую в следующий раз вам прикрепить скриншот с структурой вашего проекта а так же содержимое IA, а так же информацию которую выводит компилятор, так вы сможете получить более детальный и точный ответ, поскольку ошибок/проблем может быть сразу несколько.

Sh4rx
  • 91
  • 8