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

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

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

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

собака - кошки (дети)

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

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

16 ответов


нет, a List<Dog> is не 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>.


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

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


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

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


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

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

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

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


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

List<Dog> Не-а List<Animal> в Java

Это правда

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

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

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

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


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

List<Dog> и не подкласс List<Animal>.
но Dog[] is подкласс 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());

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

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

звучит неплохо, не так ли? Но вы можете только пройти Consumers и Suppliers для Animals. Если у вас есть 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());
}

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


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


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

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

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

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

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


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

в большинстве случаев, мы можем использовать Collection<? extends T> а то Collection<T> и это должно быть первым выбором. Тем не менее, я найти случаи, когда это сделать нелегко. Вопрос о том, всегда ли это лучшее, что можно сделать, остается открытым. Я представляю здесь класс DownCastCollection, который может принимать преобразование 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();

}

}


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

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

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
}

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

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


давайте возьмем пример из JavaSE учебник

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

Так Ява "архитекторов" было 2 варианты решения этой проблемы:

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

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

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


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

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

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


проблема была хорошо определены. Но есть решение; make doSomething generic:

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

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


другое решение-создать новый список

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