Emanuel Vintilă
Emanuel Vintilă

Reputation: 1939

Java map Collection<T> to Collection<U>

I need to transform an arbitrary Collection<T> into another arbitrary Collection<U>. For example, I would like to transform an ArrayList<String> into a HashSet<Integer>.

I wrote the following code, which gives me a compile-time error on UCollection::new (Cannot resolve constructor 'UCollection'). I tried replacing it with () -> new UCollection(), which gives me another compile-time error (Type parameter 'UCollection' cannot be instantiated directly).

import java.util.Collection;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Utils {
    public static <T, U, TCollection extends Collection<T>, UCollection extends Collection<U>>
    UCollection MappedCollection(TCollection collection, Function<T, U> function) {
        return MappedStream(collection.stream(), function).collect(Collectors.toCollection(UCollection::new));
    }

    public static <T, U> Stream<U> MappedStream(Stream<T> stream, Function<T, U> function) {
        return stream.map(function);
    }
}

Upvotes: 0

Views: 578

Answers (3)

ernest_k
ernest_k

Reputation: 45339

UCollection::new is invalid because UCollection is a type variable. You can't construct a type that you don't know in the method.

The easiest fix here is to make your caller supply a UCollection factory:

public static <T, U, TCollection extends Collection<T>, 
                UCollection extends Collection<U>>
    UCollection MappedCollection(TCollection collection, 
                                 Function<T, U> function,
                                 Supplier<UCollection> uCollectionSupplier) {

    return MappedStream(collection.stream(), function)
           .collect(Collectors.toCollection(uCollectionSupplier));
}

As a side note, I think you have one type variable too many. You could dispense with TCollection (using C for UCollection below)...

public static <T, U, C extends Collection<U>> 
    C mappedCollection(Collection<T> collection, 
                       Function<T, U> function,
                       Supplier<C> collectionSupplier) {
    return MappedStream(collection.stream(), function)
           .collect(Collectors.toCollection(collectionSupplier));
}

Upvotes: 4

Joop Eggen
Joop Eggen

Reputation: 109613

I would suggest an output parameter instead of a result, as it allows interface based variables, already providing the correct implementation class. The other answer requires actually passing an ArrayList::new which is a bit unfortunately, though a result is more functional programming style.

public <P, R> void convert(Collection<P> cp, Collection<R> cr, Function<P, R> mapper) {
    cp.stream().map(mapper).forEach(cr::add);
}

    List<String> slist = new ArrayList<>();
    Collections.addAll(slist, "2", "3", "5", "7", "5", "3");
    Set<Integer> iset = new HashSet<>();
    convert(slist, iset, Integer::valueOf);
    System.out.println(iset);

Stream aggregating and collecting often has such a new collection supplier as parameter as in the other answer. But where the resulting collection is created seems irrelevant. And a result sometimes requires type inference (no interface vars, or needing a cast).

Upvotes: 1

Turing85
Turing85

Reputation: 20205

I would suggest a different, in my opinion more readable, solution:

public static <T, U> List<U> convert(Collection<T> collection, Function<T, U> mapper) {
    return collection.stream()
        .map(mapper)
        .collect(Collectors.toList());
}

Ideone Demo

If you really need to control the exact collection type, then use ernest_k's approach by passing a Supplier<UCollection> supplier to the method:

public static <T, U, UCollection extends Collection<U>> UCollection convert(
        Collection<T> collection,
        Function<T, U> mapper, 
        Supplier<UCollection> collectionSupplier) {
    return collection.stream()
        .map(mapper)
        .collect(Collectors.toCollection(collectionSupplier));
}

Ideone Demo

Upvotes: 0

Related Questions