1

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?

4
  • 1
    Your current code looks good. What benefit do you expect to get from using streams instead? Commented Aug 29, 2022 at 15:14
  • 2
    Besides using an enum KeyEnum { TYPES, MESSAGES } and an EnumMap it looks okay. Commented Aug 29, 2022 at 15:23
  • @BasilBourque solely for compactness, and for general study Commented Aug 30, 2022 at 4:01
  • @JoopEggen good point, thanks, added Enum Commented Aug 30, 2022 at 4:04

2 Answers 2

3

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);
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

unfortunately, I'm using java 11, is there any similar solution?
@AlekseyBakharev You should put such constraints into your Question.
1

The Answer by Ivanchenko is correct and quite clever. But that solution requires Java 12+, and you later commented that you must use Java 11.

list.stream().map( getter ).collect( …

Here is another solution, this one working in Java 11.

Here we run through the list twice. On the first run, we use the getter method type to extract the type member field on the Log class. We collect those strings into a Set. Then we make a second run through the List of Log objects, this time extracting the message member field by calling the getter message. Those strings are also collected into a Set.

We put each of the two sets into a Map.

Map < String, Set < String > > map =
        Map.of(
                "types" , list.stream().map( Log :: type ).collect( Collectors.toSet() ) ,
                "messages" , list.stream().map( Log :: message ).collect( Collectors.toSet() )
        );

Caveat: This approach is a little bit inefficient in that it loops the list twice, once for "types" and once for "messages". But unless you are doing this many times for huge amounts of data, the impact on performance should be insignificant.

Generally best to work with unmodifiable collections until you know otherwise. So let's make those sets unmodifiable. The Map.of is already producing an unmodifiable Map object.

Map < String, Set < String > > map =
        Map.of(
                "types" , list.stream().map( Log :: type ).collect( Collectors.toUnmodifiableSet() ) ,
                "messages" , list.stream().map( Log :: message ).collect( Collectors.toUnmodifiableSet() )
        );

Here is a complete example. For brevity I used the records feature in Java 16+, but you could just as well define a conventional class. By the way, note that in Java 16+, records, enums, and interfaces can all be declared locally.

record Log( String type , String message ) { }
List < Log > list =
        List.of(
                new Log( "INFO" , "dog" ) ,
                new Log( "ERROR" , "dog" ) ,
                new Log( "INFO" , "cat" ) ,
                new Log( "INFO" , "cat" ) ,
                new Log( "DEBUG" , "cat" )
        );

Map < String, Set < String > > map =
        Map.of(
                "types" , list.stream().map( Log :: type ).collect( Collectors.toUnmodifiableSet() ) ,
                "messages" , list.stream().map( Log :: message ).collect( Collectors.toUnmodifiableSet() )
        );

System.out.println( "map = " + map );

map = {types=[INFO, DEBUG, ERROR], messages=[cat, dog]}

By the way, be aware that Set and Map by definition may iterate in any arbitrary order. The order may change even between two iterations done one after another. If order matters to you, use a NavigableSet implementation and a NavigableMap implementation.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.