-1

I have a class :

public class A {
     String type;
     List<String> names;
}

Given a list of objects of this class (List<A>) how can I create a Map<String, A'> in which :

  • the key is the type String.
  • the value A' is a new object of type A, formed by merging all A instances of the input list having the same type - the names List of that new object will contain all the names of the merged A instances' names lists.

I tried using toMap and groupingBy from Java Collectors framework, but toMap throws an IllegalStateException since there are duplicate keys, whereas groupingBy creates a map of the form Map<String, List<A>>, whereas I want a Map<String, A>.

Eran
  • 387,369
  • 54
  • 702
  • 768
Abhigyan Mehra
  • 215
  • 1
  • 4
  • 7
  • 1
    what have you tried so for? Better to show that with an example of input and expected output. – Mritunjay Dec 14 '16 at 12:53
  • That suggested duplicate is not closely related to this question. – Eran Dec 14 '16 at 12:57
  • @choasia The type of 'A' is the class mentioned in the question. – Abhigyan Mehra Dec 14 '16 at 12:59
  • 2
    @Eran why that? The topic is partially the same. the part where two equal `type` for two different instances of `A` will be a merged into a single `A` with the content of both `names` `List`'s beeing merged is the one thing not beeing adressed there. Though i wouldn´t mark it as a dupe of it either, but it contains necessary information. – SomeJavaGuy Dec 14 '16 at 13:01
  • 2
    @KevinEsche The problem in this question cannot be solved with a simple `Collectors.toMap` (as the duplicate target, which is a much simpler question), since the OP is not mapping each element of the input List to a single entry of the output Map. This question requires `Collectors.groupingBy` and even that won't be enough, since the value of the output map should be an aggregation of the `A` instances grouped for the same key. – Eran Dec 14 '16 at 13:04
  • I'm voting to open since it's not a correct duplicate. – Eran Dec 14 '16 at 13:11

4 Answers4

4

I don't think it can be done in a single Stream pipeline (though I think it may be possible in Java 9).

groupingBy takes you in the right direction, but you cannot combine it with another Collector that would map the List<A> elements into a single A instance (that contains all the names).

You can use two Stream pipelines :

Map<String, A> result =
    listOfA.stream()
           .collect(Collectors.groupingBy(a -> a.type)) // Map<String,List<A>>
           .entrySet()
           .stream()
           .collectors.toMap(e -> e.getKey (),
                             e -> new A (e.getKey(),
                                         e.getValue()
                                          .stream()
                                          .flatMap(a->a.names.stream()) 
                                          .collect(Collectors.toList())));

This assumes that A has an A (String type, List<String> names) constructor.

The names List of the output A instances may include duplicates. If you don't want that, you can collect the names to a Set and then create a List initialized by that Set.

Eran
  • 387,369
  • 54
  • 702
  • 768
  • Or you create a custom `Collector` creating `A`s in the first place. [Then](http://stackoverflow.com/a/41146162/2711488), it works with a single Stream op and under Java 8. As a rule of thumb, whenever the elements end up being stored in a Collection, you can get away without the [`flatMapping`](http://download.java.net/java/jdk9/docs/api/java/util/stream/Collectors.html#flatMapping-java.util.function.Function-java.util.stream.Collector-) collector by simply replacing `add` with `addAll` (compared to the ordinary `toList()` or `toCollection(…)` Collector). – Holger Dec 14 '16 at 15:26
3

Assuming that A’s constructor initializes names with a mutable List, you can use

Map<String, A> result=list.stream()
    .collect(Collectors.groupingBy(a -> a.type, Collector.of(A::new,
        (a,t)  ->{ a.type=t.type; a.names.addAll(t.names); },
        (a1,a2)->{ a1.names.addAll(a2.names); return a1; })));

Otherwise, you had to use

Map<String, A> result=list.stream()
    .collect(Collectors.groupingBy(a -> a.type, Collector.of(
        ()     ->{ A a = new A(); a.names=new ArrayList<>(); return a; },
        (a,t)  ->{ a.type=t.type; a.names.addAll(t.names); },
        (a1,a2)->{ a1.names.addAll(a2.names); return a1; })));

Of course, the code would be simpler, if you have a real class with usable methods instead of that A sketch consisting of two fields only.

Holger
  • 285,553
  • 42
  • 434
  • 765
1

You can replace the value of the Map with a combined value with the toMap Collector.

If your class A looks like this:

class A {
  private final String type;
  private final List<String> names;

  private A(String type, List<String> names) {
    this.type = type;
    this.names = names;
  }

  public String getType() {
    return type;
  }

  public List<String> getNames() {
    return names;
  }

  public static A merge(A a, A b) {
    if (!Objects.equals(a.type, b.type)) {
      throw new IllegalArgumentException();
    }
    return of(a.type, Stream.concat(a.names.stream(), b.names.stream()).toArray(String[]::new));
  }

  public static A of(String type, String... names) {
    return new A(type, Arrays.asList(names));
  }

  @Override
  public String toString() {
    return "A [type=" + type + ", names=" + names + "]";
  }

}

You can easily create your desired output with:

List<A> list = Arrays.asList(A.of("a", "a", "b", "c"), A.of("a", "d", "e"), A.of("b"), A.of("c", "x", "y", "z"));

Map<String, A> collect = list.stream().collect(Collectors.toMap(A::getType, Function.identity(), A::merge));

Outcome:

{a=A [type=a, names=[a, b, c, d, e]], b=A [type=b, names=[]], c=A [type=c, names=[x, y, z]]}
Flown
  • 11,480
  • 3
  • 45
  • 62
0

The reason you failed to use toMap is because you didn't provide a merge function to handle the duplicate keys. (See the javadoc for the three-argument version of Collectors.toMap, which includes an excellent example for merging addresses.)

Here is my solution, which is similar to the answer provided by Flown, but makes no changes to the original class (my code does not assume it is an immutable type).

    Map<String, A> result = list.stream().collect(
            Collectors.toMap(
                a -> a.type, // keyMapper
                a -> { // valueMapper
                    A value = new A();
                    value.type = a.type;
                    value.names = new ArrayList<>(a.names);
                    return value;
                },
                (A value1, A value2) -> { // mergeFunction
                    value1.names.addAll(value2.names);
                    return value1;
                }));
Community
  • 1
  • 1
Patrick Parker
  • 4,863
  • 4
  • 19
  • 51