Search

Dark theme | Light theme

January 27, 2021

Java Joy: Transform Stream Of Strings To List Of Key Value Pairs Or Map

Suppose we have a Stream of String objects where two sequential values belong together as a pair. We want to transform the stream into a List where each pair is transformed into a Map object with a key and value. We can write a custom Collector that stores the first String value of a pair. When the next element in the Stream is processed by the Collector a Map object is created with the stored first value and the new value. The new Map is added to the result List.

In the next example we write the ListMapCollector to transform a Stream of paired String values into a List of Map objects:

package mrhaki.streams;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        List<Map<String, String>> pairs =
                Stream.of("language", "Java", "username", "mrhaki")
                      .collect(new ListMapCollector());

        // Result is list of maps: [{language=Java},{username=mrhaki}]
        assert pairs.size() == 2;
        assert pairs.get(0).get("language").equals("Java");
        assert pairs.get(1).get("username").equals("mrhaki");
    }

    private static class ListMapCollector
            implements Collector<String, List<Map<String, String>>, List<Map<String, String>>> {

        private String key;

        /**
         * @return An empty list to add our Map objects to.
         */
        @Override
        public Supplier<List<Map<String, String>>> supplier() {
            return () -> new ArrayList<>();
        }

        /**
         * @return Accumulator to add Map with key and value to the result list.
         */
        @Override
        public BiConsumer<List<Map<String, String>>, String> accumulator() {
            return (list, value) -> {
                if (key != null) {
                    list.add(Map.of(key, value));
                    key = null;
                } else {
                    key = value;
                }
            };
        }

        /**
         * @return Combine two result lists into a single list with all Map objects.
         */
        @Override
        public BinaryOperator<List<Map<String, String>>> combiner() {
            return (list1, list2) -> {
                list1.addAll(list2);
                return list1;
            };
        }

        /**
         * @return Use identity function to return result.
         */
        @Override
        public Function<List<Map<String, String>>, List<Map<String, String>>> finisher() {
            return Function.identity();
        }

        /**
         * @return Collector characteristic to indicate finisher method is identity function.
         */
        @Override
        public Set<Characteristics> characteristics() {
            return Set.of(Characteristics.IDENTITY_FINISH);
        }
    }
}

Another solution could be to turn the Stream with values into a single Map. Each pair of values is a key/value pair in the resulting Map:

package mrhaki.streams;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Map<String, String> map = 
                Stream.of("language", "Java", "username", "mrhaki")
                      .collect(new MapCollector());
        
        // Result is map: {language=Java,username=mrhaki}
        assert map.size() == 2;
        assert map.get("language").equals("Java");
        assert map.get("username").equals("mrhaki");
    }

    private static class MapCollector
            implements Collector<String, Map<String, String>, Map<String, String>> {

        private String key;

        /**
         * @return An empty map to add keys with values to.
         */
        @Override
        public Supplier<Map<String, String>> supplier() {
            return () -> new HashMap<>();
        }

        /**
         * @return Accumulator to add key and value to the result map.
         */
        @Override
        public BiConsumer<Map<String, String>, String> accumulator() {
            return (map, value) -> {
                if (key != null) {
                    map.put(key, value);
                    key = null;
                } else {
                    key = value;
                }
            };
        }

        /**
         * @return Combine two result maps into a single map.
         */
        @Override
        public BinaryOperator<Map<String, String>> combiner() {
            return (map1, map2) -> {
                map1.putAll(map2);
                return map1;
            };
        }

        /**
         * @return Use identity function to return result.
         */
        @Override
        public Function<Map<String, String>, Map<String, String>> finisher() {
            return Function.identity();
        }

        /**
         * @return Collector characteristic to indicate finisher method is identity function.
         */
        @Override
        public Set<Characteristics> characteristics() {
            return Set.of(Characteristics.IDENTITY_FINISH);
        }
    }
}

Written with Java 15.0.1.