it-roy-ru.com

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

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

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

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

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

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

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

672
froadie

Нет, List<Dog> - это нет List<Animal>. Подумайте, что вы можете сделать с List<Animal> - вы можете добавить любое 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>.

817
Jon Skeet

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

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

72
Michael Ekstrand

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

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

42
Michael Aaron Safyan

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

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

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

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

32
Yishai

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

List<Dog> not-a List<Animal>в Java

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

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

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

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

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

30
einpoklum

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

List<Dog> является подклассом notList<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());
8
outdev

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

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

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

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

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.

4
glglgl

Базовая логика для такого поведения состоит в том, что 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
Hitesh

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

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());
3
Angel Koh

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

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

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

1
sagits

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

Чтобы понять это далее, давайте посмотрим на байт-код, сгенерированный 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
}

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

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

1
Root G

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

В большинстве случаев мы можем использовать 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();

}

}

1
dan b

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

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

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

0
gerardw

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

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

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

0
Cristik

Давайте возьмем пример из 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());    
}

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

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

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

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

0
aurelius

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

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