javahashmapjava-streamcollectors

Create a Map<String, Set<String>> from a List<MyObject> using Stream API


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<LoggerKey, Set<String>> map, LoggerMessageDto next) ->
                    map.forEach((loggerKey, set) -> set.add(loggerKey.getKey(next))),
                (left, right) ->
                    right.forEach((k, v) -> left.get(k).addAll(v))
            );
    }
    

    LoggerKey enum having a keyExtractor Function as a property:

    public enum LoggerKey {
        TYPES(LoggerMessageDto::getType), MESSAGES(LoggerMessageDto::getMessage);
        
        private Function<LoggerMessageDto, String> keyExtractor;
    
        LoggerKeys(Function<LoggerMessageDto, String> keyExtractor) {
            this.keyExtractor = keyExtractor;
        }
    
        public Function<LoggerMessageDto, String> getKey(LoggerMessageDto dto) {
            return keyExtractor.apply(dto);
        }
    }