Type dependency in Java, Part 2

Using covariance and contravariance in your Java programs

1 2 3 Page 2
Page 2 of 3

A method is dependent on the type of its parameters. Along with input parameters, we also consider the method's return type as an output parameter. However, the return type does not belong to the method's signature, only its input (raw) parameter types.

Methods with different names or with a different number of parameters are incompatible. The question of compatibility arises only for methods with the same name and the same number of parameters.

Compatibility of method declarations and definitions

A method call in the body of a class can be compatible or incompatible to method definitions (in classes) and declarations (in abstract classes and interfaces). For a method declaration, the question is the compatibility to other declarations--this information helps you decide if the method is to be overridden or overloaded.

Among Java declarations and definitions, there is no variance concerning signature: if a method overrides another method, the signatures must be same. However, there is covariance concerning result type:


interface SuperType {
	void procedure(SuperType parameter);
	SuperType function();
 }
interface SubType extends SuperType {
	void procedure(SubType parameter); // overloaded: different signature
	@Override
	SubType function(); // overridden: same signature, different result type
}

Abiding Java's strict rules regarding signatures, procedure() is in this case overloaded, not overridden. Adding the annotation @Override for SubType.procedure() would produce an error message in the compiler, because the parameter type of procedure() is not the same for the two interfaces.

Whether a call is compatible to a declaration or a definition is decided on the basis of the signature. There is covariance here: the upward compatibility of the parameters implies the compatibility of the call. If we are concerned with the target object of a call, however, we don't speak about variance but about polymorphism:


SuperType superParameter = ... ;
SubType subParameter = ... ;
superReference.procedure(subParameter); // call is covariant concerning signature
subReference.procedure(superParameter); // polymorph

Concerning signatures, calls to declarations and definitions are covariant. Note that the last ones below are invariant.

Type dependency in Java: Variance of methods
Figure 1. Variance of methods concerning signature

Variance in functions

Method declarations and definitions in Java are covariant concerning result types. In contrast to parameters, the result type of a function does not belong to its signature. The result type can be extended upward in the overridden version, although it cannot be otherwise changed.

When it comes to function results in a call, we don't look for variance but, instead, upward compatibility:


SuperType superResult = subReference.function(); // upward compatible
SubType subResult = superReference.function();
	// type error: no downward compatibility

Note, however, that the upward compatibility demonstrated in the above code could also be considered a form of covariance.

Like the function result, access protection (private, public, etc.) and the exception specification (throws) do not belong to the signature. (These could be extended upward if the method was overriden, however.) Appending final prevents further overriding, as shown:


class SuperClass {
	void method() throws SuperException { ... }
}
class SubClass extends SuperClass {
	@Override
	final void method() throws SubException { ... }
}

All in all, method variances are simple: Declarations and definitions of the method signature are invariant to each other; all other elements--result types, exception specifications, and calls to declarations and definitions--are covariant. Table 2 summarizes method variances.

Using variances in lambda expressionsLambda expressions are anonymous methods. Like all anonymous language elements (including classes and objects) they are most suited to one-time usage. Lambdas satisfy the principle of code readability, and thus of software quality. We can summarize that principle as: The closer the definition is to its usage, the better.

With a lambda expression, you define anonymous program elements as they are used; like so:


method(new MyClass()); // anonymous object without reference, for one-time usage
new Interface() { ... };
	// anonymous class implementing the interface, for one-time usage
procedure(parameter -> { ... }); // lambda
	// anonymous method passed as parameter to procedure

The code assumes the following declarations:


@FunctionalInterface
interface Functionalinterface {
	void method(Type parameter);
}
...
void procedure(Functionalinterface functionalinterface) { ... }

Before Java 8, calling procedure() would have looked more complicated:


class Implementation implements Functionalinterface {
	public void method(Type parameter) { ... } }
...
Functionalinterface fi = new Implementation();
procedure(fi);

A somewhat simpler example, implemented with an anonymous class and object:


procedure(new Functionalinterface() {
	public void method(Type parameter) { ... } } );

Prior to Java 8, a callback was implemented like so:

iType dependency in Java: Callbacks
Figure 2. Callbacks

In a traditional callback procedure, method a passes method c as parameter while calling b, so that that b calls c back. If you are programming b, you know that you will call a method at the place marked with the red circle in Figure 2, but you don't know which method you'll call.

Lambdas offer a much simpler way to implement callbacks:


procedure(parameter -> { ... }); // lambda

Simplifying calls with lambdas

If you were to program an algorithm for integrating a function, you would need to write it without knowing which function (for instance, sine) was to be integrated. Figure 3 shows an algorithm for calculating integrals of sine.

type dependency-integral1
Type dependency in Java-integral2 Wikipedia
Figure 3. Integrals of sine

The user of your integration algorithm would then pass their function (along with the limits a and b) when calling the algorithm. Below, you see the algorithm used to calculate the integral of sine between 0 and π, which is 1.0.


double result = integral((double x) -> Math.sin(x), 0, Math.PI);

In Java, a listener is a good example for a callback:


button.addActionListener( // pre-Java 8 version
	new ActionListener() {
		public void actionPerformed(ActionEvent e) {
			System.out.println("Button pushed"); }});

Say you are programming a class, Button. Your button needs to be programmed to react to a button push, but the action is undefined. Only the user of the class knows what exact action the application requires (in this case it will be System.out.println()).

