Является ли 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 ]

1 голос
/ 29 января 2016

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

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

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

1 голос
/ 15 декабря 2014

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

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

}

}

0 голосов
/ 20 июля 2018

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

List<Dog> dogs = new ArrayList<Dog>(); 
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());
0 голосов
/ 28 марта 2018

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

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

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

0 голосов
/ 02 марта 2018

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

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

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

0 голосов
/ 27 февраля 2016

Давайте возьмем пример из 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 было 2 варианта решения этой проблемы:

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

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

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

...