Я немного озадачен тем, как дженерики Java обрабатывают наследование / полиморфизм.
Предположим следующую иерархию -
Животное (родитель)
Собака - Кот (Дети)
Итак, предположим, у меня есть метод doSomething(List<Animal> animals)
. По всем правилам наследования и полиморфизма я бы предположил, что a List<Dog>
есть a, List<Animal>
а a List<Cat>
есть a, List<Animal>
и поэтому любой из них может быть передан этому методу. Не так. Если я хочу , чтобы добиться такого поведения, я должен явно указать метод , чтобы принять список любого подкласса животных говоря doSomething(List<? extends Animal> animals)
.
Я понимаю, что это поведение Java. У меня вопрос почему ? Почему полиморфизм обычно неявный, но когда дело доходит до дженериков, он должен быть указан?
java
generics
inheritance
polymorphism
froadie
источник
источник
Ответы:
Нет,
List<Dog>
это неList<Animal>
. Подумайте, что вы можете сделать сList<Animal>
- вы можете добавить к нему любое животное ... включая кошку. Теперь, вы можете логически добавить кошку в помет щенков? Точно нет.Внезапно у вас очень запутанная кошка.
Теперь вы не можете добавить
Cat
к,List<? extends Animal>
потому что вы не знаете, что этоList<Cat>
. Вы можете получить значение и знать, что оно будетAnimal
, но вы не можете добавлять произвольных животных. Обратное верно дляList<? super Animal>
- в этом случае вы можете безопасно добавитьAnimal
к нему, но вы ничего не знаете о том, что можно извлечь из него, потому что это может бытьList<Object>
.источник
То, что вы ищете, называется параметрами ковариантного типа . Это означает, что если один тип объекта может быть заменен другим в методе (например,
Animal
может быть заменен наDog
), то же самое относится к выражениям, использующим эти объекты (поэтомуList<Animal>
может быть заменено наList<Dog>
). Проблема в том, что ковариация не является безопасной для изменяемых списков в целом. Предположим, у вас естьList<Dog>
, и он используется какList<Animal>
. Что происходит, когда вы пытаетесь добавить к этому кота,List<Animal>
который на самом делеList<Dog>
? Автоматическое разрешение ковариантности параметров типа нарушает систему типов.Было бы полезно добавить синтаксис, позволяющий указывать параметры типа как ковариантные, что исключает
? extends Foo
объявления методов in, но добавляет дополнительную сложность.источник
Причина, по которой a
List<Dog>
не является aList<Animal>
, заключается в том, что, например, вы можете вставить aCat
в aList<Animal>
, но не вList<Dog>
... вы можете использовать подстановочные знаки, чтобы сделать дженерики более расширяемыми, где это возможно; например, чтение из aList<Dog>
похоже на чтение изList<Animal>
- но не запись.В Обобщения в языке Java и секции по Дженерики из Java Tutorials имеют очень хорошее, глубокое объяснение, почему некоторые вещи являются или не являются полиморфными или разрешаться с обобщениями.
источник
Я думаю, что к тому, что упоминают другие ответы, следует добавить
это также правда, что
То, как работает интуиция ОП - что, разумеется, совершенно верно, - это последнее предложение. Однако, если мы применим эту интуицию, мы получим язык, который не является Java-esque в своей системе типов: предположим, что наш язык позволяет добавлять кошку в наш список собак. Что бы это значило? Это означало бы, что список перестает быть списком собак и остается просто списком животных. И список млекопитающих, и список четвероногих.
Другими словами: A
List<Dog>
на Java не означает «список собак» на английском языке, это означает «список, в котором могут быть собаки и ничего больше».В более общем смысле, интуиция OP ориентирована на язык, в котором операции над объектами могут изменять свой тип , или, скорее, тип (ы) объекта является (динамической) функцией его значения.
источник
Я бы сказал, что весь смысл Generics в том, что он этого не допускает. Рассмотрим ситуацию с массивами, которые допускают этот тип ковариации:
Этот код прекрасно компилируется, но выдает ошибку времени выполнения (
java.lang.ArrayStoreException: java.lang.Boolean
во второй строке). Это небезопасно. Задача Generics состоит в том, чтобы добавить безопасность типов времени компиляции, в противном случае вы можете просто придерживаться простого класса без обобщений.Сейчас бывают моменты, когда вам нужно быть более гибким, и для этого
? super Class
и? extends Class
нужны. Первый - это когда вам нужно вставить в типCollection
(например), а второй - когда вам нужно читать из него безопасным для типа способом. Но единственный способ сделать оба одновременно - это иметь определенный тип.источник
Чтобы понять проблему, полезно сравнить с массивами.
List<Dog>
не является подклассомList<Animal>
.Но
Dog[]
это подклассAnimal[]
.Массивы являются переопределимыми и ковариантными .
Reifiable означает, что информация о их типе полностью доступна во время выполнения.
Поэтому массивы обеспечивают безопасность типа времени выполнения, но не безопасность типа времени компиляции.
Для дженериков все наоборот: дженерики стираются и
остаются неизменными .
Поэтому дженерики не могут обеспечить безопасность типов во время выполнения, но они обеспечивают безопасность типов во время компиляции.
В приведенном ниже коде, если генерики были ковариантными, в строке 3 будет возможно загрязнение кучи .
источник
Ответы, данные здесь, не полностью убедили меня. Поэтому вместо этого я приведу другой пример.
звучит нормально, не так ли? Но вы можете передать только
Consumer
s иSupplier
s дляAnimal
s. Если у вас естьMammal
потребитель, ноDuck
поставщик, они не должны подходить, хотя оба являются животными. Чтобы запретить это, были добавлены дополнительные ограничения.Вместо вышеупомянутого мы должны определить отношения между типами, которые мы используем.
Например,
гарантирует, что мы можем использовать только поставщика, который предоставляет нам правильный тип объекта для потребителя.
Ото, мы могли бы также сделать
куда мы идем другим путем: мы определяем тип
Supplier
и ограничиваем его использование вConsumer
.Мы даже можем сделать
где, имея интуитивные отношения
Life
->Animal
->Mammal
->Dog
иCat
т. д., мы могли бы даже поместитьMammal
вLife
потребителя, но неString
вLife
потребителя.источник
(Consumer<Runnable>, Supplier<Dog>)
покаDog
подтипAnimal & Runnable
Основой логики такого поведения является то, что
Generics
следуют механизму стирания типа. Таким образом, во время выполнения у вас нет никакого способа, если вы идентифицируете тип вcollection
отличие от того,arrays
где нет такого процесса стирания. Итак, возвращаясь к вашему вопросу ...Итак, предположим, что есть метод, описанный ниже:
Теперь, если java позволяет вызывающей стороне добавлять Список типа Animal к этому методу, вы можете добавить в коллекцию неправильную вещь, и во время выполнения она также будет работать из-за удаления типа. Хотя в случае массивов вы получите исключение времени выполнения для таких сценариев ...
Таким образом, по сути, это поведение реализовано так, что нельзя добавить в коллекцию не то, что нужно. Теперь я считаю, что стирание типов существует, чтобы обеспечить совместимость с унаследованной Java без использования обобщений ....
источник
Подтипирование является инвариантным для параметризованных типов. Даже если класс
Dog
является подтипомAnimal
, параметризованный типList<Dog>
не является подтипомList<Animal>
. Напротив, ковариантныйDog[]
подтип используется массивами, поэтому тип массива является подтипомAnimal[]
.Инвариантный подтип гарантирует, что ограничения типов, навязанные Java, не будут нарушены. Рассмотрим следующий код, данный @Jon Skeet:
Как утверждает @Jon Skeet, этот код недопустим, потому что в противном случае он нарушил бы ограничения типа, возвращая кошку, когда собака ожидала.
Поучительно сравнить приведенное выше с аналогичным кодом для массивов.
Код является законным. Однако выдает исключение хранилища массива . Массив несет свой тип во время выполнения, так что JVM может обеспечить безопасность типов ковариантного подтипирования.
Чтобы понять это далее, давайте посмотрим на байт-код, сгенерированный
javap
классом ниже:Используя команду
javap -c Demonstration
, это показывает следующий байт-код Java:Обратите внимание, что переведенный код тела метода идентичен. Компилятор заменил каждый параметризованный тип его стиранием . Это свойство имеет решающее значение, так как оно не нарушает обратную совместимость.
В заключение, безопасность во время выполнения невозможна для параметризованных типов, поскольку компилятор заменяет каждый параметризованный тип его стиранием. Это делает параметризованные типы не более чем синтаксическим сахаром.
источник
На самом деле вы можете использовать интерфейс для достижения того, что вы хотите.
}
Затем вы можете использовать коллекции с помощью
источник
Если вы уверены, что элементы списка являются подклассами этого заданного супертипа, вы можете привести список, используя этот подход:
Это полезно, когда вы хотите передать список в конструкторе или перебрать его
источник
ответ , а также другие правильные ответы. Я собираюсь добавить к этим ответам решение, которое, я думаю, будет полезным. Я думаю, что это часто встречается в программировании. Следует отметить, что для коллекций (списков, наборов и т. Д.) Основной проблемой является добавление в коллекцию. Вот где все рушится. Даже удаление в порядке.
В большинстве случаев мы можем использовать именно
Collection<? extends T>
тогда,Collection<T>
и это должно быть первым выбором. Однако я нахожу случаи, когда это нелегко сделать. Это спор о том, всегда ли это лучше всего делать. Я представляю здесь класс DownCastCollection, который может преобразовать aCollection<? extends T>
в aCollection<T>
(мы можем определить аналогичные классы для List, Set, NavigableSet, ..), которые будут использоваться при использовании стандартного подхода, очень неудобно. Ниже приведен пример того, как его использовать (мы могли бы также использоватьCollection<? extends Object>
в этом случае, но я оставляю это простым для иллюстрации, используя DownCastCollection.Теперь класс:
}
источник
Collections.unmodifiableCollection
Collection<? extends E>
уже обрабатывает это поведение правильно, если только вы не используете его способом, который не является типобезопасным (например, приведение его к чему-то другому). Единственное преимущество, которое я вижу, это когда вы вызываетеadd
операцию, она генерирует исключение, даже если вы ее произвели.Давайте возьмем пример из учебника по JavaSE
Так почему список собак (кружков) не следует неявно считать списком животных (фигур) из-за этой ситуации:
Таким образом, у Java-архитекторов было два варианта решения этой проблемы:
не считайте, что подтип неявно является его супертипом, и выдают ошибку компиляции, как это происходит сейчас
Считайте, что подтип является его супертипом, и ограничивайте при компиляции метод «add» (поэтому в методе drawAll, если будет передан список окружностей, подтип формы, компилятор должен обнаружить это и ограничить вас ошибкой компиляции. который).
По понятным причинам, что выбрали первый путь.
источник
Мы также должны принять во внимание, как компилятор угрожает универсальным классам: в «создает» другой тип всякий раз, когда мы заполняем универсальные аргументы.
Таким образом , мы имеем
ListOfAnimal
,ListOfDog
,ListOfCat
и т.д., которые являются различными классами , которые в конечном итоге «создал» компилятор , когда мы указуем общие аргументы. И это плоская иерархия (на самом деле этоList
вообще не иерархия).Другим аргументом, почему ковариация не имеет смысла в случае универсальных классов, является тот факт, что в основе все классы одинаковы - это
List
экземпляры. Специализация aList
путем заполнения универсального аргумента не расширяет класс, а просто заставляет его работать с этим конкретным универсальным аргументом.источник
Проблема была хорошо определена. Но есть решение; сделайте что- нибудь общее:
теперь вы можете вызывать doSomething с помощью List <Dog> или List <Cat> или List <Animal>.
источник
Другое решение заключается в создании нового списка
источник
В дополнение к ответу Джона Скита, который использует этот пример кода:
На самом глубоком уровне, проблема здесь в том, что
dogs
иanimals
поделиться ссылкой. Это означает, что один из способов сделать эту работу - скопировать весь список, что нарушит равенство ссылок:После вызова
List<Animal> animals = new ArrayList<>(dogs);
, вы не можете впоследствии напрямую назначитьanimals
либоdogs
илиcats
:поэтому вы не можете поместить неправильный подтип
Animal
в список, потому что нет неправильного подтипа - любой объект подтипа? extends Animal
может быть добавленanimals
.Очевидно, это меняет семантику, поскольку списки
animals
иdogs
больше не являются общими, поэтому добавление в один список не добавляет в другой (это именно то, что вы хотите, чтобы избежать проблемы, которуюCat
можно добавить в список, который является только должен содержатьDog
объекты). Кроме того, копирование всего списка может быть неэффективным. Однако это решает проблему эквивалентности типов, нарушая равенство ссылок.источник