Stuart
Stuart

Reputation: 130

Transform list to mapping using java streams

I have the following pattern repeated throughout my code:

class X<T, V>
{
    V doTransform(T t) {
        return null; // dummy implementation
    }

    Map<T, V> transform(List<T> item) {
        return item.stream().map(x->new AbstractMap.SimpleEntry<>(x, doTransform(x))).collect(toMap(x->x.getKey(), x->x.getValue()));
    }
}

Requiring the use of AbstractMap.SimpleEntry is messy and clunky. Linqs use of anonymous types is more elegant.

Is there a simpler way to achieve this using streams?

Thx in advance.

Upvotes: 1

Views: 68

Answers (3)

Holger
Holger

Reputation: 298579

In this specific example, there is no need to do the intermediate storage at all:

Map<T, V> transform(List<T> item) {
    return item.stream().collect(toMap(x -> x, x -> doTransform(x)));
}

But if you need it, Java 9 offers a simpler factory method,

Map<T, V> transform(List<T> item) {
    return item.stream()
               .map(x -> Map.entry(x, doTransform(x)))
               .collect(toMap(x -> x.getKey(), x -> x.getValue()));
}

as long as you don’t have to deal with null.

You can use an anonymous inner class here,

Map<T, V> transform(List<T> item) {
    return item.stream()
               .map(x -> new Object(){ T t = x; V v = doTransform(x); })
               .collect(toMap(x -> x.t, x -> x.v));
}

but it’s less efficient. It’s an inner class which captures a reference to the surrounding this, also it captures x, so you have two fields, t and the synthetic one for capturing x, for the same thing.

The latter could be circumvented by using a method, e.g.

Map<T, V> transform(List<T> item) {
    return item.stream()
               .map(x -> new Object(){ T getKey() { return x; } V v = doTransform(x); })
               .collect(toMap(x -> x.getKey(), x -> x.v));
}

But it doesn’t add to readability.

The only true anonymous types are the types generated for lambda expressions, which could be used to store information via higher order functions:

Map<T, V> transform(List<T> item) {
    return item.stream()
               .map(x -> capture(x, doTransform(x)))
               .collect(HashMap::new, (m,f) -> f.accept(m::put), HashMap::putAll);
}
public static <A,B> Consumer<BiConsumer<A,B>> capture(A a, B b) {
    return f -> f.accept(a, b);
}

but you’d soon hit the limitations of Java’s type system (it still isn’t a functional programming language) if you try this with more complex scenarios.

Upvotes: 1

Ousmane D.
Ousmane D.

Reputation: 56489

Unfortunately, Java doesn't have an exact equivalent of C#'s anonymous types.

In this specific case, you don't need the intermediate map operation as @Jorn Vernee has suggested. instead, you can perform the key and value extraction in the toMap collector.

However, when it gets to cases where you think you need something as such of C#'s anonymous types you may consider:

  1. anonymous objects (may not always be what you want depending on your use case)
  2. Arrays.asList(...), List.of(...) (may not always be what you want depending on your use case)
  3. an array (may not always be what you want depending on your use case)

Ultimately, If you really need to map to something that can contain two different types of elements then I'd stick with the AbstractMap.SimpleEntry.

That, said your current example can be simplified to:

Map<T, V> transform(List<T> items) {
    return items.stream().collect(toMap(Function.identity(),this::doTransform));
}

Upvotes: 2

Jorn Vernee
Jorn Vernee

Reputation: 33905

You can call doTransform in the value mapper:

Map<T, V> transform(List<T> item) {
    return item.stream().collect(toMap(x -> x, x -> doTransform(x)));
}

Upvotes: 3

Related Questions