Monday, 20 September 2010

Synthetic Composite Types with Java Generics

Perhaps everyone already knows about this, but I just found a very interesting feature of Java Generics - you can declare synthetic composite types using multiple bounds to allow you to treat two different classes that implement the same interfaces as if they had a common super type, even if they don't.

Consider the following classes:

interface FooInterface {
    void methodOnFooInterface();
}
interface BarInterface {
    void methodOnBarInterface();
}
class Foo implements FooInterface, BarInterface { ... }
class Bar implements FooInterface, BarInterface { ... }

Now imagine that you want to write a method that depends on taking items that are both FooInterface and BarInterface, but beyond that does not care about their types. It should accept both instances of Foo and Bar. In the past I'd have cursed the fact that Foo and Bar have no common supertype capturing being both FooInterface and BarInterface, but I now know you can declare a method as so:

<T extends FooInterface & BarInterface> void doSomething(T instance) {
    instance.methodOnFooInterface();
    instance.methodOnBarInterface();
}
doSomething(new Foo());
doSomething(new Bar());

However, what I haven't yet worked out how to do is use a method that can take a Collection of that synthetic type, as so:

<T extends FooInterface & BarInterface> void doSomethingElse(Collection<T> collection) {
    for (T instance : collection) {
        instance.methodOnFooInterface();
        instance.methodOnBarInterface();
    }
}

The method declaration works, but how to call it? I can find no way to instantiate a Collection with a synthetic type - this does not work:

Collection<FooInterface & BarInterface> things = new ArrayList<FooInterface & BarInterface>); // DOES NOT COMPILE
this.add(new Foo());
this.add(new Bar());
doSomethingElse(things);

Can anyone help? This feels to me like a very powerful feature, potentially.

Edit: found a clunky way to do it:

<T extends FooInterface & BarInterface> Collection<T> makeCollection(T instance1, T instance2) {
    Collection<T> collection = new ArrayList<T>();
    collection.add(instance1);
    collection.add(instance2);
    return collection;
}
doSomethingElse(makeCollection(new Foo(), new Bar()));

It would look more flexible with varargs, but then the compiler issues a warning about unchecked operations; a warning isn't the end of the world so that may be a preferable solution. But it's still pretty ugly - you cannot pull the

makeCollection
call out into an instance variable as there is no way to declare its type, so it has to stay inlined.

7 comments:

  1. public static [T] List[T] newArrayList() {
    return new ArrayList[T]();
    }

    // Replace the [] characters of course.. ;-)

    ReplyDelete
  2. How about creating an interface that simply extends both FooInterface and BarInterface and does not do anything else and using it instead? It prevents a lot of code duplication.

    ReplyDelete
  3. @Stephan - I can't get that to compile. Well, the method does - but I can't find a way to call it and return a value that can be passed to doSomethingElse()

    @OHaleck - because Foo and Bar would need to implement that interface for it to be useful, and I don't control Foo and Bar.

    ReplyDelete
  4. Some example code:


    public [T extends MyInterface & YourInterface] T myMethod(Collection[T] col1) {
    List[T] col2 = newArrayList();
    T result = null;
    for (T t : col1) {
    result = t;
    }
    doSomethingElse(result);
    doSomethingElseCollection(col1);
    doSomethingElseCollection(col2);
    return result;
    }

    public static [T] List[T] newArrayList() {
    return new ArrayList[T]();
    }

    public [T extends MyInterface & YourInterface] void doSomethingElse(T item) {

    }

    public [T extends MyInterface & YourInterface] void doSomethingElseCollection(Collection[T] item) {

    }

    ReplyDelete
  5. @Stephan - hasn't that just pushed the problem back a level? I now can't call myMethod, for exactly the same reason - no means of constructing the collection. Perhaps an example starting with a static void main would clarify?

    ReplyDelete
  6. yes, you can't declare such a variable outside of some generic context.
    but you still can do much pushing it thru parameters only.
    BTW, Arrays#asList works even better for your situation.
    public void doch() {
    doch(Arrays.asList(new Donkey(), new Monkey()));
    }
    public void doch(Collection mds) {
    mds.forEach(Mon::monkey);
    noch(mds);
    }
    public void noch(Collection mds) {
    mds.forEach(Don::donkey);
    }

    ReplyDelete
  7. <T extends Mon&Don> void (d|n)och(Collection<T> mds)

    ReplyDelete