Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 groupingby with custom key

I have a series of input Strings in the following format:

typeA:code1,
typeA:code2,
typeA:code3,
typeB:code4,
typeB:code5,
typeB:code6,
typeC:code7,
...

and I need to get a Map<String, List<String>> with the following structure:

typeA, [code1, code2, code3]
typeB, [code4, code5, code6]
typeC, [code7, code8, ...]

The catch is that to generate each type I need to call a function like this one on each input String:

public static String getType(String code)
{
  return code.split(":")[0];  // yes this is horrible code, it's just for the example, honestly
}

I'm pretty confident that Streams and Collectors can do this, but I'm struggling to get the right incantation of spells to make it happen.

like image 615
endian Avatar asked Aug 28 '15 13:08

endian


3 Answers

Here is one way to do it (assuming the class is named A):

Map<String, List<String>> result = Stream.of(input)
                          .collect(groupingBy(A::getType, mapping(A::getValue, toList())));

If you want the output sorted you can use a TreeMap instead of the default HashMap:

.collect(groupingBy(A::getType, TreeMap::new, mapping(A::getValue, toList())));

Full example:

public static void main(String[] args) {
  String input[] = ("typeA:code1," +
                "typeA:code2," +
                "typeA:code3," +
                "typeB:code4," +
                "typeB:code5," +
                "typeB:code6," +
                "typeC:code7").split(",");

  Map<String, List<String>> result = Stream.of(input)
                    .collect(groupingBy(A::getType, mapping(A::getValue, toList())));
  System.out.println(result);
}

public static String getType(String code) {
  return code.split(":")[0];
}
public static String getValue(String code) {
  return code.split(":")[1];
}
like image 175
assylias Avatar answered Sep 18 '22 11:09

assylias


The code becomes simple if you consider what you have omitted, that you need the second part of the split string as well:

Map<String, List<String>> result = Stream.of(input).map(s->s.split(":", 2))
    .collect(groupingBy(a->a[0], mapping(a->a[1], toList())));

(assuming you have a import static java.util.stream.Collectors.*;)

There is nothing wrong with splitting a String into an array, the implementation has even a “fast-path” for the common case you are splitting using a single simple character instead of a complicate regular expression.

like image 36
Holger Avatar answered Sep 20 '22 11:09

Holger


Although I was too slow, here is an MCVE showing how this can be solved with Collectors#groupingBy.

There are obviously different options for defining the "classifier" and "mapper". Here I'm simply using String#substring to find the part before and after the ":".

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

public class GroupingBySubstringsTest
{
    public static void main(String[] args)
    {
        List<String> strings = new ArrayList<String>();
        strings.add("typeA:code1");
        strings.add("typeA:code2");
        strings.add("typeA:code3");
        strings.add("typeB:code4");
        strings.add("typeB:code5");
        strings.add("typeB:code6");
        strings.add("typeC:code7");

        Map<String, List<String>> result = strings.stream().collect(
            groupingBy(s -> s.substring(0, s.indexOf(":")), 
                mapping(s -> s.substring(s.indexOf(":")+1), toList())));

        for (Entry<String, List<String>> entry : result.entrySet())
        {
            System.out.println(entry);
        }
    }
}
like image 36
Marco13 Avatar answered Sep 19 '22 11:09

Marco13