Friday, July 03, 2009

Generic types are not required for covariance

Java 5.0 introduced Generics. It also introduced covariant return types. Wikipedia does a fine job describing covariant return types.

Since they were released simultaneously, I consider them to be tightly coupled. For instance, here are simplified versions of an interface and implementation I recently wrote:

Note: I am having difficulty representing greater-than and less-than symbols in Blogger's editor, so you'll have to do with { and }.

Version 1: Java 5, Generics, Covariant return types
public interface Model{T extends Model{T}} {
  T read(InputStream in);
  T write(OutputStream out);
}
public class MyModel implements Model{MyModel} {
  public MyModel read(InputStream in) {
    ...
  }

  public MyModel write(OutputStream out) {
    ...
  }

 
public MyModel setName(String name) {
    ...
    return this;
  }
  public String getName() { ... }

}
Thanks to the covariance, I can write a method chain like this:
new MyModel()
   .read(in)
   .setName("foo")
   .setStopAtMain(false)
   ...
   .write(out);

With Java 1.4, the code would have to look like this

Version 2: Java 1.4
public interface Model {
  Model read(InputStream in);
  Model write(OutputStream out);
}

class MyModel implements Model {
  public Model read(InputStream in) { ... }
  public Model write(OutputStream out) { ... }
 
...
}
And the method chain would result in a syntax error:
public static void foo() {
  new MyModel()
      .read(in)
      .setName("foo")
     
^ The method setName(String) is undefined
        for the type Model.
      .write(out);
}
Which you could hack around with an ugly cast:
public static void foo() {
  ((MyModel) new MyModel()
      .read(in))
      .setName("foo")
     
.write(out);
 }
Back to the Java 5 example: My point is just this: covariant return types don't require generics. All that messy code in version 1 could look much simpler because covariant return types exist on their own without generics:

Version 3: Java 5, Covariant return types
public interface Model{T extends Model{T}} {
  T Model read(InputStream in);
  T Model write(OutputStream out);
}
public class MyModel implements Model{MyModel} {
  public MyModel read(InputStream in) {
    ...
  }

  public MyModel write(OutputStream out) {
    ...
  }

 
public MyModel setName(String name) {
    ...
    return this;
  }
  public String getName() { ... }

}
Lesson learned: I know generics fairly well, but there's a difference between knowing when it's useful and when it isn't. Said another way: when you have a Generic hammer everything looks like a generic nail.

Thanks to David Plass for pointing this out.

8 comments:

Eric Rizzo said...

Oh, how i wish that every Java programmer were required to study this lesson before being given their license to use generics. It's a bit crude, but I've often used this joke to illustrate my feelings about generics:
Q: Why does a dog lick his genitals?
A: Because he can.

That is what I think of when I see the rampant abuse of this language feature - so many programmers are licking themselves just because they can.
I've blogged about it in the past, with references to some "big thinkers" who agree, here and here.

David Plass said...

Actually, Rob, Version 1 isn't covariant return types; it's plain old generics. Because the T gets "replaced" by MyModel (or something like that. Maybe I should ask Kevin to tell me a story.) If you had another class (public class DavidsModel extends MyModel) and then the read method returns DavidsModel, then *that* would be covariant return values.

But thanks for the shout-out.

konberg said...

Version one still is covariant return types, it's just masked by the generics. In this example, T read(in) is compiled to

Model read(in)
Model write(out)

as method signatures. Which is the mistake I made.

Now I'm not a language expert, so someone may come along and explain why proper covariance isn't really in Java, or that it's not covariance when an interface is in the equation. Who knows?

David Plass said...

Yeah, Rob, you're right about version 1.

David Plass said...

Although... it's not quite covariant unless the different T's are subclasses of each other.

For example, you could write

public class Foo implements Model{Integer}

and it would no longer be covariant(ic?) with MyModel.

konberg said...

David, since version one has Model defined as

interface Model{T extends Model{T}}, it's impossible to write

class Foo{Integer} since Integer does not extend Model.

Unknown said...
This comment has been removed by a blog administrator.
Pawel Co Faddey said...
This comment has been removed by a blog administrator.