When I find something interesting and new, I post it here - that's mostly programming, of course, not everything.

Monday, October 20, 2008

Experimenting with Covariance and Contravariance in Java Generics

Here I'll just publish the code that demonstrates what is accepted and what is not by the compiler when you use generics. Not many comments, since most of the code is extremely trivial.

Oh, and definitions. An expression is contravariant in variable x of class X if x can be substituted with an instance y of class Y which is a subclass of X. An expression is covariant in variable x of class X if x can be substituted with an instance y of class Y which is a superclass of class X.

E.g. (a = f(b)) is contravariant in b and covariant in a.

Let's have the following class inheritance diagram ('=>' means iheritance):

E => C, D => C, C => B, B => A

In details,

class A {
String id;

A(String id) { this.id = id; }
}

class B extends A {
B(String id) { super(id); }
}

class C extends B {
C(String id) { super(id); }
}

class D extends C {
D(String id) { super(id); }
}

class E extends C {
E(String id) { super(id); }
}


And let's have an interface and a class, something similar to Map and HashMap:

interface M<X, Y> {
void put(X x, Y y);
Y get(X x);
}

class M1<X, Y> implements M<X, Y> {
M1() {};
public void put(X x, Y y){};
public Y get(X x){ return null; }
}


The following code demonstrates a ubiquitous and pretty legal usage of covariance and contravariance.

First, the example where we vary the second parameter ("the value"), and provide an exact knowledge of its type on declaration (and instantiation):

void testParameter2() {
M<String, B> mSB = new M1<String, B>();
mSB.put("", new B(""));
mSB.put("", new C(""));
B aa = mSB.get("");
M<String, C> mSC = new M1<String, C>();
mSC.put("", new C(""));
B b = mSC.get("");
C c = mSC.get("");
}


Now let's try a contravariant version of wildcard in the second parameter of M. Turns out that using wildcard duly blocks some of the "expected" functionality.

void testParameter2WithExtendsWildcard() {
M<String, ? extends C> mSCx = new M1<String, D>();
mSCx = new M1<String, E>();
// Actually, only E should be accepted, but this information is lost on assignment.
// So the next line won't compile:
mSCx.put("", new C(""));
// put(java.lang.String,capture#660 of ? extends common.VarianceTest.C) in
// common.VarianceTest.M<java.lang.String,capture#660 of ? extends common.VarianceTest.C>
// cannot be applied to (java.lang.String,common.VarianceTest.C)

//
// And we don't know if it is D (it is not).
// So the next line won't compile:
mSCx.put("", new D(""));
// put(java.lang.String,capture#511 of ? extends common.VarianceTest.C)
// in common.VarianceTest.M
// cannot be applied to (java.lang.String,common.VarianceTest.D)

B b = mSCx.get("");
C c = mSCx.get("");
}


Trying the same, but 'super' instead of 'extends':


void testParameter2WithSuperWildcard() {
M<String, ? super C> mSCs = new M1<String, C>();
mSCs = new M1<String, B>();
// We don't know what's hiding behind 'super', maybe it's Object
// So the next line won't compile:
mSCs.put("", new B(""));
// put(capture#701 of ? extends common.VarianceTest.C,java.lang.String)
// in common.VarianceTest.M
// cannot be applied to (common.VarianceTest.C,java.lang.String)

mSCs.put("", new C(""));
mSCs.put("", new D(""));
// We don't know what's hiding behind 'super', maybe it's Object
// So the next line won't compile:
B b = mSCs.get("");
// incompatible types found
// : capture#222 of ? super common.VarianceTest.C required: common.VarianceTest.B

Object any = mSCs.get("");
}


The same with parameter 1. No surprises here:

void testParameter1() {
M<B, String> mBS = new M1<B, String>();
mBS.put(new B(""), "");
mBS.put(new C(""), "");
String s = mBS.get(new B(""));
s = mBS.get(new B(""));
M<C, String> mCS = new M1<C, String>();
mCS.put(new C(""), "");
mCS.put(new D(""), "");
s = mCS.get(new C(""));
s = mCS.get(new D(""));
}


The following example shows that it is totally useless to specify 'extends' wildcard in contravariant position:

void testParameter1WithExtendsWildcard() {
String s;
M<? extends C, String> mCxS = new M1<C, String>();
mCxS = new M1<D, String>();
// Who knows what is the actual type of param1...
// The following two lines do not compile:
mCxS.put(new C("");
// put(capture#701 of ? extends common.VarianceTest.C,java.lang.String)
// in common.VarianceTest.M
// cannot be applied to (common.VarianceTest.C,java.lang.String)

mCxS.put(new D(""), "");
// put(capture#701 of ? extends common.VarianceTest.C,java.lang.String)
// in common.VarianceTest.M
// cannot be applied to (common.VarianceTest.D,java.lang.String)


// Same here, parameter type unknown
// The following two lines do not compile either:
s = mCxS.get(new C(""));
// get(capture#897 of ? extends common.VarianceTest.C) in common.VarianceTest.M // of ? extends common.VarianceTest.C,java.lang.String>
// cannot be applied to (common.VarianceTest.C)

s = mCxS.get(new D(""));
// get(capture#897 of ? extends common.VarianceTest.C) in common.VarianceTest.M // of ? extends common.VarianceTest.C,java.lang.String>
// cannot be applied to (common.VarianceTest.D)


}


Now let's try the same with 'super' instead of 'extends'. This means that an instance of any superclass instance can show up (down to Object), we just have no way to know which one it is:


void testParameter1WithSuperWildcard() {
M<? super C, String> mCsS = new M1<C, String>();
mCsS = new M1<B, String>();
// Who knows what should be the actual type of param1...
// The following line does not compile:
mCsS.put(new B(""), "");
// put(capture#248 of ? super common.VarianceTest.C,java.lang.String) in
// common.VarianceTest.M
// cannot be applied to (common.VarianceTest.B,java.lang.String)

// C can be cast to any superclass of C
mCsS.put(new C(""), "");
mCsS.put(new D(""), "");
String s = mCsS.get(new C(""));
s = mCsS.get(new D(""));
s = mCsS.get(new C(""));
// Who knows what should be the actual type of param1...
// The following line does not compile:
s = mCsS.get(new B(""));
// get(capture#330 of ? super common.VarianceTest.C) in common.VarianceTest.M // of ? super common.VarianceTest.C,java.lang.String>
// cannot be applied to (common.VarianceTest.B)

}


Generics with wildcards are not broken. The lack of understanding on how they work stems from the lack of understanding of covariant and contravariant substitution. Once you grasp it, the rest is easy.

And there's something specific about wildcards. If it is ? extends A, it does not mean that anything extending A can substitute. It means that there is something that extends A, and there' no way to figure out what.

No comments:

Followers

Subscribe To My Podcast

whos.amung.us