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

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

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

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

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

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

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

Ответы

Ответ 1

Нет, List<Dog> не является 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>.

Ответ 2

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

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

Ответ 3

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

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

Ответ 4

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

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

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

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

Ответ 5

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

List<Dog> isn't-a List<Animal> в Java

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

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

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

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

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

Ответ 6

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

List<Dog> - не подкласс List<Animal>.
Но Dog[] есть подкласс Animal[].

Массивы reifiable и ковариантные.
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());

Ответ 7

Основой логики такого поведения является то, что 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 без дженериков....

Ответ 8

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

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

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

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

Е. г.,

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

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

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

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());
}

где, имея интуитивные отношения LifeAnimalMammalDog, Cat и т.д., мы могли бы даже поместить a Mammal в пользователя Life, но а не String в пользователя Life.

Ответ 9

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

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());

Ответ 10

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

В большинстве случаев мы можем использовать Collection<? extends T>, а не Collection<T>, и это должен быть первый выбор. Однако я нахожу случаи, когда это непросто сделать. Он обсуждает вопрос о том, всегда ли это самое лучшее. Я представляю здесь класс DownCastCollection, который может преобразовать a Collection<? extends T> в Collection<T> (мы можем определить похожие классы для List, Set, NavigableSet,..), которые будут использоваться, когда использование стандартного подхода очень неудобно. Ниже приведен пример того, как его использовать (мы могли бы также использовать Collection<? extends Object> в этом случае, но я просто не могу проиллюстрировать использование DownCastCollection.

/**Could use Collection<? extends Object> and that is the better choice. 
* But I am doing this to illustrate how to use DownCastCollection. **/

public static void print(Collection<Object> col){  
    for(Object obj : col){
    System.out.println(obj);
    }
}
public static void main(String[] args){
  ArrayList<String> list = new ArrayList<>();
  list.addAll(Arrays.asList("a","b","c"));
  print(new DownCastCollection<Object>(list));
}

Теперь класс:

import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;

public class DownCastCollection<E> extends AbstractCollection<E> implements Collection<E> {
private Collection<? extends E> delegate;

public DownCastCollection(Collection<? extends E> delegate) {
    super();
    this.delegate = delegate;
}

@Override
public int size() {
    return delegate ==null ? 0 : delegate.size();
}

@Override
public boolean isEmpty() {
    return delegate==null || delegate.isEmpty();
}

@Override
public boolean contains(Object o) {
    if(isEmpty()) return false;
    return delegate.contains(o);
}
private class MyIterator implements Iterator<E>{
    Iterator<? extends E> delegateIterator;

    protected MyIterator() {
        super();
        this.delegateIterator = delegate == null ? null :delegate.iterator();
    }

    @Override
    public boolean hasNext() {
        return delegateIterator != null && delegateIterator.hasNext();
    }

    @Override
    public  E next() {
        if(!hasNext()) throw new NoSuchElementException("The iterator is empty");
        return delegateIterator.next();
    }

    @Override
    public void remove() {
        delegateIterator.remove();

    }

}
@Override
public Iterator<E> iterator() {
    return new MyIterator();
}



@Override
public boolean add(E e) {
    throw new UnsupportedOperationException();
}

@Override
public boolean remove(Object o) {
    if(delegate == null) return false;
    return delegate.remove(o);
}

@Override
public boolean containsAll(Collection<?> c) {
    if(delegate==null) return false;
    return delegate.containsAll(c);
}

@Override
public boolean addAll(Collection<? extends E> c) {
    throw new UnsupportedOperationException();
}

@Override
public boolean removeAll(Collection<?> c) {
    if(delegate == null) return false;
    return delegate.removeAll(c);
}

@Override
public boolean retainAll(Collection<?> c) {
    if(delegate == null) return false;
    return delegate.retainAll(c);
}

@Override
public void clear() {
    if(delegate == null) return;
        delegate.clear();

}

}

Ответ 11

Если вы уверены, что элементы списка являются подклассами данного типа супер, вы можете использовать список, используя этот подход:

(List<Animal>) (List<?>) dogs

Это полезно, когда вы хотите передать список в конструкторе или перебрать его

Ответ 12

Подтипирование является инвариантным для параметризованных типов. Даже жесткий класс 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
}

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

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

Ответ 13

Давайте возьмем пример из JavaSE tutorial

public abstract class Shape {
    public abstract void draw(Canvas c);
}

public class Circle extends Shape {
    private int x, y, radius;
    public void draw(Canvas c) {
        ...
    }
}

public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) {
        ...
    }
}

Итак, почему список собак (кругов) не должен считаться неявным списком животных (фигур) из-за этой ситуации:

// drawAll method call
drawAll(circleList);


public void drawAll(List<Shape> shapes) {
   shapes.add(new Rectangle());    
}

Итак, у "архитекторов" Java было два варианта решения этой проблемы:

  • не считают, что подтип неявно является супертипом и дает ошибку компиляции, как это происходит сейчас

  • считать подтип супертипом и ограничиться при компиляции метода "add" (так что в методе drawAll, если будет передан список подтипов формы, компилятор должен обнаружить это и ограничить вы с ошибкой компиляции в этом).

По понятным причинам это выбрало первый путь.

Ответ 14

У меня такая же проблема. Но общий тип поддержки <? extends Y> для устранения этой проблемы:

Collection<A> aCollection= getAFromDB(); AUtil.findMetConidition(aCollection);

Класс:

public interface A extends B {...}

Объявление метода:

b findMetConidition(Collection<? extends  B> bCollection ) { ...}

Ответ 15

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

Таким образом, мы имеем ListOfAnimal, ListOfDog, ListOfCat и т.д., которые представляют собой различные классы, которые в конечном итоге "создаются" компилятором при указании общих аргументов. И это плоская иерархия (на самом деле относительно List не является иерархией вообще).

Другой аргумент, почему ковариация не имеет смысла в случае общих классов, состоит в том, что в базе все классы одинаковы - это экземпляры List. Специализируясь на List, заполняя общий аргумент, не расширяет класс, он просто заставляет его работать для этого конкретного общего аргумента.

Ответ 16

Проблема была хорошо идентифицирована. Но есть решение; сделать doSomething generic:

<T extends Animal> void doSomething<List<T> animals) {
}

теперь вы можете вызвать doSomething с помощью List <Dog> или List <Cat> или List <Animal> .

Ответ 17

другим решением является создание нового списка

List<Dog> dogs = new ArrayList<Dog>(); 
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());