Type dependency in Java, Part 1

Covariance and contravariance for array types, generic types, and the wildcard element

Type dependency and variances in Java
chrstphre (CC BY 2.0)

Understanding type compatibility is fundamental to writing good Java programs, but the interplay of variances between Java language elements can seem highly academic to the uninitiated. This article is for software developers ready to tackle the challenge! Part 1 reveals the covariant and contravariant relationships between simpler elements such as array types and generic types, as well as the special Java language element, the wildcard. Part 2 explores type dependency and variance in common API examples and in lambda expressions.

download
Get the source code for this article, "Type dependency in Java, Part 1." Created for JavaWorld by Dr. Andreas Solymosi.

Concepts and terminology

Before we get into the relationships of covariance and contravariance among various Java language elements, let's be sure that we have a shared conceptual framework.

Compatibility

In object-oriented programming, compatibility refers to a directed relation between types, as shown in Figure 1.

variances fig1 Andreas Solymosi

Figure 1. Type compatibility

We say that two types are compatible in Java if it's possible to transfer data between variables of the types. Data transfer is possible if the compiler accepts it, and is done through assignment or parameter passing. As an example, short is compatible to int because the assignment intVariable = shortVariable; is possible. But boolean is not compatible to int because the assignment intVariable = booleanVariable; is not possible; the compiler won't accept it.

Because compatibility is a directed relation, sometimes T1 is compatible to T2 but T2 is not compatible to T1, or not in the same way. We'll see this further when we get to discussing explicit or implicit compatibility.

What matters is that compatibility among reference types is possible only within a type hierarchy. All class types are compatible to Object, for example, because all classes inherit implicitly from Object. Integer is not compatible to Float, however, because Float is not a superclass of Integer. Integer is compatible to Number, because Number is an (abstract) superclass of Integer. Because they are located in the same type hierarchy, the compiler accepts the assignment numberReference = integerReference;.

We talk about implicit or explicit compatibility, depending on whether compatibility has to be marked explicitly or not. For example, short is implicitly compatible to int (as shown above) but not vice versa: the assignment shortVariable = intVariable; is not possible. However, short is explicitly compatible to int, because the assignment shortVariable = (short)intVariable; is possible. Here we must mark compatibility by casting, also known as type conversion.

Similarly, among reference types: integerReference = numberReference; is not acceptable, only integerReference = (Integer) numberReference; would be accepted. Therefore, Integer is implicitly compatible to Number but Number is only explicitly compatible to Integer.

Dependency

A type might depend on other types. For example, the array type int[] depends on the primitive type int. Similarly, the generic type ArrayList<Customer> is dependent on the type Customer. Methods can also be type dependent, depending on the types of their parameters. For example, the method void increment(Integer i); depends on the type Integer. Some methods (like some generic types) depend on more than one types--such as methods having more than one parameter.

Covariance and contravariance

Covariance and contravariance determine compatibility based on types. In either case, variance is a directed relation. Covariance can be translated as "different in the same direction," or with-different, whereas contravariance means "different in the opposite direction," or against-different. Covariant and contravariant types are not the same, but there is a correlation between them. The names imply the direction of the correlation.

So, covariance means that the compatibility of two types implies the compatibility of the types dependent on them. Given type compatibility, one assumes that dependent types are covariant, as shown in Figure 2.

variances fig2 Andreas Solymosi

Figure 2. Covariance

The compatibility of T1 to T2 implies the compatibility of A(T1) to A(T2). The dependent type A(T) is called covariant; or more precisely, A(T1) is covariant to A(T2).

For another example: because the assignment numberArray = integerArray; is possible (in Java, at least), the array types Integer[] and Number[] are covariant. So, we can say that Integer[] is implicitly covariant to Number[]. And while the opposite is not true--the assignment integerArray = numberArray; is not possible--the assignment with type casting (integerArray = (Integer[])numberArray;) is possible; therefore, we say, Number[] is explicitly covariant to Integer[] .

To summarize: Integer is implicitly compatible to Number, therefore Integer[] is implicitly covariant to Number[], and Number[] is explicitly covariant to Integer[] . Figure 3 illustrates.

variances fig3 Andreas Solymosi

Figure 3. Contravariance

Generally speaking, we can say that array types are covariant in Java. We'll look at examples of covariance among generic types later in the article.

Contravariance

