In Java, the covariant return type feature permits the return type of a method that is being overridden to be a subtype of the return type declared in the superclass. Essentially, the covariant return type indicates that the return type can change in alignment with the subclass.
Prior to Java 5, modifying the return type of a method and overriding it was not feasible. However, starting from Java 5 and onwards, it became achievable to override a method by altering its return type. This is applicable when a subclass overrides a method with a non-primitive return type and changes this return type to a subclass type.
Note that if you are new to Java, it is advisable to defer this subject and revisit it following the understanding of Object-Oriented Programming concepts.
Example of Covariant Return Type
In the provided illustration, the get method in class B1 is being overridden from class A with a covariant return type. By returning B1 instead of A in the overridden get method of B1, it becomes possible to directly invoke the message method on the returned object. This showcases the utilization of covariant return types to specify more precise return types during method overriding.
class A{
A get(){return this;}
}
class B1 extends A{
@Override
B1 get(){return this;}
void message(){System.out.println("Welcome to the covariant return type");}
public static void main(String args[]){
new B1().get().message();
}
}
Output:
Welcome to the covariant return type
Advantages of Covariant Return Type
There are following advantages of Covariant Return Type in Java.
- Covariant return type assists to stay away from the confusing type casts in the class hierarchy and makes the code more usable, readable, and maintainable.
- In the method overriding, the covariant return type provides the liberty to have more to-the-point return types.
- Covariant return type helps in preventing the run-time ClassCastExceptions on returns.
To illustrate the benefits of a covariant return type, let's consider an example:
Example
class A1
{
A1 foo()
{
return this;
}
void print()
{
System.out.println("Inside the class A1");
}
}
// A2 is the child class of A1
class A2 extends A1
{
@Override
A1 foo()
{
return this;
}
void print()
{
System.out.println("Inside the class A2");
}
}
// A3 is the child class of A2
class A3 extends A2
{
@Override
A1 foo()
{
return this;
}
@Override
void print()
{
System.out.println("Inside the class A3");
}
}
public class Main
{
// main method
public static void main(String argvs[])
{
A1 a1 = new A1();
// this is ok
a1.foo().print();
A2 a2 = new A2();
// we need to do the type casting to make it
// more clear to reader about the kind of object created
((A2)a2.foo()).print();
A3 a3 = new A3();
// doing the type casting
((A3)a3.foo()).print();
}
}
Output:
Inside the class A1
Inside the class A2
Inside the class A3
Explanation:
Within the program mentioned, class A3 extends class A2, and class A2 extends class A1, establishing A1 as the superclass of A2 and A3. Consequently, any instance of A2 or A3 is inherently an instance of A1 as well. Given that the method foo consistently returns the same type across all classes, the specific object type returned by the method remains ambiguous.
It can be inferred that the object returned will belong to type A1, which is the most general class. The exact type of the returned object, whether it is A2 or A3, cannot be definitively determined. This necessitates performing typecasting in order to ascertain the precise type of object that is returned by the foo method.
The verbosity of the code is increased, necessitating precision from the programmer to correctly handle typecasting in order to avoid the occurrence of a ClassCastException.
Imagine a scenario where the hierarchical organization extends to 10 - 15 classes or beyond, with each class containing the method foo returning the same data type. Such a situation can be daunting for both the code reader and writer.
The better way to write the above is:
Example
class A1
{
A1 foo()
{
return this;
}
void print()
{
System.out.println("Inside the class A1");
}
}
// A2 is the child class of A1
class A2 extends A1
{
@Override
A2 foo()
{
return this;
}
void print()
{
System.out.println("Inside the class A2");
}
}
// A3 is the child class of A2
class A3 extends A2
{
@Override
A3 foo()
{
return this;
}
@Override
void print()
{
System.out.println("Inside the class A3");
}
}
public class CovariantExample
{
// main method
public static void main(String argvs[])
{
A1 a1 = new A1();
a1.foo().print();
A2 a2 = new A2();
a2.foo().print();
A3 a3 = new A3();
a3.foo().print();
}
}
Output:
Inside the class A1
Inside the class A2
Inside the class A3
In the provided program, there is no requirement for typecasting since the return type is well-defined, eliminating any ambiguity about the type of object being returned by the method foo. Additionally, when coding for multiple classes (between 10 to 15), there will be clarity about the return types of the methods without any confusion. This clarity is achievable due to the presence of covariant return type.
How JVM Supports Covariant Return Types?
The Java Virtual Machine (JVM) supports overloading based on return types. When resolving methods, the JVM considers the full signature, which encompasses both the return type and the argument types. This means that a class can contain multiple methods that vary solely in their return types. The Java compiler (javac) leverages this characteristic to enable covariant return types.
Put differently, the covariant return type permits a subclass method to return a subtype of the superclass method's return type. The subclass method needs to match the superclass method's signature while being able to return a more specialized type.
Method Overriding Using Covariant Return Type in Java
When a non-primitive type is returned by a method within a class, the method in a subclass of that class can override it by using the same non-primitive type or a subclass of it as the return type.
In a scenario where a class A contains a method with a return type of an Object, it is possible for a subclass B (which is a child of class A) to override this method using a return type of Number or Integer. This is feasible because Number and Integer are considered covariant types of the Object class.
Example
Consider an example to illustrate the concept of method overriding with covariant return types in Java.
Example
class A {
Object print() {
System.out.println("print method of class A");
return new Object();
}
}
class B extends A {
Integer print() {
System.out.println("print method of class B");
return new Integer(2);
}
public static void main(String [] args) {
B b = new B();
b.print();
A a = new B();
a.print();
}
}
Output:
print method of class B
print method of class B
In the provided code snippet, it is evident that the print function within class B is replacing the print function from the superclass A. Despite the child class method having a return type of Integer, this behavior is facilitated by Java's support for covariant return types during method overriding.