Является ли 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. У меня вопрос почему? Почему полиморфизм обычно неявный, но когда дело доходит до дженериков, он должен быть указан?

20 ответов

Решение

Нет 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>,

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

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

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

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

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

List<Dog> не правда-а List<Animal> на Яве

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

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

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

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

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

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

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

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

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

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

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

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

Тем не менее, многие люди посещают этот вопрос в поисках решения.

Итак, решение этой проблемы в современной java выглядит следующим образом:

(Примечание: S = суперкласс)

      List<S> supers = List.copyOf( descendants );

Объяснение того, почему это безопасно (принимая во внимание потенциальные ловушки, упомянутые в других ответах), и почему это лучший способ добиться этого, см. в соответствующем вопросе и моем ответе на него в 2022 году: /questions/47753476/naibolee-effektivnyij-sposob-privedeniya-listsubclass-k-listbaseclass/62283884#62283884

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

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

звучит нормально, не так ли? Но вы можете только пройти Consumerс и Supplierс для 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 потребитель.

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

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

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

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

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

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

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

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

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

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

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

}

}

Проблема была правильно определена как относящаяся к отклонениям, но детали неверны. Чисто функциональный список - это функтор ковариантных данных, что означает, что если тип Sub является подтипом Super, то список Sub определенно является подтипом списка Super.

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

Несколько упрощая, давайте рассмотрим присвоение объекта B типа T объекту A типа T как мутацию. Это без потери общности: мутацию A можно записать как A = f (A), где f: T -> T. Проблема, конечно, в том, что, хотя функции ковариантны в своей области, они контравариантны в своей домен, но с назначениями домен и кодомен совпадают, поэтому присвоение инвариантно!

Из этого следует, обобщая, что подтипы не могут изменяться. Но в объектной ориентации мутация является фундаментальной, поэтому объектная ориентация изначально ошибочна.

Вот простой пример: в чисто функциональной настройке симметричная матрица явно является матрицей, это подтип, без проблем. Теперь давайте добавим в матрицу возможность устанавливать один элемент в координатах (x,y) с правилом, что другие элементы не меняются. Теперь симметричная матрица больше не является подтипом, если вы измените (x,y), вы также измените (y,x). Функциональная операция - дельта: Sym -> Mat, если вы измените один элемент симметричной матрицы, вы получите обратно общую несимметричную матрицу. Поэтому, если вы включили в Mat метод "изменить один элемент", Sym не является подтипом. На самом деле... подходящих подтипов почти наверняка НЕТ.

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

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

В любом случае здесь есть разумное обсуждение:

https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)

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

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

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

Давайте возьмем пример из учебника по 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, если будет передан список окружностей, подтип формы, компилятор должен обнаружить это и ограничить вас ошибкой компиляции. тот).

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

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

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

В дополнение к ответу Джона Скита, который использует этот пример кода:

// 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?

На самом глубоком уровне проблема в том, что dogs а также animalsподелитесь ссылкой. Это означает, что один из способов сделать эту работу - скопировать весь список, что нарушит равенство ссылок:

// This code is fine
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog());
List<Animal> animals = new ArrayList<>(dogs); // Copy list
animals.add(new Cat());
Dog dog = dogs.get(0);   // This is fine now, because it does not return the Cat

После звонка List<Animal> animals = new ArrayList<>(dogs);, вы не можете впоследствии напрямую назначить animals либо dogs или cats:

// These are both illegal
dogs = animals;
cats = animals;

поэтому вы не можете указать неправильный подтип Animal в список, потому что нет неправильного подтипа - любой объект подтипа ? extends Animal можно добавить к animals.

Очевидно, это меняет семантику, поскольку списки animals а также dogs больше не являются общими, поэтому добавление в один список не добавляет к другому (это именно то, что вы хотите, чтобы избежать проблемы, связанной с Cat можно добавить в список, который должен содержать только Dogобъекты). Кроме того, копирование всего списка может быть неэффективным. Однако это решает проблему эквивалентности типов, нарушая ссылочное равенство.