To develop the generic button, you would create an ActionListener object and pass it to the Button object by calling addActionListener(). You would then implement the ActionListener method actionPerformed(), where the action is programmed. (In a more complex example you could use the ActionEvent parameter as well.) The user method will be "called back" by the Button class when the button is pushed.

Introducing lambdas simplifies this program:


button.addActionListener(
  e -> System.out.println("Button pushed") // lambda expression
);

Using the ready-to-use method reference for System simplifies even further:


button.addActionListener(System.out::println);

Because Math exports a method reference for sin, you can also use this one when calling integral():


result = integral(Math::sin, 0, Math.PI);

Then you must have declared


double integral(Function<Double, Double> function, double a, double b)

where Function is a generic interface with two type parameters.

Lambdas in functional interfaces

You can use lambda expressions after declaring a functional interface. Any interface is acceptable, so long as it declares exactly one (abstract) non-generic method. The compiler checks for these criteria if the interface has been marked by the annotation @FunctionalInterface.

The parameter type of Button.addActionListener(), namely java.awt.event.ActionListener, satisfies this criteria; therefore it can be called with a lambda expression as parameter. The lambda expression represents an anonymous method (the "value of the lambda") with one parameter, e, before the arrow ->, and a method body (a block) after it.

In Java 8, the lambda expression behaves like an object and can be passed as a parameter. The object is considered to be "of a lambda type." In the case of our example, the object is of type ActionListener of the functional interface.

You can also define a reference to a lambda expression:


ActionListener actionListener = e -> System.out.println("Button pushed");

and then use it later in the call:


button.addActionListener(actionListener);

This might be useful if you want to assign the same listener to more than one button.

Another frequent example is Runnable:


class OldRunnable implements Runnable {
  public void run() {
    System.out.println("Old");
  }
};
Runnable old = new OldRunnable();
new Thread(old).start();

This call is much simpler with a lambda expression:


new Thread(() -> System.out.println("New")).start();

A lambda expression has the type of its corresponding functional interface. In the following example, we could specify the types (String) of the two parameters, left and right, but the compiler can also determine types by inference:


Comparator<String> c;
c = (String left, String right) -> left.compareTo(right);
c = (left, right) -> left.compareTo(right); // equivalent

For this reference, we call the lambda value (a method body) as follows:


System.out.println(c.compare("Hallo", "World"));

Simplifying calls is not the only advantage of lambdas. Lambda expressions enable a new programming paradigm in Java, of mixing object-oriented programming with functional-style programming. You can write many algorithms more concisely and more readably using lambdas, and they even help improve program correctness, in many cases.

Covariance of lambda expressions

Lambdas must not be generic; therefore, they leave little room for variances. Oftentimes, it's a challenge for the compiler's inference algorithm to determine the type of a lambda expression; therefore the parameter types of the lambda expression must fit (almost) exactly to the types of the functional interface: just the usual compatibility rules for method parameters apply:


@FunctionalInterface
interface SuperFI {
	void method(SuperType parameter);
}
@FunctionalInterface
interface SubFI {
	void method(SubType parameter);
}
...
void covariantProcedure(SuperFI fi, SuperType superParameter {
	fi.method(superParameter);
}
void contravariantProcedure(SubFI sfi, SuperType subParameter) {
	sfi.method((SubType)subParameter); // run time error: ClassCastException
}
...
covariantProcedure(parameter -> {}, new SubType());
	// SubType is compatible to SuperType
contravariantProcedure(parameter -> {}, new SuperType());

The compiler accepts the unfitting SuperType object in the last row, but the casting in the body of contravariantProcedure() raises a ClassCastException at runtime. So we can say that lambda expressions (just like method calls) are covariant with respect to their signature.

Lambdas also behave like methods with respect to their result type and exception specification; in these cases they are covariant:


@FunctionalInterface
interface SuperInterface {
	SuperType function() throws SuperException;
}
@FunctionalInterface
interface SubInterface {
	SubType function() throws SubException;
}
...
SuperType superFunction(SuperInterface superParameter) throws SuperException {
	return superParameter.function(); // or do something more interesting
}
SubType subFunction(SubInterface subParameter) throws SubException {
	return subParameter.function(); // similarly
}
...
SuperType superVariable = superFunction(() -> new SuperType()); // normal
superVariable = subFunction(() -> new SubType()); // simply compatible
superVariable = superFunction(() -> new SubType()); // covariant
SubType subVariable = (SubType)superFunction(() -> new SuperType()); // error
subVariable = (SubType)superFunction(() -> new SuperType()); // run time error

The castings in the last two lines above cause a ClassCastException. Casting isn't helpful because there is no explicit contravariance. However, lambda expressions (just like methods) are implicitly covariant with respect to the types of their function result and exception specifications (see the third line in the above code).

Here, we've passed a lambda expression of SubType to a parameter of SuperType. This is a special case for lambdas: superFunction's parameter type is SuperInterface; usually the call would take a parameter of SubInterface, which is not its subtype. But lambdas are implicitly covariant, so it works.

Inference is the trick: if we define the two versions of function() with distinguishable signatures--meaning with parameters of different types--and implement the SubInterface traditionally, we must specify function()'s parameter type explicitly. But if a lambda expression has an unspecified parameter type, the compiler finds the one best fitting, and this is flexible:


superVariable = superFunction(parameter -> new SubType());
	// unspecified parameter type: inference changes SubType to SuperType

1 2 3 Page 2
Page 2 of 3