Covariant Return Types in Java with Example

In this tutorial, we will understand covariant return types in Java with the help of example programs. As we know that in the overriding, the return type of subclass method must be the same as the superclass method return type.

But, this rule is applicable until Java 1.4 version. From Java 1.5 onwards, a new covariant return type feature was introduced by Sun Microsystems. By using this feature, it is possible to override any method by changing the return type only.

If the return type of overriding method in the subclass is a subtype of the declared return type of overridden method instead of being exactly the same type, it is known as covariant return type in Java.

In simple words, if the return type of the overriding method is a subclass of the return type of the overridden method (instead of being exactly the same type), this feature is known as a covariant return type. Let’s understand it with the help of a very simple example.

class Superclass {
    Superclass getMe() {
        return this;
    }
}
class Subclass extends Superclass {
    @Override
    Subclass getMe() {
        return this;
    }
}

In this example code, the return type of getMe() method in the subclass Subclass is a subclass of the return type in the superclass Superclass. This is allowed in due to the covariant return type feature in Java programming.

Rules for Covariant Return Type in Java


There are mainly three rules for covariant return types in Java that you should keep in mind. They are as follows:

1. The return type of overriding method in the subclass should be either the same as the return type of superclass or subclass.

2. The return type of overriding method in the subclass should not be a parent of the parent method return type.

3. The covariant return type is applicable only for object types, not for primitive types.


Applicable covariant return type in Java method overriding.

Covariant Return Type Example Program


Let’s take some important example programs of covariant return types in Java.

Example 1:

package covariantReturnType; 
public class A 
{ 
  public Object m1()
  { 
     System.out.println("Hello, this is a superclass"); 
     return null; 
  } 
} 
public class B extends A
{ 
   @Override 
   public String m1() 
   { 
      System.out.println("Hello, this is the subclass"); 
      return null; 
   } 
 } 
public class AB { 
public static void main(String[] args) 
{ 
     B b = new B(); 
     b.m1(); 
     
     A a = new B(); 
     a.m1(); 
  } 
}
Output: 
       Hello, this is the subclass 
       Hello, this is the subclass

Explanation:

In the above example program, superclass declares m1() method of Object return type and B class overrides the m1() method by changing the return type from Object to String.


The return type of the overriding method m1() in the subclass is a string that is the subclass of an Object class.

1. The statement b.m1(); will call m1() method of class B because the reference variable is pointing to the objects of class B. As you learned from the previous tutorial that in overriding, method resolution always takes care by JVM based on the runtime object.

2. Similarly, the statement a.m1(); will also call m1() method of class B because it is also pointing to the object of class B.

Example 2:

package covariantReturnType; 
public class A 
{ 
   public String m1()
   { 
      System.out.println("Hello, this is a superclass"); 
      return null; 
   } 
} 
public class B extends A
{ 
   @Override 
   public Object m1() 
   { 
      System.out.println("Hello, this is the subclass"); 
      return null; 
   } 
} 
public class AB { 
public static void main(String[] args) 
{ 
     A a = new B(); 
     a.m1(); 
  } 
}
Output: 
       Hello, this is a superclass

In this example, the return type of overriding method is incompatible with A.m1() because the return type of overriding method in the subclass should not be parent. Here, Object is the parent class of String. Therefore, it is not an overridden method. Because of not being overriding in the subclass, a.m1() will call by default available m1() method in class B.

Example 3:

package covariantReturnType; 
public class A 
{ 
   public Number m1(int a, double b)
   { 
      System.out.println("Hello, this is a superclass"); 
      return null; 
   } 
} 
public class B extends A
{ 
    @Override 
    public Integer m1(int a, double b)
    { 
       System.out.println("Hello, this is the subclass"); 
       return null; 
    } 
} 
public class AB { 
public static void main(String[] args) 
{ 
     A a = new B(); 
     a.m1(10,20.5); 
  } 
}
Output: 
        Hello, this is the subclass

In this example code, we have defined three classes A, B, and AB within the covariantReturnType package. The class A is the superclass in which we have defined m1() method of return type Number. This method takes an int and a double as parameters.

