Question

Java Generic Invariance: Why List<Dog> Is Not List<Animal>

javagenericsinheritancepolymorphism

Question

Assume this Java class hierarchy:

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

Given a method that accepts List<Animal>:

void doSomething(List<Animal> animals) {
    // ...
}

Why can a List<Dog> or List<Cat> not be passed to this method, even though Dog and Cat are subclasses of Animal? Why must the method explicitly use a wildcard such as List<? extends Animal> to accept lists of subclasses? More generally, why are Java generic types not implicitly polymorphic in this situation?

Short Answer

Java allows a Dog reference where an Animal reference is expected, but it does not make List<Dog> a subtype of List<Animal>. Generic types such as List<T> are invariant by default. This prevents code from inserting a Cat into a list that was created to contain only Dog objects. You can use List<? extends Animal> when a method only needs to read Animal values from a list.

Concept

Java inheritance applies to the objects themselves:

Animal animal = new Dog(); // Valid: Dog is an Animal

It does not automatically apply to generic containers:

List<Dog> dogs = new ArrayList<>();
// List<Animal> animals = dogs; // Does not compile

This rule is called generic invariance. For two types Dog and Animal, even when Dog extends Animal, Java treats List<Dog> and List<Animal> as separate, incompatible types.

The reason is safety. A List<Animal> promises that callers may add any Animal to it, including a Cat. But a List<Dog> must never contain a Cat.

If Java allowed this assignment, the following code would become possible:

List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // Imagine this were allowed
animals.add(new Cat());      // Valid for List<Animal>

Dog dog = dogs.get(0);       // A Cat could be returned here

The last line would violate the guarantee that dogs contains Dog values. Java prevents the problem at compile time by disallowing the assignment in the first place.

This differs from Java arrays. Arrays are covariant: Dog[] can be assigned to Animal[]. However, arrays must perform a runtime check and can throw ArrayStoreException. Generics choose earlier, compile-time safety instead.

Mental Model

Think of a List<Dog> as a kennel labelled Dogs Only. Every item in it must be a dog.

An Animal variable is like a general animal enclosure: it can hold a dog, cat, or another kind of animal. A single dog can safely go into an animal enclosure because it is an animal.

But a dogs-only kennel is not a general animal enclosure. If someone treats it as one, they could put a cat inside it. Java therefore keeps List<Dog> and List<Animal> separate.

List<? extends Animal> means: “a list containing some particular, unknown subtype of Animal.” It might be a dogs-only kennel or a cats-only kennel. You can safely take animals out and view them as Animal, but you cannot safely add a dog or cat because Java does not know which specific subtype the list accepts.

Take Quiz

Syntax and Examples

A method that requires a list whose element type is exactly Animal:

import java.util.List;

void addAnimals(List<Animal> animals) {
    animals.add(new Dog());
    animals.add(new Cat());
}

This method can add both dogs and cats, so it needs a list that is genuinely allowed to contain any Animal.

A method that only reads each value as an Animal should use an upper-bounded wildcard:

import java.util.List;

void printAnimals(List<? extends Animal> animals) {
    for (Animal animal : animals) {
        System.out.println(animal);
    }
}

Now all of these calls are valid:

List<Dog> dogs = List.of(new Dog(), new Dog());
List<Cat> cats = List.of(new Cat());
List<Animal> animals = List.of(new Dog(), new Cat());

printAnimals(dogs);
printAnimals(cats);
printAnimals(animals);

Inside printAnimals, each element can be read as an Animal. However, this is not allowed:

void printAnimals(List<? extends Animal> animals) {
    // animals.add(new Dog()); // Does not compile
}

The actual element type could be Cat, so adding a Dog would not always be safe. The only value you can add safely is null, but doing so is usually not useful.

Step by Step Execution

Consider this method:

import java.util.List;

void describeAnimals(List<? extends Animal> animals) {
    for (Animal animal : animals) {
        System.out.println(animal.getClass().getSimpleName());
    }
}

And this call:

List<Dog> dogs = List.of(new Dog(), new Dog());
describeAnimals(dogs);

Step by step:

  1. dogs has the type List<Dog>.
  2. Dog is a subtype of Animal.
  3. List<? extends Animal> accepts a list of an unknown type that extends Animal.
  4. Java can therefore pass dogs to describeAnimals.
  5. Within the method, Java knows every item is at least an Animal.
  6. The loop reads each item into an Animal variable safely.
  7. Java does not know whether the original list is a List<Dog>, List<Cat>, or another subtype list, so it blocks adding a specific animal subtype.

The wildcard preserves both facts: the caller keeps its specific list type, and the method can safely read common Animal behavior.

Real World Use Cases

  • Displaying domain objects: A UI method that renders names can accept List<? extends Person> so it can display employees, customers, or administrators.
  • Processing API results: A reporting function can read List<? extends Transaction> regardless of the specific transaction subtype returned by a service.
  • Calculating totals: Methods in Java’s standard library often use List<? extends Number> when they only need to read numeric values.
  • Copying data: A destination collection may accept a broader type while a source collection provides a narrower type.
  • Plugin systems: A framework can process collections of specific plugin implementations through a shared interface or base class without knowing the concrete implementation type.
Take Quiz

Real Codebase Usage

In real Java code, choose a generic parameter based on what the method does with the collection.

Read from a producer: ? extends T

Use ? extends T when the method receives values from the collection and treats them as T.

void sendNotifications(List<? extends Notification> notifications) {
    for (Notification notification : notifications) {
        notification.send();
    }
}

Write to a consumer: ? super T

Use ? super T when the method adds T values to the collection.

void addDefaultDogs(List<? super Dog> destination) {
    destination.add(new Dog());
}

The destination may be a List<Dog>, List<Animal>, or List<Object>, because all can store a Dog.

Preserve a specific type with a type parameter

Use a named type parameter when input and output must keep the same concrete subtype.

<T extends Animal> T firstAnimal(List<T> animals) {
    return animals.get(0);
}

If the input is List<Dog>, the result is known to be a Dog, not merely an Animal.

A common rule is PECS: Producer Extends, Consumer Super. A collection that produces values for your method usually uses extends; a collection that consumes values from your method usually uses super.

Common Mistakes

Assuming subclass relationships transfer to generic types

This does not compile:

List<Dog> dogs = new ArrayList<>();
// List<Animal> animals = dogs;

Avoid assuming that Container<Child> is a Container<Parent>. With ordinary Java generic classes, it is not.

Using ? extends Animal when items must be added

void addCat(List<? extends Animal> animals) {
    // animals.add(new Cat()); // Does not compile
}

Use List<Animal> if the method must add arbitrary Animal subtypes. Use List<? super Cat> if it specifically needs to add cats.

Using raw types to bypass the compiler

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

Raw types disable important generic checks and can cause failures later. Do not use them except when working carefully with unavoidable legacy APIs.

Casting a list to force compatibility

List<Dog> dogs = new ArrayList<>();
// List<Animal> animals = (List<Animal>) dogs; // Unsafe cast

This cast is not type-safe and may produce an unchecked warning. Redesign the method signature with a wildcard instead.

Confusing List<? extends Animal> with List<Animal>

List<? extends Animal> can refer to a list of dogs or cats, but it is not a mutable list into which arbitrary animals can be inserted. It is primarily a read-as-Animal view.

Comparisons

TypeCan pass List<Dog>?Can read as Animal?Can add Dog?Can add Cat?
List<Animal>NoYesYesYes
List<? extends Animal>YesYesNoNo
List<? super Dog>YesOnly as ObjectYesNo guarantee
List<Dog>Yes, when exactly requiredYesYesNo

Generics versus arrays

FeatureDog[]List<Dog>
Can be assigned to an Animal container type?Yes: Animal[] animals = dogsNo: List<Animal> animals = dogs
When is an invalid insertion detected?RuntimeCompile time
Typical failureArrayStoreExceptionCompilation error
Dog[] dogs = new Dog[1];
Animal[] animals = dogs;
// animals[0] = new Cat(); // Compiles, but throws ArrayStoreException at runtime

Java generics are intentionally stricter so this category of error is caught before the program runs.

Take Quiz

Cheat Sheet

  • Java generic types are invariant by default.
  • Dog extends Animal does not imply List<Dog> extends List<Animal>.
  • List<Animal> means the method may read and add any Animal subtype.
  • Use List<? extends Animal> to accept lists of Animal subtypes when reading values.
  • Use List<? super Dog> to add Dog values to a destination list.
  • Use <T extends Animal> when the method must preserve the precise subtype in its return type or between parameters.
  • With ? extends T, read values as T; do not add concrete T values.
  • With ? super T, add T; values read back are only guaranteed to be Object.
  • Memory rule: PECS — Producer Extends, Consumer Super.
  • Avoid raw List types and unchecked casts; they bypass generic safety.

FAQ

Is List<Dog> a subclass of List<Animal> in Java?

No. List<Dog> and List<Animal> are different invariant generic types, even though Dog is an Animal.

Why does Java not make generics covariant automatically?

Automatic covariance would allow code receiving List<Animal> to add a Cat to an object that is actually a List<Dog>. Invariance prevents this type-safety violation.

Why can I pass Dog[] where Animal[] is expected?

Java arrays are covariant for historical reasons. They detect invalid writes at runtime with ArrayStoreException. Generic collections reject the unsafe relationship at compile time.

Can I add anything to List<? extends Animal>?

You cannot safely add a specific Animal subtype because the list may actually be a List<Dog>, List<Cat>, or another subtype list. Technically, null is allowed.

When should I use List<Animal> instead of List<? extends Animal>?

Use List<Animal> when the method needs to add arbitrary Animal objects. Use List<? extends Animal> when it only needs to read animals.

What does ? super Dog mean?

It means an unknown supertype of Dog, such as Dog, Animal, or Object. It is useful when your method needs to add dogs to a collection.

Does Collection<Dog> work differently from List<Dog>?

No. The same invariance rule applies to normal Java generic types such as Collection<T>, Set<T>, and Map<K, V>.

Should every method parameter use wildcards?

No. Use a wildcard when it describes the method’s real needs. If the method must add mixed animal subtypes, List<Animal> is the correct and clearer type.

Take Quiz

Mini Project

Description

Build a small animal shelter utility with methods that inspect animals from subtype-specific lists and add dogs to suitable destination lists. The project demonstrates why reading and writing require different wildcard bounds.

Goal

Use ? extends Animal for safe reading and ? super Dog for safe writing without unsafe casts or raw types.

Requirements

Create Animal, Dog, and Cat classes with a name and a useful toString() method. Create a method that prints animals from a List<? extends Animal>. Create a method that adds two dogs to a List<? super Dog>. Call the reading method with both a dog list and a cat list. Call the writing method with a List<Animal> and print the resulting list.

Take Quiz

Keep learning

Related questions

View more