Является ли List <Dog>подклассом List <Animal>? Почему дженерики Java не являются неявно полиморфными? - PullRequest
707 голосов
/ 30 апреля 2010

Я немного озадачен тем, как дженерики Java обрабатывают наследование / полиморфизм.

Предположим следующую иерархию -

Животное (родитель)

Собака - Кошка (Дети)

Итак, предположим, у меня есть метод doSomething(List<Animal> animals). По всем правилам наследования и полиморфизма я бы предположил, что List<Dog> равен a List<Animal>, а List<Cat> равен a List<Animal> - и поэтому любой быть переданным этому методу. Не так. Если я хочу добиться такого поведения, я должен явно указать методу, чтобы он принимал список любого подкласса Animal, сказав doSomething(List<? extends Animal> animals).

Я понимаю, что это поведение Java. У меня вопрос почему ? Почему полиморфизм обычно неявный, но когда дело доходит до дженериков, он должен быть указан?

Ответы [ 16 ]

842 голосов
/ 30 апреля 2010

Нет, List<Dog> - это , а не a List<Animal>. Подумайте, что вы можете сделать с List<Animal> - вы можете добавить к нему любое животное ... включая кошку. Теперь, вы можете логически добавить кошку в помет щенков? Абсолютно нет.

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

Внезапно у вас появляется очень растерянный кот.

Теперь вы не можете добавить Cat к List<? extends Animal>, потому что вы не знаете, что это List<Cat>. Вы можете получить значение и знать, что это будет Animal, но вы не можете добавлять произвольных животных. Обратное верно для List<? super Animal> - в этом случае вы можете безопасно добавить Animal к нему, но вы ничего не знаете о том, что можно извлечь из него, поскольку это может быть List<Object>.

73 голосов
/ 30 апреля 2010

То, что вы ищете, называется ковариантный тип параметры . Это означает, что если один тип объекта может быть заменен другим методом в методе (например, Animal может быть заменен на Dog), то же самое относится к выражениям, использующим эти объекты (поэтому List<Animal> можно заменить на List<Dog>). Проблема в том, что ковариантность небезопасна для изменяемых списков в целом. Предположим, у вас есть List<Dog>, и он используется как List<Animal>. Что происходит, когда вы пытаетесь добавить Кошку к этому List<Animal>, который на самом деле List<Dog>? Автоматическое разрешение ковариантности параметров типа нарушает систему типов.

Было бы полезно добавить синтаксис, позволяющий указывать параметры типа как ковариантные, что исключает ? extends Foo в объявлениях методов, но добавляет дополнительную сложность.

43 голосов
/ 30 апреля 2010

Причина, по которой List<Dog> не является List<Animal>, заключается в том, что, например, вы можете вставить Cat в List<Animal>, но не в List<Dog> ... вы можете использовать групповые символы для сделать генерики более расширяемыми, где это возможно; например, чтение из List<Dog> аналогично чтению из List<Animal>, но не из записи.

Обобщение в языке Java и Раздел по родовым элементам из учебников по Java дают очень хорошее, подробное объяснение того, почему некоторые вещи являются или не являются полиморфными или разрешено с дженериками.

33 голосов
/ 30 марта 2013

Я думаю, что следует добавить к тому, что другие ответы упомянули, что пока

List<Dog> not-a List<Animal> на Java

также верно, что

Список собак - это список животных на английском языке (ну при разумной интерпретации)

То, как работает интуиция ОП - что, конечно, полностью верно, - это последнее предложение. Однако, если мы применим эту интуицию, мы получим язык, который не является Java-esque в своей системе типов: предположим, что наш язык позволяет добавлять кошку в наш список собак. Что бы это значило? Это означало бы, что список перестает быть списком собак и остается просто списком животных. И список млекопитающих, и список четвероногих.

Другими словами: List<Dog> в Java не означает «список собак» на английском языке, это означает «список, в котором могут быть собаки и ничего больше».

В целом, Интуиция OP поддается языку, в котором операции над объектами могут изменять свой тип , или, скорее, тип (ы) объекта является (динамической) функцией его значения.

32 голосов
/ 30 апреля 2010

Я бы сказал, что весь смысл дженериков в том, что он этого не допускает. Рассмотрим ситуацию с массивами, которые допускают ковариацию этого типа:

  Object[] objects = new String[10];
  objects[0] = Boolean.FALSE;

Этот код прекрасно компилируется, но выдает ошибку времени выполнения (java.lang.ArrayStoreException: java.lang.Boolean во второй строке). Это небезопасно. Задача Generics - добавить безопасность типов времени компиляции, в противном случае вы могли бы просто придерживаться простого класса без generics.

Теперь бывают моменты, когда вам нужно быть более гибким, и именно для этого нужны ? super Class и ? extends Class. Первый - когда вам нужно вставить в тип Collection (например), а второй - когда вам нужно читать с него, безопасным способом. Но единственный способ сделать оба одновременно - это иметь определенный тип.

9 голосов
/ 29 сентября 2017

Чтобы понять проблему, полезно сравнить с массивами.

List<Dog> является не подклассом List<Animal>.
Но Dog[] является подклассом Animal[].

Массивы пригодны для повторного использования и ковариантны .
Reifiable означает, что информация о их типе полностью доступна во время выполнения.
Поэтому массивы обеспечивают безопасность типа во время выполнения, но не безопасность типа во время компиляции.

    // All compiles but throws ArrayStoreException at runtime at last line
    Dog[] dogs = new Dog[10];
    Animal[] animals = dogs; // compiles
    animals[0] = new Cat(); // throws ArrayStoreException at runtime