Проблема была хорошо определена. Но есть решение; сделайте что- нибудь общее:

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

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

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

Давайте продолжим и создадим упрощенную иерархию классов Animal.

abstract class Animal {
    void eat() {
        System.out.println("animal eating");
    }
}

class Dog extends Animal {
    void bark() { }
}

class Cat extends Animal {
    void meow() { }
}

Теперь давайте посмотрим на наших старых друзей Arrays, которые, как мы знаем, неявно поддерживают полиморфизм -

class TestAnimals {
    public static void main(String[] args) {
        Animal[] animals = {new Dog(), new Cat(), new Dog()};
        Dog[] dogs = {new Dog(), new Dog(), new Dog()};
        takeAnimals(animals);
        takeAnimals(dogs);
    }

    public void takeAnimals(Animal[] animals) {
        for(Animal a : animals) {
            System.out.println(a.eat());
        }
    }   
}

Класс компилируется нормально, и когда мы запускаем указанный выше класс, мы получаем результат

animal eating
animal eating
animal eating
animal eating
animal eating
animal eating

Здесь следует отметить, что метод takeAnimals() определен для приема всего, что относится к типу Animal, он может принимать массив типа Animal, а также может принимать массив Dog, потому что Dog-is-a-Animal. Итак, это Полиморфизм в действии.

Давайте теперь использовать тот же подход с дженериками,

Теперь предположим, что мы немного подправили наш код и используем ArrayLists вместо Arrays -

class TestAnimals {
    public static void main(String[] args) {
        ArrayList<Animal> animals = new ArrayList<Animal>();
        animals.add(new Dog());
        animals.add(new Cat());
        animals.add(new Dog());
        takeAnimals(animals);
    }

    public void takeAnimals(ArrayList<Animal> animals) {
        for(Animal a : animals) {
            System.out.println(a.eat());
        }
    }   
}

Приведенный выше класс будет компилироваться и выдаст результат -

animal eating
animal eating
animal eating
animal eating
animal eating
animal eating

Итак, мы знаем, что это работает, теперь давайте немного настроим этот класс, чтобы полиморфно использовать тип Animal -

class TestAnimals {
    public static void main(String[] args) {
        ArrayList<Animal> animals = new ArrayList<Animal>();
        animals.add(new Dog());
        animals.add(new Cat());
        animals.add(new Dog());

        ArrayList<Dog> dogs = new ArrayList<Dog>();
        takeAnimals(animals);
        takeAnimals(dogs);
    }

    public void takeAnimals(ArrayList<Animal> animals) {
        for(Animal a : animals) {
            System.out.println(a.eat());
        }
    }   
}

Похоже, что не должно возникнуть проблем с компиляцией вышеуказанного класса, поскольку метод takeAnimals() предназначен для приема любого ArrayList типа Animal и Dog-is-a-Animal, поэтому здесь он не должен мешать.

Но, к сожалению, компилятор выдает ошибку и не позволяет нам передать Dog ArrayList переменной, ожидающей Animal ArrayList.

Вы спросите, почему?

Просто представьте, если бы JAVA разрешила поместить Dog ArrayList - dogs - в Animal ArrayList - animals - а затем внутри метода takeAnimals() кто-то сделает что-то вроде:

animals.add(new Cat());

думая, что это должно быть выполнимо, потому что в идеале это Animal ArrayList, и вы должны иметь возможность добавить к нему любую кошку как Cat-is-also-a-Animal, но на самом деле вы передали ему ArrayList типа Dog.

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

Если кто-то попытается сделать то же самое с массивами, тогда массивы также будут выдавать ошибку, но массивы обрабатывают эту ошибку во время выполнения, тогда как ArrayLists обрабатывают эту ошибку во время компиляции.

Другие вопросы по тегам