The class B is the subclass of class A in which we have overridden m1() method with the same method signature, but its return type is an Integer instead of a Number.

The class AB contains the main method, which is the entry point of the program. Inside the main method, we have created an object of class B and assigned it to a variable “a” of type A. Then, we have called m1() method using reference variable “a” by passing arguments 10 and 20.5.

Since Java uses dynamic method binding to determine the appropriate method call at runtime based on the actual object type. Therefore, the overridden m1 method of subclass B will be invoked.

In addition, this example code exhibits the concept of covariant return types, where the overridden method in the subclass can return a more specific type than the method in the superclass.

Example 4:

package covariantReturnType; 
public class X 
{ 
   public Object m1(char c)
   { 
      System.out.println("m1-X"); 
      return new X(); 
   } 
} 
public class Y extends X
{ 
   @Override 
   public StringBuffer m1(char c)
   { 
      System.out.println("m1-Y"); 
      return null; 
   } 
} 
public class XY { 
public static void main(String[] args) 
{ 
    X x = new Y(); 
    x.m1('a'); 

    X x1 = new X(); 
    x1.m1('b'); 
  } 
}
Output: 
       m1-Y 
       m1-X

Example 5:

package covariantReturnType; 
public class X 
{ 
   public double m1(char c)
   { 
      System.out.println("m1-X"); 
      return 25; 
   } 
} 
public class Y extends X
{ 
   @Override 
   public Integer m1(char c) 
   { 
      System.out.println("m1-Y"); 
      return 20; 
   } 
} 
public class XY { 
public static void main(String[] args) 
{ 
     X x = new Y(); 
      x.m1('a'); 
     
     X x1 = new X(); 
      x1.m1('b'); 
  } 
}
Output: 
       m1-X 
       m1-X

In this example code, we will get an error in the line public Integer m1(char c) because the return type is incompatible with X.m1(char).

Covariant return type concept is applicable only for an object reference type, not for primitive types because there is no subtype relationship between primitive types.

Because of not being overriding in the subclass, x.m1(‘a’); will call by default available m1() method in class Y. Similarly, x1.m1(‘b’); will call m1() method of class A because the reference variable x1 is pointing to the objects of class A.

Now, suppose if we declare also double as the return type of the overriding method in the subclass as in the superclass, then there will not be a compile-time error. So, in this case, the method will be overriding. The result will be “m1-Y” and “m1-X”.


Key Points:

1. Java SE5 adds a new feature covariant return type, which means we can override any method by changing the return type if the return type of subclass overriding method is subclass type.

2. Covariant return type also helps to minimize upcasting and downcasting in Java.

Common Mistakes on Covariant Return Type in Java


Here, we have explained some common mistakes that beginners do when using covariant return type in Java programs.

1. Incorrect Return Type in Overridden Method:

Mistake: Sometimes, beginners incorrectly change the return type to something that is not a subclass of the original return type, which leads to a compile-time error.

Example 6:

class Animal {
    Animal getAnimal() {
        return new Animal();
    }
}

class Dog extends Animal {
    @Override
    Dog getAnimal() { // Correct: Dog is a subclass of Animal
        return new Dog();
    }
}

class Cat extends Animal {
    @Override
    String getAnimal() { // Error: String is not a subclass of Animal
        return "Cat";
    }
}

2. Ignoring Inheritance Hierarchy:

Mistake: Beginners sometimes forget that the return type must follow the inheritance hierarchy, otherwise, it may cause confusion and potential runtime issues.

Example 7:

class Animal {
    Animal getAnimal() {
        return new Animal();
    }
}

class Dog extends Animal {
    @Override
    Dog getAnimal() {
        return new Dog();
    }
}

class GoldenRetriever extends Dog {
    @Override
    Dog getAnimal() { // Correct: GoldenRetriever can also return Dog
        return new GoldenRetriever();
    }
}

class Poodle extends Dog {
    @Override
    Animal getAnimal() { // Error: Animal is not a subclass of Dog
        return new Poodle();
    }
}