Like covariance, contravariance is a directed relationship. While covariance means with-different, contravariance means against-different. As I previously mentioned, the names express the direction of the correlation. It is also important to note that variance is not an attribute of types generally, but only of dependent types (such as arrays and generic types, and also of methods , which I'll discuss in Part 2).

A dependent type such as A(T) is called contravariant if the compatibility of T1 to T2 implies the compatibility of A(T2) to A(T1). Figure 4 illustrates.

variances fig4 Andreas Solymosi

Figure 4. Covariance and contravariance

A language element (type or method) A(T) depending on T is covariant if the compatibility of T1 to T2 implies the compatibility of A(T1) to A(T2). If the compatibility of T1 to T2 implies the compatibility of A(T2) to A(T1), then the type A(T) is contravariant. If the compatibility of T1 between T2 does not imply any compatibility between A(T1) and A(T2), then A(T) is invariant.

Array types in Java are not implicitly contravariant, but they can be explicitly contravariant , just like generic types. I'll offer some examples later in the article.

Type-dependent elements: Methods and types

In Java, methods, array types, and generic (parametrized) types are the type-dependent elements. Methods are dependent on the types of their parameters. An array type, T[], is dependent on the types of its elements, T. A generic type G<T> is dependent on its type parameter, T. Figure 5 illustrates.

variances fig5 Andreas Solymosi

Figure 5. Dependent Java language elements

Mostly this article focuses on type compatibility, though I will touch on compatibility among methods toward the end of Part 2.

Implicit and explicit type compatibility

Earlier, you saw the type T1 being implicitly (or explicitly) compatible to T2. This is only true if the assignment of a variable of type T1 to a variable of type T2 is allowed without (or with) tagging. Type casting is the most frequent way to tag explicit compatibility:


variableOfTypeT2 = variableOfTypeT1; // implicit compatible
variableOfTypeT2 = (T2)variableOfTypeT1; // explicit compatible

For example, int is implicitly compatible to long and explicitly compatible to short:


int intVariable = 5;
long longVariable = intVariable; // implicit compatible
short shortVariable = (short)intVariable; // explicit compatible

Implicit and explicit compatibility exists not only in assignments, but also in passing parameters from a method call to a method definition and back. Together with input parameters, this means also passing a function result, which you would do as an output parameter.

Note that boolean isn't compatible to any other type, nor can a primitive and a reference type ever be compatible.

A (reference) subtype is implicitly compatible to its supertype, and a supertype is explicitly compatible to its subtype. This means that reference types are compatible only within their hierarchy branch--upward implicitly and downward explicitly:


referenceOfSuperType = referenceOfSubType; // implicit compatible
referenceOfSubType = (SubType)referenceOfSuperType; // explicit compatible

The Java compiler typically allows implicit compatibility for an assignment only if there is no danger of losing information at runtime between the different types. (Note, however, that this rule isn't valid for losing precision, such as in an assignment from int to float.) For example, int is implicitly compatible to long because a long variable holds every int value. In contrast, a short variable does not hold any int values; thus, only explicit compatibility is allowed between these elements.

variances fig6 Andreas Solymosi

Figure 6. Implicit compatibility of arithmetic types in Java

Note that the implicit compatibility in Figure 6 assumes the relationship is transitive: short is compatible to long.

Similar to what you see in Figure 6, it's always possible to assign a reference of a subtype int a reference of a supertype. Keep in mind that the same assignment in the other direction could throw a ClassCastException, however, so the Java compiler allows it only with type casting.

Covariance and contravariance for array types

In Java, some array types are covariant and/or contravariant. In the case of covariance, this means that if T is compatible to U, then T[] is also compatible to U[]. In the case of contravariance, it means that U[] is compatible to T[]. Arrays of primitive types are invariant in Java:


longArray = intArray; // type error
shortArray = (short[])intArray; // type error

Arrays of reference types are implicitly covariant and explicitly contravariant, however:


SuperType[] superArray;
SubType[] subArray;
	...
superArray = subArray; // implicit covariant
subArray = (SubType[])superArray; // explicit contravariant

variances fig7 Andreas Solymosi

Figure 7. Implicit covariance for arrays

Figure 7. Implicit covariance for arrays

What this means, practically, is that an assignment of array components could throw ArrayStoreException at runtime. If an array reference of SuperType references an array object of SubType, and one of its component is then assigned to a SuperType object, then:


superArray[1] = new SuperType(); // throws ArrayStoreException

This is sometimes called the covariance problem. The true problem is not so much the exception (which could be avoided with programming discipline), but that the virtual machine must check every assignment in an array element at runtime. This puts Java at an efficiency disadvantage against languages without covariance (where a compatible assignment for array references is prohibited) or languages like Scala, where covariance can be switched off.

An example for covariance

In a simple example, the array reference is of type Object[] but the array object and the elements are of different classes:


Object[] objectArray; // array reference
objectArray = new String[3]; // array object; compatible assignment
objectArray[0] = new Integer(5); // throws ArrayStoreException

Because of covariance, the compiler cannot check the correctness of the last assignment to the array elements--the JVM does this, and at significant expense. However, the compiler can optimize the expense away, if there is no use of type compatibility between array types.

variances fig8 Andreas Solymosi

Figure 8. The covariance problem for arrays

Remember that in Java, for a reference variable of some type referring an object of its supertype is forbidden: arrows in Figure 8 must not be directed upwards.

Variances and wildcards in generic types

Generic (parametrized) types are implicitly invariant in Java, meaning that different instantiations of a generic type are not compatible among each other. Even type casting will not result in compatibility:


Generic<SuperType> superGeneric;
Generic<SubType> subGeneric;
subGeneric = (Generic<SubType>)superGeneric; // type error
superGeneric = (Generic<SuperType>)subGeneric; // type error

The type errors arise even though subGeneric.getClass() == superGeneric.getClass(). The problem is that the method getClass() determines the raw type--this is why a type parameter does not belong to the signature of a method. Thus, the two method declarations


void method(Generic<SuperType> p); 
void method(Generic<SubType> p); 

must not occur together in an interface (or abstract class) definition.

1 2 Page 1