Java 8 Nested (Multi level) group by
You can’t group a single item by multiple keys, unless you accept the item to potentially appear in multiple groups. In that case, you want to perform a kind of flatMap
operation.
One way to achieve this, is to use Stream.flatMap
with a temporary pair holding the combinations of Item
and SubItem
before collecting. Due to the absence of a standard pair type, a typical solution is to use Map.Entry
for that:
Map<T, Map<V, List<SubItem>>> result = pojo.getItems().stream()
.flatMap(item -> item.subItems.stream()
.map(sub -> new AbstractMap.SimpleImmutableEntry<>(item.getKey1(), sub)))
.collect(Collectors.groupingBy(AbstractMap.SimpleImmutableEntry::getKey,
Collectors.mapping(Map.Entry::getValue,
Collectors.groupingBy(SubItem::getKey2))));
An alternative not requiring these temporary objects would be performing the flatMap
operation right in the collector, but unfortunately, flatMapping
won't be there until Java 9.
With that, the solution would look like
Map<T, Map<V, List<SubItem>>> result = pojo.getItems().stream()
.collect(Collectors.groupingBy(Item::getKey1,
Collectors.flatMapping(item -> item.getSubItems().stream(),
Collectors.groupingBy(SubItem::getKey2))));
and if we don’t want to wait for Java 9 for that, we may add a similar collector to our code base, as it’s not so hard to implement:
static <T,U,A,R> Collector<T,?,R> flatMapping(
Function<? super T,? extends Stream<? extends U>> mapper,
Collector<? super U,A,R> downstream) {
BiConsumer<A, ? super U> acc = downstream.accumulator();
return Collector.of(downstream.supplier(),
(a, t) -> { try(Stream<? extends U> s=mapper.apply(t)) {
if(s!=null) s.forEachOrdered(u -> acc.accept(a, u));
}},
downstream.combiner(), downstream.finisher(),
downstream.characteristics().toArray(new Collector.Characteristics[0]));
}
Collectors nested grouping-by with multiple fields
If you can use Java 9 or higher, you can use Collectors.flatMapping()
to achieve that:
Map<String, Map<String, Long>> eventList = list.stream()
.collect(Collectors.groupingBy(MyObject::getSite, Collectors.flatMapping(
o -> Stream.of(o.getSource(), o.getSeverity()),
Collectors.groupingBy(Function.identity(), Collectors.counting())
)));
The result will be this:
{
USA={maint=2, HARMLESS=2},
GERMANY={CPU_Checker=1, MINOR=1}
}
If you are not able to use Java 9 you can implement the flatMapping()
function yourself. You can take a look at Java 9 Collectors.flatMapping rewritten in Java 8, which should help you with that.
How to create a nested Map using Collectors.groupingBy?
Abstract / Brief discussion
Having a map of maps of maps is questionable when seen from an object-oriented prespective, as it might seem that you're lacking some abstraction (i.e. you could create a class Result
that encapsulates the results of the nested grouping). However, it's perfectly reasonable when considered exclusively from a pure data-oriented approach.
So here I present two approaches: the first one is purely data-oriented (with nested groupingBy
calls, hence nested maps), while the second one is more OO-friendly and makes a better job at abstracting the grouping criteria. Just pick the one which better represents your intentions and coding standards/traditions and, more importantly, the one you most like.
Data-oriented approach
For the first approach, you can just nest the groupingBy
calls:
Map<String, Map<String, Map<String, List<Booker>>>> result = list.stream()
.collect(Collectors.groupingBy(ProductDto::getStatus,
Collectors.groupingBy(ProductDto::getCategory,
Collectors.groupingBy(ProductDto::getType,
Collectors.mapping(
ProductDto::getBooker,
Collectors.toList())))));
As you see, the result is a Map<String, Map<String, Map<String, List<Booker>>>>
. This is because there might be more than one ProductDto
instance with the same (status, category, type)
combination.
Also, as you need Booker
instances instead of ProductDto
instances, I'm adapting the last groupingBy
collector so that it returns Booker
s instead of productDto
s.
About reduction
If you need to have only one Booker
instance instead of a List<Booker>
as the value of the innermost map, you would need a way to reduce Booker
instances, i.e. convert many instances into one by means of an associative operation (accumulating the sum of some attribute being the most common one).
Object-oriented friendly approach
For the second approach, having a Map<String, Map<String, Map<String, List<Booker>>>>
might be seen as bad practice or even as pure evil. So, instead of having a map of maps of maps of lists, you could have only one map of lists whose keys represent the combination of the 3 properties you want to group by.
The easiest way to do this is to use a List
as the key, as lists already provide hashCode
and equals
implementations:
Map<List<String>, List<Booker>> result = list.stream()
.collect(Collectors.groupingBy(
dto -> Arrays.asList(dto.getStatus(), dto.getCategory(), dto.getType()),
Collectors.mapping(
ProductDto::getBooker,
Collectors.toList())))));
If you are on Java 9+, you can use List.of
instead of Arrays.asList
, as List.of
returns a fully immutable and highly optimized list.
Java 8 multilevel grouping and reducing
is there any reason you made the name
in EmployeeInfo
final? if you can change that this solution will work
add these two methods to EmployeeInfo
public void setName(String name) {
this.name = name;
}
public void AddAccount(String account) {
this.accounts.add(account);
}
and then you can do this
Collector<Employee, EmployeeInfo, EmployeeInfo> empToInfo = Collector.of(
() -> new EmployeeInfo("", new ArrayList<String>()),
(info, e) -> {
info.AddAccount(e.getAccount());
info.setName(e.getName());
},
(p1,p2) -> p1.addToList(p2));
Collector<Employee, ?, Collection<EmployeeInfo>> byName = collectingAndThen(groupingBy(Employee::getName, empToInfo),
(Map<String, EmployeeInfo> finisher) -> {return finisher.values();});
Map<Integer, Collection<EmployeeInfo>> r2 = employees.stream().collect(groupingBy(Employee::getDept, byName));
if you want to keep The EmployeeInfo immutable, you can use reduction instead of collection and it will be like this
Map<Integer, Collection<EmployeeInfo>> result2 = employees.stream().collect(groupingBy(Employee::getDept,
collectingAndThen(groupingBy(Employee::getName, reducing(new EmployeeInfo("", new ArrayList<String>()),
empl3 -> new EmployeeInfo(empl3.getName(),Arrays.asList(empl3.getAccount())),
(inf1, inf2) -> inf1.addToList(inf2))),
finisher -> finisher.values())));
Group By in java 8 with multiple levels of grouping
Based on the grouping requirement, you should try using nested grouping along with Collectors.mapping
such as:
Map<String, Map<String, List<String>>> groupingRequirement = workersList.stream()
.collect(Collectors.groupingBy(Workers::getDirectorName,
Collectors.groupingBy(Workers::getManagerName,
Collectors.mapping(Workers::getEmployeeName,
Collectors.toList()))));
Thereafter mapping to objects of the desired type is the only constraint left while you iterated over the entries of the collected Map
-
List<WorkersResponse> workersResponses = groupingRequirement.entrySet().stream()
.map(e -> new WorkersResponse(e.getKey(), // director name
e.getValue().entrySet()
.stream()
.map(ie -> new Manager(ie.getKey(), // manager name
ie.getValue()
.stream()
.map(Employee::new)
.collect(Collectors.toList())))
.collect(Collectors.toList())))
.collect(Collectors.toList());
How to use groupingBy of stream API for multi level nested classes?
As you mentioned in the old question you referenced, can use the flat map approach where each nesting level is flat mapped down to the level required as follows:
Map<Triple<Long, Long, Long>, Double> result = allStudents.stream()
.flatMap(s -> s.getCourses().stream().map(
c -> ImmutableTuple.of(s.getStudentId(), c)))
.flatMap(sc -> sc.get().getTasks().stream().map(
t -> ImmutableTuple.of(sc.getFirst(), sc.get().getCourseId(), t)))
.flatMap(sct -> sct.get().getAssessments().stream().map(
a -> ImmutableTuple.of(sct.getFirst(), sct.getSecond(), sct.get().taskId, a.getScore())))
.collect(Collectors.groupingBy(
ImmutableQuadruple::remove,
Collectors.summingDouble(ImmutableQuadruple::get)
));
Note: This is using tuples from the typedtuples library
Java 8 nested groupingby
Try the following:
public Map<String, List<Map<String, String>>> doGrouping(
List<String> columns,
List<Map<String, String>> data) {
return data.stream()
.collect(Collectors.groupingBy(
elem -> columns.stream()
.map(elem::get)
.collect(Collectors.joining())));
}
First, I streamed the data
, which is a list of maps. I immediately collected the stream to a map of lists using Collectors.groupingBy
with a key that is calculated for each element of the stream.
Calculating the key was the tricky part. For this, I streamed the given list of columns
and I transformed each one of these columns into its corresponding value of the element of the stream. I did this by means of the Stream.map
method, passing elem::map
as the mapping function. Finally, I collected this inner stream into a single string by using Collectors.joining
, which concatenates each element of the stream into a final string in an efficient manner.
Edit: The code above works well if all the elements of columns
exist as keys of the map elements in data
. To be more secure use the following:
return data.stream()
.collect(Collectors.groupingBy(
elem -> columns.stream()
.map(elem::get)
.filter(Objects::nonNull)
.collect(Collectors.joining())));
This version filters out null
elements from the stream, which might occur if some map element does not contain a key specified in the columns
list.
Using groupingBy into a nested Map, but collecting to a different type of object
With a custom collector like so:
private static Collector<Collection<SomeClassB>, ?, ImmutableList<SomeClassB>>
flatMapToImmutableList() {
return Collectors.collectingAndThen(Collectors.toList(),
listOfCollectionsOfB ->
listOfCollectionsOfB.stream()
.flatMap(Collection::stream)
.collect(GuavaCollectors.toImmutableList()));
}
you can achieve what you're after:
Map<String, Map<String, List<SomeClassB>>> someMap =
someListOfClassA.stream()
.filter(...)
.collect(Collectors.groupingBy(SomeClassA::getSomeCriteriaA,
Collectors.groupingBy(SomeClassA::getSomeCriteriaB,
Collectors.mapping(a -> getSomeClassBsFromSomeClassA(a),
flatMapToImmutableList()))));
Java Streams - group-by and return a Nested Map
Is there any direct or simple way to do this, like use Collectors.toMap API or something else?
If you want to utilize only built-in collectors, you might try a combination of groupingBy()
and teeing()
.
Collectors.teeing()
expects three arguments: 2
downstream collectors and a merger
function. Each element from the stream will be passed into both collectors, and when these collectors are done, results produced by them will get merged by the function.
In the code below, toMap()
is used as both downstream collectors of teeing()
. And each of these collectors is responsible for retrieving its type of value.
The code might look like that:
public static void main(String[] args) {
List<Unit> list =
List.of(Unit.of("a", 2021, 10, 11 ),
Unit.of("a", 2022, 15, 13),
Unit.of("b", 2021, 20, 25),
Unit.of("b", 2022, 30, 37));
Map<String, Map<String, Integer>> map = list.stream()
.collect(Collectors.groupingBy(Unit::getUnitId,
Collectors.teeing(
Collectors.toMap(
unit -> unit.getYear() + "_value1",
Unit::getValue1),
Collectors.toMap(
unit -> unit.getYear() + "_value2",
Unit::getValue2),
(values1, values2) -> {values1.putAll(values2); return values1;})
));
printMap(map);
}
Output:
a: {2022_value2: 13, 2021_value1: 10, 2022_value1: 15, 2021_value2: 11}
b: {2022_value2: 37, 2021_value1: 20, 2022_value1: 30, 2021_value2: 25}
Note:
- If performance is concerned,
Collector.of()
would be slightly better because it doesn't create intermediate collections. - For this approach to work correctly (I mean the code listed above as well as in the question), each combination of
unitId
andyear
should be unique. Otherwise, consider adding a logic for resolving duplicates.
Related Topics
What Does Value & 0Xff Do in Java
Difference Between 'Optional.Orelse()' and 'Optional.Orelseget()'
How to Call a Stored Procedure from Java and JPA
Redirect Console Output to String in Java
How to Wait for a Number of Threads to Complete
How to Parse Output of New Date().Tostring()
Java Datetimeformatterbuilder Fails on Testtime
Executing a Java Application in a Separate Process
Why Call Super() in a Constructor
Why Does JPA Have a @Transient Annotation
How to Do a Junit Assert on a Message in a Logger
What Is the Maximum Depth of the Java Call Stack
How to Execute a Java .Class from the Command Line
How to Flatten a Struct in a Spark Dataframe
How to Add and Remove Elements of Enumeration at Runtime in Java