Issue
I have a list of LoggerMessageDto
objects.
LoggerMessageDto
has two String
fields: message
and type
.
I want to convert this List
into a Map
with the following contents:
key: "types", value: Set.of(LoggerMessageDto::gettype)
key: "messages" , value: Set.of(LoggerMessageDto::getMessage)
My attempt:
List<LoggerMessageDto> result = getSomeResult();
Set<String> journalTypes = new HashSet<>();
Set<String> messages = new HashSet<>();
result.forEach(item -> {
journalTypes.add(item.getType());
messages.add(item.getMessage());
});
String typesKey = "types";
String messagesKey = "messages";
Map<String, Set<String>> map = Map.of(
typesKey, journalTypes,
messagesKey, messages
);
How can I achieve this using Stream API?
Solution
Java 12 - Collectors.teeing
You can use Java 12 collector teeing
, which expects three arguments: two collectors and a function. Each stream element gets consumed by both of the provided collectors and when they are done the function merges their results.
As both downstream collectors of teeing
we can use collector mapping()
in conjunction with the collector toSet()
.
public static Map<String, Set<String>> toMessagesByType(List<LoggerMessageDto> loggerMessageList,
String typesKey,
String messagesKey) {
return loggerMessageList.stream()
.collect(Collectors.teeing(
Collectors.mapping(LoggerMessageDto::getType, Collectors.toSet()),
Collectors.mapping(LoggerMessageDto::getMessage, Collectors.toSet()),
(types, messages) -> Map.of(
typesKey, types,
messagesKey, messages
)
));
}
Java 8 - String Keys
Here's a Java 8 compliant code which makes use of three-argument version of collect()
and produces the same result as solution with teeing()
:
public static Map<String, Set<String>> toMessagesByType(List<LoggerMessageDto> loggerMessageList,
String typesKey,
String messagesKey) {
return loggerMessageList.stream()
.collect(
() -> Map.of(
typesKey, new HashSet<>(),
messagesKey, new HashSet<>()
),
(Map<String, Set<String>> map, LoggerMessageDto next) -> {
map.get(typesKey).add(next.getType());
map.get(messagesKey).add(next.getMessage());
},
(left, right) ->
right.forEach((k, v) -> left.get(k).addAll(v))
);
}
Enum
Also, it's worth to mention that enums are more handy and reliable than strings, as @Joop Eggen has point out in the comment. Apart from saving you from typo which might occur while using strings, enums have an extensive language support (specialized collections: EnumMap
, EnumSet
; they can be used in custom annotations; and in switch
expression/statements, etc.).
Java 8 - Enum & EnumMap
Similarly to the solution shown above, we can use of three-args collect()
and provide a prepopulated EnumMap
in the supplier.
public static Map<LoggerKeys, Set<String>> toMessagesByType(List<LoggerMessageDto> loggerMessageList) {
return loggerMessageList.stream()
.collect(
() -> EnumSet.allOf(LoggerKeys.class).stream()
.collect(Collectors.toMap(
Function.identity(),
e -> new HashSet<>(),
(v1, v2) -> { throw new AssertionError("duplicates are not expected"); },
() -> new EnumMap<>(LoggerKeys.class)
)),
(Map<LoggerKeys, Set<String>> map, LoggerMessageDto next) ->
map.forEach((k, v) -> v.add(k.getKeyExtractor().apply(next))),
(left, right) ->
right.forEach((k, v) -> left.get(k).addAll(v))
);
}
Enum having a Function as property:
public enum LoggerKeys {
TYPES(LoggerMessageDto::getType), MESSAGES(LoggerMessageDto::getMessage);
private Function<LoggerMessageDto, String> keyExtractor;
LoggerKeys(Function<LoggerMessageDto, String> keyExtractor) {
this.keyExtractor = keyExtractor;
}
public Function<LoggerMessageDto, String> getKeyExtractor() {
return keyExtractor;
}
}
Answered By - Alexander Ivanchenko
Answer Checked By - Cary Denson (JavaFixing Admin)