Skip to main content

Variance concepts in the context of parametric programming with Java

Variance concepts in the context of parametric programming with Java

By Obed Rios (5/7/2023)

Revision 1.0

Abstract

In Java, the concepts of variance are related to how the type parameters of a class or interface are related to each other when the class or interface is sub-typed or implemented. The key difference between invariant and covariant in the context of Java generics is how they handle sub-typing relationships. Invariant types do not allow assignments between different type parameters, while covariant types can accept a specified type or any of its sub-types. In addition contravariance enables you to use a more general type (super type) in a generic type or method that would normally require a more specific type (sub-type). In this work, we show explicitly the concepts of variance in the context of Java Generics.

Introduction

Parametric variance refers to the relationship between the type parameters of a class or interface and their subtypes. It defines how subtyping is inherited by the type parameters of a generic class or interface. There are three types of parametric variance: invariant, covariant, and contravariantbloch2017oracle2023.

  1. Invariant: If a class or interface has invariant type parameters, then the subtyping relationship between the class or interface and its subtype is not inherited by the type parameters. In other words, if a class or interface Foo<T> is a subtype of another class or interface Bar<T>, then Foo<A> is not a subtype of Bar<B> for any types A and B that are not identical.

  2. Covariant: If a class or interface has covariant type parameters, then the subtyping relationship between the class or interface and its subtype is inherited by the type parameters. In other words, if a class or interface Foo<+T> is a subtype of another class or interface Bar<+T>, then Foo<A> is also a subtype of Bar<A> for any type A.

  3. Contravariant: If a class or interface has contravariant type parameters, then the subtyping relationship between the class or interface and its subtype is reversed for the type parameters. In other words, if a class or interface Foo<-T> is a subtype of another class or interface Bar<-T>, then Bar<A> is a subtype of Foo<A> for any type A.

Invariance and Covariance Worked Example

In this example, we create two lists, intList and doubleList, which are of type List<Integer> and List<Double>, respectively. Due to the invariant nature of the generic List<E> class, we cannot directly assign intList or doubleList to a variable of type List<Number>.

The printSum() method takes a List<? extends Number> parameter, which uses a wildcard to allow any subtype of Number as its argument. This allows us to pass both intList and doubleList to the method without issues.

In this case, the printList() method accepts a parameter of type List<? extends Number>, which allows any sub-type of Number. This makes the method covariant with respect to its input parameter, allowing it to process List<Integer>, List<Double>, and List<Number> without issues.

Here's a comparison of invariant and covariant using the List<Number> class:

/**
* Invariant type means that the type parameter remains unchanged. In the context of Java generics, it means that a generic type with a specified type parameter cannot be treated as a generic type with a different type parameter, even if one type is a subtype of another.
*
* Covariant type means that the type parameter can vary in a way that follows the class hierarchy.In Java generics, covariance can be achieved using the ? extends T wildcard, which allows a generic type to accept the specified type or any of its subtypes.
*/

import java.util.ArrayList;
import java.util.List;

public class InvarianceCovarianceExample {

   public static void main(String[] args) {
       List<Integer> intList = new ArrayList<>();
       intList.add(1);
       intList.add(2);
       intList.add(3);

       List<Double> doubleList = new ArrayList<>();
       doubleList.add(1.1);
       doubleList.add(2.2);
       doubleList.add(3.3);

       // The following lines would cause compile-time errors due to type invariance.
       // List<Number> numberList1 = intList;
       // List<Number> numberList2 = doubleList;

       printSum(intList); // Valid, prints "Sum: 6.0"
       printSum(doubleList); // Valid, prints "Sum: 6.6"

       // But we cannot pass a List<Number> to printSum() method
       List<Number> numberList = new ArrayList<>();
       numberList.add(1);
       numberList.add(2.0);
       numberList.add(3.3);

       printSum(numberList);
  }

   public static void printSum(List<? extends Number> listNumbers) {
       double sum = 0;
       for (Number number : listNumbers) {
           sum += number.doubleValue();
      }
       System.out.println("Sum: " + sum);
  }

   public static void printList(List<? extends Number> numbers) {
       for (Number number : numbers) {
           System.out.print(number + " ");
      }
       System.out.println();
  }

}

Contravariance Worked Example

In this example, we create a custom Consumer<List<? super Number>> type, which uses a wildcard with a super clause (? super T) to achieve contravariance. This allows the generic type to accept the specified type or any of its super-types.

