Open In App

Variance in Java

Last Updated : 26 Nov, 2022
Improve
Improve
Like Article
Like
Save
Share
Report

Variance refers to how subtyping between more complex types relates to subtyping between their components. “More complex types” here refers to higher-level structures like containers and functions. So, variance is about the assignment compatibility between containers and functions composed of parameters that are connected via a Type Hierarchy. It allows the safe integration of parametric and subtype polymorphism. 

In java, variance is defined at the use-site.

Types of variance: There are 4 kinds of variance which are as follows. A type constructor is of the following types:

Covariant  If it accepts subtypes but not supertypes
Contravariant If it accepts supertypes but not subtypes
Bivariant If it accepts both supertypes and subtypes
Invariant  If it accepts neither supertypes nor subtypes.

Invariance in Java: The use-site must have no open bounds on the type parameter. If A is a supertype of B, then GenericType<A> is not a Supertype of GenericType<B> and vice versa. This means these two types have no relation to each other and neither can be exchanged for the other under any circumstance.

Type 1: Invariant Containers

In java, invariants are likely the first examples of generic you’ll encounter and rate the most intuitive. The methods of the type parameter are usable as one would expect. All methods of the type parameter are accessible. They cannot be exchanged and one can read both from them which is illustrated below illustrations how.

Illustration 1: Cannot be exchanged

// Type hierarchy Person :> Joe :> JoeJr

List<Person> p = new ArrayList<>();

// Ok
p.add(new Person()); 
// Ok 
p.add(new Joe());   
// Ok
p.add(new JoeJr());    

Illustration 2: Reading objects from them

// Type hierarchy : Person :>Joe :> JoeJr

List <Joe> joes = new ArrayList<>();
// Ok
Joe j = joes.get(0);  
// Ok
Person p = joes.get(0);  

Type 2: Covariance in Java

The use-site must an open lower bound on the type parameter. If B is a subtype of A, then GenericType<B> is a subtype of GenericType<? Extends A>.

Note: Arrays in Java have always been covariant

Before generics were introduced in Java 1.5, arrays were the only generic containers available. They have always been covariant, eg. Integer[] is a subtype of Object[]. The compiler allows you to pass your Integer[] to a method that accepts Object[]. If the method inserts a supertype of Integer, an ArrayStoreException is thrown at runtime. Covariant generic type rules implement this check at compile time, disallowing the mistake to ever happen in the first place.

Example

Java




class GFG {
 
    public static void main(String args[])
    {
        Number[] numbers = new Number[] { 1, 2, 3, 4, 5 };
        trick(numbers);
    }
    private static void trick(zobject[] objects)
    {
        objects[0] = new Float(123); // ok
        objects[1] = new Objects(); // ArrayStoreException
                                    // thrown at runtime
    }
}


Output:

Now let us discuss Covariant containers. Java allows subtyping (covariant) generic types but it places restrictions on what can “flow into and out of” these generic types in accordance with the Principle of Least Astonishment. In other words, methods with return values of the type parameter are accessible, while methods with input arguments of the type parameter are inaccessible.

Illustration 1: One can exchange the supertype for the subtype:

// Type hierarchy : Person :> Joe :> JoeJr
List<? extends Joe> = new ArrayList<Joe>();  //ok
List<? extends Joe> = new ArrayList<JoeJr>();  //ok
List<? extends Joe> = new ArrayList<Person>();  // Compile error

Illustration 2: Reading from them is intuitive:

//Type hierarchy : Person :> Joe :> JoeJr
List<? extends Joe> joes = new ArrayList<>();
Joe j = joes.get(0);  //ok
Person p = joes.get(0);  //ok
JoeJr jr = joes.get(0);  // compile error

Writing to them is prohibited (counterintuitive) to guard against the pitfalls with arrays described above. Eg. in the example code below, the caller/owner of a List<Joe> would be astonished if someone else’s method with covariant arg List<? extends Person>  added a Jill.

// Type hierarchy : Person > Joe > JoeJr
List<? extends Joe> joes = new ArrayList<>();
joes.add(new Joe());  // compile error (you don't  what subtype of Joe is in the list)
joes.add(new JoeJr());  // compile error
joes.add(new Person());  //compile error
joes.add(new Object());  // compile error

Type 3: Contravariant Containers

Contravariant containers behave counterintuitively: contrary to covariant containers, access to methods with return values of the type parameter are inaccessible while methods with input arguments of the type parameter are accessible:

Illustration 1: You can exchange the subtype for the supertype:

List<> super Joe> joes = new ArrayList<Joe>();  // ok
List<? super Joe> joes = new ArrayList<Person>();  // ok
List<? super Joe> joes = new ArrayList<JoeJr>();  //Compile Error

Illustration 2: Cannot capture a specific type when reading from them:

List<? super Joe> joes = new ArrayList<>();
Joe j = joes.get(0);  // compile error
Person p = joes.get(0);  // compile error
Object o = joes.get(0);   // because everything is a object in java

Illustration 3: You can add subtypes of the “lower bound”:

List<? super Joe> Joes = new ArrayList<>();
joes.add(new JoeJr());  allowed

Illustration 4: But you cannot add supertypes:

List<? super Joe> joes = new ArrayList<>();
joes.add(new Person());  // compile error
joes.add(new Object());  // compile error

Type 4: Bivariance in Java

The use-site must declare an unbounded wildcard on the type parameter.

A generic type with an unbounded wildcard is a supertype of all bounded variations of the same generic type.Example is GenericType<?> is a supertype of GenericType<String>. Since the unbounded type is the root of the type hierarchy, it follows that of its parametric types it can only access methods inherited from java.lang.Object.

Think of GenericType<?> as GenericType<Object>.

Let us do discuss the variance of structures with N-type parameters. What about more complex types such as Functions? The same principles apply, you just have more type parameters to consider. It is illustrated in the below illustration to answer such set of dilemas.

Illustration:

Function<Person, Joe> personToJoe = null;
Function<Joe, JoeJr> joeToJoeJr = null;
personToJoe = joeToJoeJr; // compile error

// Covariance
Function<? extends Person, ? extends Joe> personToJoe
    = null;
Function<Joe, JoeJr> jorToJorJr = null;
personToJoe = joeToJoeJr; // ok

// Contravariance
Function<? super Joe, ? super JoeJr> joeToJoeJr = null;
Function<? super Person, ? super Joe> personToJoe = null;
joeToJoeJr = personToJoe; // ok

Variance and Inheritance

Illustration 1: Java allows overriding methods with covariant return types and exception types:

interface person {
    Person get();
    void fail() throws Exception;
}
interface Joe extends Person {
    JoeJr get();
    void fail() throws IOException;
}
class JoeImpl implements Joe {
    public JoeJr get() {} // overridden
    public void fail throws IOException {} // Overridden
}

Illustration 2: Attempting to override methods with covariant arguments results in merely an overload:

interface Person {
    void add(Person p);
}
interface Joe extends Person {
    void add(Joe j);
}
class JoeImpl implements Joe {
    public void add(Person p) {} // overload
    public void add(Joe j) {} // overload
}

Conclusion: Variance introduces additional complexity to Java. While the typing rules around variance are easy to understand, the rules regarding accessibility of methods of the type parameter are counterintuitive. Understanding them isn’t just “obvious”. It requires pausing to think through the logical consequences.

Variance provides moderate net benefits in my daily programming, particularly when compatibility with subtypes is required(which is a regular occurrence in OOP).



Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads