4

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?

4

4 回答 4

2

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.

于 2010-07-29T10:26:51.730 回答
1

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.

于 2010-07-28T13:03:04.787 回答
0

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);
于 2010-07-28T13:11:39.217 回答
0

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);
}
于 2010-07-28T13:17:17.633 回答