Tuesday, 17 August 2010

Java generics migration compatibility

A quick call for help on generics.

Joda-Time has the following non-generic interface, with an example dummy implementation:

public interface ReadableInstant extends Comparable {
  ...
  int compareTo(Object readableInstant);
  ...
}
public class Dummy implements ReadableInstant {
  ...
  public int compareTo(Object readableInstant) { ... }
  ...
}

I've been trying to generify this and maintain backwards binary and source compatibility without success. The following naive implementation works fine for new code:

public interface ReadableInstant extends Comparable<ReadableInstant> {
  ...
  int compareTo(ReadableInstant readableInstant);
  ...
}

This doesn't work fine when the old code is combined (unaltered and without recompilation) with the new library.

Firstly, the old code is not source compatible. Dummy does not implement the altered method in the newly generified interface, thus the old code doesn't compile without change.

Furthermore, it is not binary compatible. Imagine a second class Foo that implements the generified version of ReadableInstant. Now consider a class which loads Dummy (unaltered and without recompilation) by reflection as a ReadableInstant and calls compareTo to compare. This will throw an AbstractMethodError. This is because Foo expects there to be a method of the generified form, taking ReadableInstant.

So, its basically a mess. I can't come up with a way to preserve backwards compatibility while adding generics. But I thought the whole point was to allow migration compatibility!

So, I thought that perhaps I'm missing something stupid. If so, please let me know! All the code is in svn (1.6 branch and TRUNK) for anyone wanting to play with real code.

8 comments:

  1. (Firstly your generic parameters are missing from your code example - JRoller is treating them like HTML)

    You are correct about the lack of source compatibility. If classes that implement ReadableInstant are compiled against the new generified version, they will fail to compile and will need to be updated. This happened in the standard library when it was generified. e.g. http://www.velocityreviews.com/forums/t150999-stupid-calendar-class.html

    I don't believe you are correct in the 2nd case. The compareTo method will always erase down to "int compareTo(Object)".
    i.e. ReadableInstance.class.getMethod("compareTo", ReadableInstance.class) will always throw NoSuchMethodException, and ReadableInstance.class.getMethod("compareTo", Object.class) will work.

    ReplyDelete
  2. Have you looked at Java Generics and Collections by Maurice Naftalin and and Philip Wadler? Chapter 5 deals with all kinds of backward and forward compatibility issues related to generics.

    ReplyDelete
  3. This is why projects like javolution ship both a 1.4 and 1.5+ version of their library.

    ReplyDelete
  4. How about creating two compareTo methods:
    int compareTo(ReadableInstant instant) {
    // ...
    }

    int compareTo(Object instant) {
    // call above
    }

    ReplyDelete
  5. Don't you want something like:

    public interface ReadableInstant extends Comparable

    ReplyDelete
  6. Stephen Colebourne17 August 2010 at 15:40

    Michael, I thought that something involving the & operator would solve the problem, but I don't want ReadableInstant to be parameterized. Any ReadableInstant can be compared to any other - its not exact subclass based, which your approach would result in.

    ReplyDelete
  7. Michael Bar-Sinai18 August 2010 at 07:01

    I, too, suspect that there's no clean way out. Moreover, bending your code to allow backward compatibility might effect the Joda Time's readability, learnabilty etc. If comparing ReadableInstants only makes sense when doing it against other ReadableInstants, that's what the declaration should say. In the short run, migration might be hard, but in the long run you'll have a better library.

    As a migration path, you could provide an abstract class that implements the generified method, and wires it to the classic one. This will make migration easier (although not seamless) in some cases. You can @Deprecate it in the next-next release.

    abstract class MigrationReadableInstant implements ReadableInstant {

    @Override
    public int compareTo(ReadableInstant r ) {
    return compareTo( (Object)r );
    }

    abstract int compareTo( Object o );

    }

    ReplyDelete
  8. Stephen Colebourne18 August 2010 at 08:56

    The best solution I found was to remove the method definition from the ReadableInstant interface. This is compatible as it was inherited from the Comparable interface anyway. Having a real method of compareTo(ReadableInstant) was effectively adding a new method to the interface (as the erasure of Comaparable was compareTo(Object). Binary compatibility is now complete.

    There is also an AbstractInstant class. If projects have extended this class then they are unaffected by the change. Only projects that implemented ReadableInstant directly will have source incompatibility.

    ReplyDelete