We then create a numberConsumer that takes a List<? super Number> as an argument and processes the list. The processList() method takes a List<?> and a Consumer<List<? super Number>> and applies the consumer to the list. As a result, we can pass both numberList and objectList to the processList() method without any issues, demonstrating contravariance using a custom functional interface in Java

Here's an example of contravariance using a custom functional interface:

/**
* Contravariance is not directly supported for the List<T> type in Java because List<T> only provides methods that are either invariant or covariant with respect to its type ap arameter. However, we can demonstrate contravariance using a
custom generic functional interface with a single method that takes a List<Number> or any of its supertypes as an argument.
*/

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

public class ContraVarianceExample {
   public static void main(String[] args) {
       List<Number> numberList = new ArrayList<>();
       numberList.add(1);
       numberList.add(2.0);
       numberList.add(3.3);

       List<Object> objectList = new ArrayList<>();
       objectList.add("Hello");
       objectList.add(42);
       objectList.add(3.14);

       Consumer<List<? super Number>> numberConsumer = list -> {
           for (Object item : list) {
               System.out.println("Item: " + item);
          }
      };

       processList(numberList, numberConsumer); // Valid
       processList(objectList, numberConsumer); // Valid
  }

   public static void processList(List<?> list, Consumer<List<? super Number>> listProcessor) {
       listProcessor.accept((List<? super Number>) list);
  }
}

Conclusion

Variance in the context of Java generics allows for more flexible and reusable code by controlling how generic types relate to their type parameters and subtyping relationships. Java supports three kinds of variance through wildcards:

  1. Invariance: By default, Java generics are invariant. This means that a generic type with a specified type parameter cannot be treated as a generic type with a different type parameter, even if one type is a subtype of another. Invariance provides type safety but can be restrictive in some scenarios.

  2. Covariance: Achieved using the ? extends T wildcard, covariance allows a generic type to accept the specified type or any of its subtypes. This increases the flexibility of generic types and methods, enabling the use of a more specific type (a subtype) in place of a more general type (a super type).

  3. Contravariance: Achieved using the ? super T wildcard, contravariance allows a generic type to accept the specified type or any of its super types. This further increases the flexibility of generic types and methods, enabling the use of a more general type (a super type) in place of a more specific type (a subtype).

Understanding and properly using variance in Java generics can lead to cleaner, more adaptable, and more robust code. It's essential to choose the right kind of variance for a particular use case to ensure that your code is flexible and type-safe at the same time.

References

Comments

Popular posts from this blog

Solid Principles in Object Oriented Programming

SOLID Principles in Object Oriented Programming Solid Principles in Object Oriented Programming Introduction SOLID is an acronym that represents five principles of object-oriented programming and design, aimed at making software systems more maintainable and scalable. The SOLID principles are: Single Responsibility Principle (SRP) - A class should have only one reason to change, meaning that a class should have only one responsibility. Open-Closed Principle (OCP) - Software entities should be open for extension but closed for modification, meaning that a class should be easily extendable without modifying its existing code. Liskov Substitution Principle (LSP) - Subtypes should be substitutable for their base types, meaning that objects of a derived class should be able to replace objects of the base class without affecting the correctness of the program. Interface Segregation Principle (ISP) - Clients should not be forced to depend on interfaces they do not use, meaning tha...

Linear Least Squares Methods with R: An Algebraic Approach - Part I

Linear Least-Squares Method with R - Part I Linear Least Squares Methods with R: An Algebraic Approach - Part I Algebraic Approach Principles The least-squares method is one of the most well-known linear optimization methods because of its flexibility. Furthermore, it gives a reasonable approximation of a given function. Among the diverse applications that it can be used for is Regression in statistical learning, Direct Linear Transformation methods in projective geometry, and so on. We will demonstrate the principles of least squares methods and implement examples in R in this article. Consider the above figure of a simple linear system where the x variable is an input variable, A is a measurements matrix, and y is the output variable. That is, the model for the equation is given by (1) y = A x , which in a more explicit notation from (1) can be expressed as (2) [ y 1 y 2 ⋮ y i ] = [ a 1 , 1 a 1 , 2 ⋯ a 1 , i a 2 , 1 a 2 , 2 ⋯ a 2 , i ⋮ ⋮ ⋯ ⋮ a j , 1 a j , 2 ⋯ a j , i ] [...