Skip to content
Java polymorphism 5 min read

Covariant Return Type

When you override a method, Java normally requires the overriding method to have the exact same return type. Covariant return types relax that rule: the overriding method is allowed to return a more specific (narrower) subtype of the original return type. This small feature removes a surprising number of ugly casts from real-world code.

The Problem Before Covariant Return Types

Before Java 5, every overriding method had to match the parent’s return type exactly. If the parent returned Animal, the child had to return Animal too — even if it logically always returned a Dog. Callers had to cast the result themselves.

class Animal {
    public Animal create() {
        return new Animal();
    }
}

class Dog extends Animal {
    // Pre-Java 5: must match return type exactly
    @Override
    public Animal create() {         // returns Animal, not Dog
        return new Dog();
    }
}

// Caller needed an ugly cast:
Dog d = (Dog) new Dog().create();

This worked, but the cast was boilerplate noise that obscured intent and could throw ClassCastException if someone refactored carelessly.

Covariant Return Types to the Rescue (Java 5+)

Since Java 5, the overriding method may narrow the return type to any subclass of the parent’s declared return type. The compiler verifies the subtype relationship at compile time, so no runtime cast is needed.

class Animal {
    public Animal create() {
        return new Animal();
    }
}

class Dog extends Animal {
    @Override
    public Dog create() {       // Dog IS-A Animal — perfectly legal
        return new Dog();
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog().create();   // no cast needed!
        System.out.println(d.getClass().getSimpleName());
    }
}

Output:

Dog

The compiler sees that Dog is a subtype of Animal, so the override is valid. The caller receives a Dog reference directly.

Note: Covariant return types only allow you to narrow the return type (go from supertype to subtype). You can never widen it — returning Object when the parent declares Animal would be a compile error.

A Practical Example: Builder / Factory Pattern

Covariant return types shine brightest in fluent builders and factory methods, where each subclass should return its own type.

class Vehicle {
    private String color;

    public Vehicle setColor(String color) {
        this.color = color;
        return this;           // returns Vehicle
    }

    @Override
    public String toString() {
        return "Vehicle[color=" + color + "]";
    }
}

class Car extends Vehicle {
    private int doors;

    @Override
    public Car setColor(String color) {   // covariant: returns Car
        super.setColor(color);
        return this;
    }

    public Car setDoors(int doors) {
        this.doors = doors;
        return this;
    }

    @Override
    public String toString() {
        return "Car[color=" + super.toString() + ", doors=" + doors + "]";
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car()
                .setColor("Red")   // returns Car, not Vehicle
                .setDoors(4);      // fluent chain works!
        System.out.println(car);
    }
}

Output:

Car[color=Vehicle[color=Red], doors=4]

Without covariant return types, setColor() would return Vehicle, breaking the fluent chain — you would not be able to call .setDoors(4) without casting.

Rules for Covariant Return Types

RuleDetails
Return type must be a subtypeThe overriding method’s return type must extend or implement the parent’s return type
Applies to class and interface hierarchiesWorks for both class inheritance and interface implementation
Primitives cannot be covariantint and long have no subtype relationship — only reference types qualify
Access modifier cannot be more restrictiveUnrelated to return type, but a reminder: overriding visibility rules still apply
@Override is recommendedLets the compiler confirm the override is intentional
// Primitives — NOT allowed
class Base {
    public long getValue() { return 42L; }
}
class Derived extends Base {
    // Compile error: int is not a subtype of long
    // public int getValue() { return 42; }
}

Tip: Always add @Override when using covariant return types. It catches typos (wrong method name, wrong parameter count) at compile time rather than silently creating a new overloaded method.

Covariant Return Types with Interfaces

The same rule applies when a class implements an interface or when one interface extends another.

interface Shape {
    Shape copy();
}

class Circle implements Shape {
    private double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public Circle copy() {             // covariant: Circle narrows Shape
        return new Circle(this.radius);
    }

    @Override
    public String toString() {
        return "Circle(r=" + radius + ")";
    }
}

public class Main {
    public static void main(String[] args) {
        Circle c1 = new Circle(5.0);
        Circle c2 = c1.copy();         // no cast!
        System.out.println(c2);
    }
}

Output:

Circle(r=5.0)

Under the Hood

At the bytecode level, the JVM does not natively support covariant return types in the way the Java language does. The compiler handles this by generating a bridge method — a synthetic method with the original (wider) return type signature that delegates to the actual overriding method.

You can inspect this with the javap -p -c Dog.class command (see javap Tool). For the Dog.create() example, the compiler emits two methods in Dog.class:

  • public Dog create() — the real implementation you wrote
  • public synthetic bridge Animal create() — a generated bridge that calls Dog.create() and returns its result as Animal

The bridge method exists so that old bytecode compiled against the Animal type contract continues to work. Callers using the Animal reference call the bridge; callers using the Dog reference call the real method directly.

Note: This is the same bridge-method mechanism used by Generics for type erasure. The JVM itself operates on raw types; the compiler inserts bridges to preserve type safety.

This means covariant return types carry zero runtime overhead for callers that use the concrete (narrower) type — they go straight to the real method. Callers using the parent reference pay one extra virtual dispatch through the bridge, which is negligible.

Quick Comparison: With and Without Covariant Return Types

// WITHOUT covariant return types (manual cast approach)
class Repository {
    public Object findById(int id) { return new Object(); }
}
class UserRepository extends Repository {
    @Override
    public Object findById(int id) { return new User(id); }
}
// Caller:
User u = (User) new UserRepository().findById(1);  // cast required, risky

// WITH covariant return types
class UserRepository2 extends Repository {
    @Override
    public User findById(int id) { return new User(id); }
}
// Caller:
User u2 = new UserRepository2().findById(1);       // clean, safe

The covariant version is self-documenting: the signature itself tells you a User comes back, not just some Object.

  • Method Overriding — the foundation that covariant return types build on
  • Runtime Polymorphism — how the JVM dispatches to the correct overriding method at runtime
  • super Keyword — calling the parent version of an overridden method from within the override
  • Overloading vs Overriding — clear comparison of both techniques and when each applies
  • Generics — uses the same bridge-method mechanism under the hood for type erasure
  • javap Tool — inspect generated bridge methods in compiled bytecode yourself
Last updated June 13, 2026
Was this helpful?