Для дженериков все наоборот:
Обобщения стерты и инвариант .
Следовательно, дженерики не могут обеспечить безопасность типов во время выполнения, но они обеспечивают безопасность типов во время компиляции.
В приведенном ниже коде, если генерики были ковариантными, можно будет сделать загрязнение кучи в строке 3.

    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
    animals.add(new Cat());
5 голосов
/ 04 декабря 2012

Основная логика для такого поведения состоит в том, что Generics следует механизму стирания типа. Таким образом, во время выполнения у вас нет способа определить тип collection в отличие от arrays, где нет такого процесса стирания. Итак, возвращаясь к вашему вопросу ...

Итак, предположим, что есть метод, описанный ниже:

add(List<Animal>){
    //You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}

Теперь, если java позволяет вызывающей стороне добавлять Список типа Animal в этот метод, вы можете добавить в коллекцию неправильную вещь, и во время выполнения она также будет запущена из-за удаления типа. Хотя в случае массивов вы получите исключение времени выполнения для таких сценариев ...

Таким образом, по сути, это поведение реализовано так, что нельзя добавить в коллекцию неправильную вещь. Теперь я считаю, что стирание типов существует, чтобы обеспечить совместимость с унаследованной Java без использования обобщений ....

4 голосов
/ 15 февраля 2015

Ответы, данные здесь, не полностью убедили меня. Поэтому вместо этого я приведу другой пример.

public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
    consumer.accept(supplier.get());
}

звучит нормально, не так ли? Но вы можете передать только Consumer с и Supplier с за Animal с. Если у вас есть Mammal потребитель, но Duck поставщик, они не должны подходить, хотя оба являются животными. Чтобы запретить это, были добавлены дополнительные ограничения.

Вместо вышесказанного мы должны определить отношения между используемыми нами типами.

E. г.,

public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) {
    consumer.accept(supplier.get());
}

гарантирует, что мы можем использовать только поставщика, который предоставляет нам правильный тип объекта для потребителя.

ОТО, мы могли бы также сделать

public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
    consumer.accept(supplier.get());
}

, куда мы идем другим путем: мы определяем тип Supplier и ограничиваем его использование в Consumer.

Мы даже можем сделать

public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) {
    consumer.accept(supplier.get());
}

где, имея интуитивные отношения Life -> Animal -> Mammal -> Dog, Cat и т. Д., Мы могли бы даже поставить Mammal в Life потребителя, но не String в Life потребителя.

3 голосов
/ 12 июля 2015

На самом деле вы можете использовать интерфейс для достижения того, что вы хотите.

public interface Animal {
    String getName();
    String getVoice();
}
public class Dog implements Animal{
    @Override 
    String getName(){return "Dog";}
    @Override
    String getVoice(){return "woof!";}

}

затем вы можете использовать коллекции, используя

List <Animal> animalGroup = new ArrayList<Animal>();
animalGroup.add(new Dog());
1 голос
/ 03 мая 2018

Подтип инвариант для параметризованных типов. Даже если класс Dog является подтипом Animal, параметризованный тип List<Dog> не является подтипом List<Animal>. Напротив, ковариантный подтип используется массивами, поэтому массив тип Dog[] является подтипом Animal[].

Инвариантный подтип гарантирует, что ограничения типов, навязанные Java, не будут нарушены. Рассмотрим следующий код, данный @Jon Skeet:

List<Dog> dogs = new ArrayList<Dog>(1);
List<Animal> animals = dogs;
animals.add(new Cat()); // compile-time error
Dog dog = dogs.get(0);

Как сказал @Jon Skeet, этот код недопустим, потому что в противном случае он нарушил бы ограничения типа, возвращая кошку, когда собака ожидала.

Поучительно сравнить приведенное выше с аналогичным кодом для массивов.

Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];

Код является законным. Однако выдает исключение хранилища массива . Массив переносит свой тип во время выполнения, так что JVM может применять безопасность типов ковариантных подтипов.

Чтобы понять это далее, давайте посмотрим на байт-код, сгенерированный javap класса ниже:

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

public class Demonstration {
    public void normal() {
        List normal = new ArrayList(1);
        normal.add("lorem ipsum");
    }

    public void parameterized() {
        List<String> parameterized = new ArrayList<>(1);
        parameterized.add("lorem ipsum");
    }
}

Используя команду javap -c Demonstration, это показывает следующий байт-код Java:

Compiled from "Demonstration.java"
public class Demonstration {
  public Demonstration();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void normal();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: iconst_1
       5: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V
       8: astore_1
       9: aload_1
      10: ldc           #4                  // String lorem ipsum
      12: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      17: pop
      18: return

  public void parameterized();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: iconst_1
       5: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V
       8: astore_1
       9: aload_1
      10: ldc           #4                  // String lorem ipsum
      12: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      17: pop
      18: return
}

Обратите внимание, что переведенные коды тел методов идентичны. Компилятор заменил каждый параметризованный тип своим стиранием . Это свойство имеет решающее значение, так как оно не нарушает обратную совместимость.

В заключение, безопасность во время выполнения невозможна для параметризованных типов, поскольку компилятор заменяет каждый параметризованный тип своим стиранием. Это делает параметризованные типы не более чем синтаксическим сахаром.

...