views:

78

answers:

4

Hi,

I want to write a type-safe map method in Java that returns a Collection of the same type as the argument passed (i.e. ArrayList, LinkedList, TreeSet, etc.) but with a different generic type (that between the angled brackets), determined by the generic type of another parameter (the resulting type of the generic mapping function).

So the code would be used as:

public interface TransformFunctor<S, R> {
    public R apply(S sourceObject);
}

TransformFunctor i2s = new TransformFunctor<Integer, String>() {
    String apply(Integer i) {return i.toString();};

ArrayList<Integer> ali = new ArrayList<Integer>(Arrays.asList(1, 2, 3, 4));
ArrayList<String> als = map(ali, i2s);

TreeSet<Integer> tsi = new TreeSet<Integer>(Arrays.asList(1, 2, 3, 4));
TreeSet<String> tss = map(tsi, i2s);

The idea would be something like:

public static <C extends Collection<?>, S, R>
C<R> map(final C<S> collection, final TransformFunctor<S, R> f)
throws Exception {
    //if this casting can be removed, the better
    C<R> result = (C<R>) collection.getClass().newInstance();
    for (S i : collection) {
        result.add(f.apply(i));
    }
    return result;
}

but that doesn't work because the compiler isn't expecting generic type variables to be further specialised (I think).

Any idea on how to make this work?

+1  A: 

It seems that it is not possible to use generic types on generic types. Since you need only a limited number of those you can just enumerate them:

public static <CS extends Collection<S>, CR extends Collection<R>, S, R> CR map(
        final CS collection, final TransformFunctor<S, R> f)
        throws Exception {
    // if this casting can be removed, the better
    CR result = (CR) collection.getClass().newInstance();
    for (S i : collection) {
        result.add(f.apply(i));
    }
    return result;
}

I used CS as source collection and CR as result collection. I'm afraid you can't remove the cast because you can't use generics at runtime. newInstance() just creates an object of type some collection of Object and the cast to CR is necessary to satisfy the compiler. But it's still something of a cheat. That's why the compiler issues a warning that you have to suppress with @SuppressWarnings("unchecked").

Interesting question btw.

musiKk
Your CS and CR generic types aren't needed. See my answer.
Jorn
It depends. If you remove them, the method loses some expressiveness. Your solution needs a cast of the result (if you want something more specific than `Collection`). Mine doesn't need a cast for all types that are equal to the input type or a super type. Sure the many type variables are ugly. But it makes it easier to use and I think the user should always have a simpler life than the implementer.
musiKk
A: 

What you want to do is not possible. You can, however, specify the generic type of your returned collection.

public static <S, R> Collection<R> map(Collection<S> collection, TransformFunctor<S,R> f) throws Exception {
    Collection<R> result = collection.getClass().newInstance();
    for (S i : collection) {
        result.add(f.apply(i));
    }
    return result;
}

This does return a collection of the type you specify with your argument, it just doesn't do so explicitly. It is, however, safe to cast the method return to the collection type of your argument. You should note, however, that the code will fail if the collection type you pass does not have a no-arg constructor.

You can then use it like this:

ArrayList<Integer> ali = new ArrayList<Integer>(Arrays.asList(1, 2, 3, 4));
ArrayList<String> als = (ArrayList<String>) map(ali, i2s);

TreeSet<Integer> tsi = new TreeSet<Integer>(Arrays.asList(1, 2, 3, 4));
TreeSet<String> tss = (TreeSet<String>) map(tsi, i2s);
Jorn
It also fails in case the `add` operation is unsupported which is allowed by the `Collection` interface or the constructor is non-public.
musiKk
Sure, but I was only trying to point out a *likely* case
Jorn
A: 

This works:

class Test {

    public static void main(String[] args) throws Exception {
        Function<Integer, String> i2s = new Function<Integer, String>() {
            public String apply(Integer i) {
                return i.toString();
            }
        };

        ArrayList<Integer> ali = new ArrayList<Integer>(Arrays.asList(1, 2, 3,
                4));
        ArrayList<String> als = map(ali, i2s);

        TreeSet<Integer> tsi = new TreeSet<Integer>(Arrays.asList(1, 2, 3, 4));
        TreeSet<String> tss = map(tsi, i2s);
        System.out.println(""+ali+als+tss);
    }

    static <SC extends Collection<S>, S, T, TC extends Collection<T>> TC map(
            SC collection, Function<S, T> func) throws Exception {
        // if this casting can be removed, the better
        TC result = (TC) collection.getClass().newInstance();
        for (S i : collection) {
            result.add(func.apply(i));
        }
        return result;
    }

}

interface Function<S, R> {
    R apply(S src);
}
Enno Shioji
It works, but it doesn't force the type of the returned collection to be equal to the type of the source collection. Mine doesn't either, but at least it has less useless generic types ;)
Jorn
It's not safe. You can do `ArrayList<String> tss = map(tsi, i2s);` and it will compile the same, but fail at runtime...
fortran
@fortan: Oh, I see...
Enno Shioji
+2  A: 

AFAIK there is no way to do this in Java so that it's both (a) compile-type-safe and (b) the caller of map does not need to repeat the collection type of source and target. Java does not support higher-order kinds, what you're asking for can be achieved in Scala 2.8, but even there the implementation details are somewhat complex, see this SO question.

Yardena