Flatten a Stream of Maps to a Single Map in Java
1. Overview
Working with streams of data has become a common task in Java development since Java 8. Often, these streams contain complex structures like maps, which can pose a challenge when processing them further. In this tutorial, I’ll demonstrate how to flatten a stream of maps into a single map with Stream.flatMap()
and Collectors.toMap()
methods.
2. Introduction to the Problem
There are two basic needs that require flattening a list of maps into a single map.
- To get aggregated statistics data. Applications need to process data from multiple sources and collect statistics in the form of maps, such as counts or averages. In order to aggregate the statistics based on a single summary map, we have to flatten the stream of maps first.
- To combine a list of maps into a single map. See the following table for common use cases.
Use Case | Use Case Description |
Configuration Properties | Applications may have different sources of configuration properties, each represented as a map. Admin users need a combined view to ensure that no duplicate properties are configured. |
Database Query Results | Applications may store similar business data into several different databases, so developers need to combine multiple queries’ results into a single map via flattening the stream of query results. |
Merging JSON Documents | Applications need to convert a stream of JSON documents containing key-value pairs into a single map object. |
Configuration Properties Overrides | Systems may have multiple configuration sources: a global base configuration and additional overrides provided by different components. Flattening the stream of configuration maps with a merge function to set the final configuration property. |
Combining Data from Multiple APIs | Applications need to convert API returned data in the form of maps representing different attributes or properties into a single map. |
3. Using Stream.flatMap() and Collectors.toMap()
The Stream interface’s flatMap method is an intermediate operation. It returns a stream consisting of the results of replacing each element of this stream by applying the provided mapping function.
Here is the Stream.flatmap
method specification:
1 | <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapperFunction); |
The Collectors class’s toMap method returns a Collector
that accumulates elements into a Map
object whose keys and values are the result of applying the provided mapping functions to the input elements.
Here are the Collectors.toMap
specifications:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) { return new CollectorImpl<>(HashMap:: new , uniqKeysMapAccumulator(keyMapper, valueMapper), uniqKeysMapMerger(), CH_ID); } public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) { return toMap(keyMapper, valueMapper, mergeFunction, HashMap:: new ); } |
4. Maven Project
In this step, I will create a simple Maven java project which demonstrates how to flatten a list of maps into a single map object.
4.1 Pom.xml
Here is a pom.xml
with the Junit
dependency.
pom.xml
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" > < modelVersion >4.0.0</ modelVersion > < groupId >org.zheng.demo</ groupId > < artifactId >stream-map</ artifactId > < version >0.0.1-SNAPSHOT</ version > < name >stream-map</ name > < description >stream map</ description > < dependencies > <!-- < dependency > < groupId >org.junit.jupiter</ groupId > < artifactId >junit-jupiter-api</ artifactId > < version >5.10.2</ version > < scope >test</ scope > </ dependency > </ dependencies > </ project > |
4.2 Flatten a List of String Map
In this step, I will create a DemoFlatStringMaps
class which has two methods to flatten a List<Map<String, String>>
into a Map<String, String>
object:
with_flatMap
– utilizeStream.flatMap
andCollectors.toMap
. This method will throw aNullPointerException
if themapEntry
‘s value isnull
. It also throws anIllegalStateException
if there are duplicate keys.with_flatMap_andMerge
– utilizeStream.flatMap
andCollectors.toMap
. This method will throw aNullPointerException
if themapEntry
‘s value isnull
.
DemoFlatStringMaps.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | package org.zheng.demo; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.BinaryOperator; import java.util.stream.Collectors; public class DemoFlatStringMaps { private String mergeValue( final String existingValue, final String newValue) { if (!existingValue.equalsIgnoreCase(newValue)) { System.out.println( "old=" + existingValue + ", newValue=" + newValue); } // TODO, apply business logic apply to select the right value. return existingValue; } public Map<String, String> with_flatMap( final List<Map<String, String>> listMaps) { Map<String, String> flattenedMap = listMaps.stream().flatMap(map -> map.entrySet().stream()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); return flattenedMap; } public Map<String, String> with_flatMap_andMerge( final List<Map<String, String>> listMaps) { BinaryOperator<String> mergeFunction = (existingValue, newValue) -> mergeValue(existingValue, newValue); Map<String, String> flattenedMap = listMaps.stream().flatMap(map -> map.entrySet().stream()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, mergeFunction)); return flattenedMap; } public Map<String, String> with_putAll( final List<Map<String, String>> listMaps) { Map<String, String> flattened = new HashMap<>(); listMaps.stream().forEach(map -> { Map<String, String> combined = map.entrySet().stream() .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue())); flattened.putAll(combined); }); return flattened; } } |
- line 12: A logic to handle the duplicate keys.
- line 16: Select the
existingvalue
when duplcate data is found. Adjust logic based on the application needs. - line 20: Call
flatMap
method to flatten a list of maps to a single map object. - line 21: Call
Collectors.toMap
with method reference syntax. - line 27: Create a
mergeFunction
to select the value when duplicate keys are encountered. - line 30: Use the
mergeFunction
when callingCollectors.toMap
method, this will handle the duplcate keys map data.
4.3 Flatten a List of Object Map
In this step, I will create a DemoFlatObjectMaps
class which has four methods to flatten a List<Map<String, Object>>
into a Map<String, Object>
object:
with_flatMap
– this method is the same as step 4.2 except usingObject
data type. It also utilizesStream.flatMap
andCollectors.toMap
. This method will throw a NullPointerExceptionif themapEntry
‘s value isnull
. It also throws anIllegalStateException
if there are duplicate keys.with_flatMap_optional
– this method usesOptional
to handle thenull
value object.with_flatMap_andMerge_optional
– this method usesOptional
to handlenull
values and amergeFunction
to handle the duplicate keys.with_flatMap_andMerge_optional_l
– this method is the same aswith_flatMap_andMerge_optional
. but using a lambda expression instead ofmergeFunction
.
Both with_flatMap_andMerge_optional
and with_flatMap_andMerge_optional_l
are the robustest methods when flattening a list of maps.
DemoFlatObjectMaps.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | package org.zheng.demo; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.BinaryOperator; import java.util.stream.Collectors; public class DemoFlatObjectMaps { private static Object mergeValue( final Object existingValue, final Object newValue) { if (!existingValue.equals(newValue)) { System.out.println( "old=" + existingValue + ", newValue=" + newValue); } return existingValue; } public Map<String, Object> with_flatMap_andMerge_optional( final List<Map<String, Object>> listMaps) { BinaryOperator<Object> mergeFunction = (existingValue, newValue) -> mergeValue(existingValue, newValue); Map<String, Object> flattenedMap = listMaps.stream().flatMap(map -> map.entrySet().stream()).collect(Collectors .toMap(entry -> entry.getKey(), entry -> Optional.ofNullable(entry.getValue()), mergeFunction)); return flattenedMap; } public Map<String, Object> with_flatMap_andMerge_optional_l( final List<Map<String, Object>> listMaps) { Map<String, Object> flattenedMap = listMaps.stream().flatMap(map -> map.entrySet().stream()) .collect(Collectors.toMap(entry -> entry.getKey(), entry -> Optional.ofNullable(entry.getValue()), (existingValue, newValue) -> mergeValue(existingValue, newValue))); return flattenedMap; } public Map<String, Object> with_flatMap_optional( final List<Map<String, Object>> listMaps) { Map<String, Object> flattenedMap = listMaps.stream().flatMap(map -> map.entrySet().stream()) .collect(Collectors.toMap(entry -> entry.getKey(), entry -> Optional.ofNullable(entry.getValue()))); return flattenedMap; } public Map<String, Object> with_flatMap( final List<Map<String, Object>> listMaps) { Map<String, Object> flattenedMap = listMaps.stream().flatMap(map -> map.entrySet().stream()) .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue())); return flattenedMap; } public Map<String, Object> with_putAll( final List<Map<String, Object>> listMaps) { Map<String, Object> flattedMap = new HashMap<>(); listMaps.stream().forEach(map -> { Map<String, Object> combined = map.entrySet().stream() .collect(Collectors.toMap(entry -> entry.getKey(), entry -> Optional.ofNullable(entry.getValue()))); flattedMap.putAll(combined); }); return flattedMap; } } |
- line 21: Create a
mergeFunction
to set the desired value when duplicate keys are encountered. - line 24: Use the
mergeFunction
when callingCollectors.toMap
method. - line 33: It is the same as line 24 except with the lambda expression.
- line 41: It uses the
Optional
class to handle the map with thenull
value.
5. Create Tests
In this step, I will utilize Junit tests to test the flattening list of maps methods created at step 4.
5.1 Create Demo Data
This is a mocked data model which contains a map object. We will use it to construct a list of maps.
DemoData.java
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | package org.zheng.demo; import java.util.Map; import java.util.Objects; public class DemoData { private Map<String, String> items; private String name; @Override public boolean equals(Object obj) { if ( this == obj) return true ; if (obj == null ) return false ; if (getClass() != obj.getClass()) return false ; DemoData other = (DemoData) obj; return Objects.equals(items, other.items) && Objects.equals(name, other.name); } public Map<String, String> getItems() { return items; } public String getName() { return name; } @Override public int hashCode() { return Objects.hash(items, name); } public void setItems(Map<String, String> items) { this .items = items; } public void setName(String name) { this .name = name; } } |
5.2 Demo FlatStringMaps via Test
In this step, I will create a DemoFlatObjectMapsTest
class which tests the flatten methods created at step 4.2 with mocked testing data.
DemoFlatObjectMapsTest.java
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | package org.zheng.demo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; class DemoFlatStringMapsTest { private DemoFlatStringMaps flatStringMaps = new DemoFlatStringMaps(); private List<Map<String, String>> setupNoDuplicateTestData() { Map<String, String> map1 = new HashMap<>(); map1.put( "map1Key" , "value13" ); Map<String, String> map2 = new HashMap<>(); map2.put( "map2Key" , "value24" ); List<Map<String, String>> maps = new ArrayList<>(); maps.add(map1); maps.add(map2); return maps; } private List<Map<String, String>> setupNullTestData() { Map<String, String> map1 = new HashMap<>(); map1.put( "map1Key" , null ); Map<String, String> map2 = new HashMap<>(); map2.put( "map2Key" , "value24" ); List<Map<String, String>> maps = new ArrayList<>(); maps.add(map1); maps.add(map2); return maps; } private List<Map<String, String>> setupTestData() { Map<String, String> map1 = new HashMap<>(); map1.put( "sameKeySameValue" , "value" ); map1.put( "sameKeyDiffValue" , "value12" ); map1.put( "map1Key" , "value13" ); Map<String, String> map2 = new HashMap<>(); map2.put( "sameKeySameValue" , "value" ); map2.put( "sameKeyDiffValue" , "value23" ); map2.put( "map2Key" , "value24" ); List<Map<String, String>> maps = new ArrayList<>(); maps.add(map1); maps.add(map2); return maps; } private List<DemoData> setupTestDemoDatas() { Map<String, String> map1 = new HashMap<>(); map1.put( "sameKeySameValue" , "value" ); map1.put( "sameKeyDiffValue" , "value12" ); map1.put( "map1Key" , "value13" ); Map<String, String> map2 = new HashMap<>(); map2.put( "sameKeySameValue" , "value" ); map2.put( "sameKeyDiffValue" , "value23" ); map2.put( "map2Key" , "value24" ); Map<String, String> map3 = new HashMap<>(); map3.put( "sameKeySameValue" , "value" ); map3.put( "sameKeyDiffValue" , "value33" ); map3.put( "map3Key" , "value44" ); DemoData demo1 = new DemoData(); demo1.setName( "Mary" ); demo1.setItems(map1); DemoData demo2 = new DemoData(); demo2.setName( "Zheng" ); demo2.setItems(map2); DemoData demo3 = new DemoData(); demo3.setName( "Zheng" ); demo3.setItems(map3); List<DemoData> demoDatas = new ArrayList<>(); demoDatas.add(demo1); demoDatas.add(demo2); demoDatas.add(demo3); return demoDatas; } @Test void test_with_filter_DemoData() { List<DemoData> demoDatas = setupTestDemoDatas(); List<Map<String, String>> maps = demoDatas.stream().filter(demo -> demo.getName().startsWith( "Zheng" )) .map(demo -> demo.getItems()).collect(Collectors.toList()); Map<String, String> flattedMap = flatStringMaps.with_putAll(maps); assertEquals( 4 , flattedMap.size()); // {sameKeyDiffValue=value33, sameKeySameValue=value, map2Key=value24, // map3Key=value44} assertEquals( "value33" , flattedMap.get( "sameKeyDiffValue" )); assertEquals( "value" , flattedMap.get( "sameKeySameValue" )); assertEquals( "value44" , flattedMap.get( "map3Key" )); assertEquals( "value24" , flattedMap.get( "map2Key" )); } @Test void test_with_flatMap() { List<Map<String, String>> maps = setupNoDuplicateTestData(); Map<String, String> flattedMap = flatStringMaps.with_flatMap(maps); assertEquals( 2 , flattedMap.size()); // {map1Key=value13, map2Key=value24} assertEquals( "value13" , flattedMap.get( "map1Key" )); assertEquals( "value24" , flattedMap.get( "map2Key" )); } @Test void test_with_flatMap_andMerge() { List<Map<String, String>> maps = setupTestData(); Map<String, String> flattedMap = flatStringMaps.with_flatMap_andMerge(maps); assertEquals( 4 , flattedMap.size()); // {sameKeyDiffValue=value12, sameKeySameValue=value, map1Key=value13, // map2Key=value24} assertEquals( "value12" , flattedMap.get( "sameKeyDiffValue" )); assertEquals( "value" , flattedMap.get( "sameKeySameValue" )); assertEquals( "value13" , flattedMap.get( "map1Key" )); assertEquals( "value24" , flattedMap.get( "map2Key" )); } @Test void test_with_flatMap_error() { List<Map<String, String>> maps = setupTestData(); assertThrows(IllegalStateException. class , () -> { flatStringMaps.with_flatMap(maps); }); } @Test void test_with_flatMap_null() { List<Map<String, String>> maps = setupNullTestData(); assertThrows(NullPointerException. class , () -> { flatStringMaps.with_flatMap(maps); }); assertThrows(NullPointerException. class , () -> { flatStringMaps.with_putAll(maps); }); } @Test void test_with_putAll() { List<Map<String, String>> maps = setupTestData(); Map<String, String> flattedMap = flatStringMaps.with_putAll(maps); assertEquals( 4 , flattedMap.size()); // {sameKeyDiffValue=value23, sameKeySameValue=value, map1Key=value13, // map2Key=value24} assertEquals( "value23" , flattedMap.get( "sameKeyDiffValue" )); assertEquals( "value" , flattedMap.get( "sameKeySameValue" )); assertEquals( "value13" , flattedMap.get( "map1Key" )); assertEquals( "value24" , flattedMap.get( "map2Key" )); } @Test void test_with_putAll_DemoData() { List<DemoData> demoDatas = setupTestDemoDatas(); List<Map<String, String>> maps = demoDatas.stream().map(demo -> demo.getItems()).collect(Collectors.toList()); Map<String, String> flattedMap = flatStringMaps.with_putAll(maps); assertEquals( 5 , flattedMap.size()); // {sameKeyDiffValue=value33, sameKeySameValue=value, map1Key=value13, // map2Key=value24, map3Key=value44} assertEquals( "value33" , flattedMap.get( "sameKeyDiffValue" )); assertEquals( "value" , flattedMap.get( "sameKeySameValue" )); assertEquals( "value13" , flattedMap.get( "map1Key" )); assertEquals( "value24" , flattedMap.get( "map2Key" )); assertEquals( "value44" , flattedMap.get( "map3Key" )); } } |
- line 18: set up two maps with no duplicate key and no null value. The flatten methods work fine for this mock data.
- line 31: set up two maps, one map has a
null
value. It’s used bytest_with_flatMap_null
which throws an exception. - line 44: set up two maps with duplicate keys:
sameKeySameValue
andsameKeyDiffValue
. It’s used by bothtest_with_flatMap_andMerge works
andtest_with_flatMap_error
. The test results confirm thatCollectors.toMap
with amergeFunction
can handle the duplicate key. - line 61: set up a list of
DemoData
which contains a map object. - line 98, 100, 101:
test_with_filter_DemoData
tests flattening a list of maps created at line 100 and 101 via List ofDemoData
. - line 115:
test_with_flatMap
works when the map has no duplicates and no null value. - line 126:
test_with_flatMap_andMerge
works when the map has nonull
value. - line 141:
test_with_flatMap_error
throws anIllegalStateException
when the map has duplicate keys. - line 151:
test_with_flatMap_null
throws aNullPointerException
when the map has anull
value.
Run the junit tests and all tests are passed but we know that the null
value is not handled as it throws a exception.
5.3 Demo FlatObjectMaps via Test
As you see at step 5.2, if the list of String
maps contain null
values, then it throws a NullPointerException
. In this step, I will create a DemoFlatObjectMapsTest
class which tests the class DemoFlatObjectMaps
created at 4.3. Test confirms that It handles both duplicate key and null
value with the Map<String, Object>
type.
DemoFlatObjectMapsTest.java
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | package org.zheng.demo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; class DemoFlatObjectMapsTest { private DemoFlatObjectMaps flatObjectMaps = new DemoFlatObjectMaps(); private List<Map<String, Object>> queryForList(String s) { final List<Map<String, Object>> result = new ArrayList<>(); Map<String, Object> map = new HashMap<>(); for ( int i = 0 ; i < 10 ; i++) { map = new HashMap<>(); map.put( "key" , "key" + i); map.put( "value" , "value" + i); result.add(map); } map = new HashMap<>(); map.put( "key" , "key1" ); map.put( "value" , "value20" ); result.add(map); map = new HashMap<>(); map.put( "key" , "key2" ); map.put( "value" , "value22" ); result.add(map); return result; } private List<Map<String, Object>> setupNoDupTestData() { Map<String, Object> map1 = new HashMap<>(); map1.put( "map1Key" , "value13" ); map1.put( "map1KeyWithNullValue" , null ); Map<String, Object> map2 = new HashMap<>(); map2.put( "map2Key" , "value24" ); map2.put( "map2KeyWithNullValue" , null ); List<Map<String, Object>> maps = new ArrayList<>(); maps.add(map1); maps.add(map2); return maps; } private List<Map<String, Object>> setupTestData() { Map<String, Object> map1 = new HashMap<>(); map1.put( "sameKeySameValue" , "value" ); map1.put( "sameKeyDiffValue" , "value12" ); map1.put( "map1Key" , "value13" ); map1.put( "map1KeyWithNullValue" , null ); Map<String, Object> map2 = new HashMap<>(); map2.put( "sameKeySameValue" , "value" ); map2.put( "sameKeyDiffValue" , "value23" ); map2.put( "map2Key" , "value24" ); map2.put( "map2KeyWithNullValue" , null ); List<Map<String, Object>> maps = new ArrayList<>(); maps.add(map1); maps.add(map2); return maps; } @Test public void test_grouping_byKey() { List<Map<String, Object>> steps = queryForList( "SELECT key, value FROM table" ); Map<String, List<String>> result1 = steps.stream() .collect(Collectors.groupingBy(k -> String.valueOf(k.get( "key" )), Collectors.mapping(l -> String.valueOf(l.get( "value" )), Collectors.toList()))); result1.entrySet().forEach(e -> System.out.println(e.getKey() + " -> " + e.getValue())); } @SuppressWarnings ( "unchecked" ) @Test void test_with_flatMap_andMerge_optional() { List<Map<String, Object>> maps = setupTestData(); Map<String, Object> flattedMap = flatObjectMaps.with_flatMap_andMerge_optional(maps); assertEquals( 6 , flattedMap.size()); assertEquals( "value" , ((Optional<String>) flattedMap.get( "sameKeySameValue" )).get()); assertEquals( "value12" , ((Optional<String>) flattedMap.get( "sameKeyDiffValue" )).get()); assertEquals( "value13" , ((Optional<String>) flattedMap.get( "map1Key" )).get()); assertEquals( "value24" , ((Optional<String>) flattedMap.get( "map2Key" )).get()); assertTrue(((Optional<String>) flattedMap.get( "map1KeyWithNullValue" )).isEmpty()); assertTrue(((Optional<String>) flattedMap.get( "map2KeyWithNullValue" )).isEmpty()); } @SuppressWarnings ( "unchecked" ) @Test void test_with_flatMap_andMerge_optional_l() { List<Map<String, Object>> maps = setupTestData(); Map<String, Object> flattedMap = flatObjectMaps.with_flatMap_andMerge_optional_l(maps); // {sameKeyDiffValue=Optional[value12], sameKeySameValue=Optional[value], // map1Key=Optional[value13], map2KeyWithNullValue=Optional.empty, // map2Key=Optional[value24], map1KeyWithNullValue=Optional.empty} assertEquals( 6 , flattedMap.size()); assertEquals( "value" , ((Optional<String>) flattedMap.get( "sameKeySameValue" )).get()); assertEquals( "value12" , ((Optional<String>) flattedMap.get( "sameKeyDiffValue" )).get()); assertEquals( "value13" , ((Optional<String>) flattedMap.get( "map1Key" )).get()); assertEquals( "value24" , ((Optional<String>) flattedMap.get( "map2Key" )).get()); assertTrue(((Optional<String>) flattedMap.get( "map1KeyWithNullValue" )).isEmpty()); assertTrue(((Optional<String>) flattedMap.get( "map2KeyWithNullValue" )).isEmpty()); } @Test void test_with_flatMap_error() { List<Map<String, Object>> maps = setupTestData(); assertThrows(IllegalStateException. class , () -> { flatObjectMaps.with_flatMap_optional(maps); }); } @Test void test_with_flatMap_null() { List<Map<String, Object>> maps = setupNoDupTestData(); assertThrows(NullPointerException. class , () -> { flatObjectMaps.with_flatMap(maps); }); } @SuppressWarnings ( "unchecked" ) @Test void test_with_flatMap_optional() { List<Map<String, Object>> maps = setupNoDupTestData(); Map<String, Object> flattedMap = flatObjectMaps.with_flatMap_optional(maps); assertEquals( 4 , flattedMap.size()); // {map1Key=Optional[value13], map2Key=Optional[value24], // map2KeyWithNullValue=Optional.empty, map1KeyWithNullValue=Optional.empty} assertEquals( "value13" , ((Optional<String>) flattedMap.get( "map1Key" )).get()); assertEquals( "value24" , ((Optional<String>) flattedMap.get( "map2Key" )).get()); assertTrue(((Optional<String>) flattedMap.get( "map1KeyWithNullValue" )).isEmpty()); assertTrue(((Optional<String>) flattedMap.get( "map2KeyWithNullValue" )).isEmpty()); } @SuppressWarnings ( "unchecked" ) @Test void test_with_putAll() { List<Map<String, Object>> maps = setupTestData(); Map<String, Object> flattedMap = flatObjectMaps.with_putAll(maps); assertEquals( 6 , flattedMap.size()); assertEquals( "value" , ((Optional<String>) flattedMap.get( "sameKeySameValue" )).get()); assertEquals( "value23" , ((Optional<String>) flattedMap.get( "sameKeyDiffValue" )).get()); assertEquals( "value13" , ((Optional<String>) flattedMap.get( "map1Key" )).get()); assertEquals( "value24" , ((Optional<String>) flattedMap.get( "map2Key" )).get()); assertTrue(((Optional<String>) flattedMap.get( "map1KeyWithNullValue" )).isEmpty()); assertTrue(((Optional<String>) flattedMap.get( "map2KeyWithNullValue" )).isEmpty()); } } |
- line 30, 35: set up 2 duplicate keys and will be used at
test_grouping_byKey
for grouping method. - line 61, 63, 66, 67, 69: set up the duplicate key, null value mock data.
- line 79: this test uses
Collectors.groupingBy
to group the map data based on the same key. As you seen at the outputs, thekey1
has two values:value1
andvalue20
. thekey2
also has two values:value2
andvalue22
which matches the data set up at thequeryForList
method. - line 92:
test_with_flatMap_andMerge_optional
tests that flattening list of maps works for both duplicate key andnull
value. - line 110:
test_with_flatMap_andMerge_optional_l
tests and confirms that flattening list of maps works for both duplicate key and null value.
test_grouping_byKey outputs
01 02 03 04 05 06 07 08 09 10 | key1 -> [value1, value20] key2 -> [value2, value22] key0 -> [value0] key5 -> [value5] key6 -> [value6] key3 -> [value3] key4 -> [value4] key9 -> [value9] key7 -> [value7] key8 -> [value8] |
6. Handling Duplicate Keys
As you see at test_with_flatMap_error
, if the list of maps contains duplicate keys, then it will throw IllegalStateException
. We handle it by utilizing the Collectors.toMap
method by passing mergeFunction
.
with_flatMap_andMerge_optional.java
1 2 3 4 5 6 7 8 9 | public Map<String, Object> with_flatMap_andMerge_optional( final List<Map<String, Object>> listMaps) { BinaryOperator<Object> mergeFunction = (existingValue, newValue) -> mergeValue(existingValue, newValue); Map<String, Object> flattenedMap = listMaps.stream().flatMap(map -> map.entrySet().stream()).collect(Collectors .toMap(entry -> entry.getKey(), entry -> Optional.ofNullable(entry.getValue()), mergeFunction)); return flattenedMap; } } |
7. Handling null Values
As you see at test_with_flatMap_null
, if the list of maps contains a null
value, then it throw NullPointerException
. We will use the Optional.ofNullable
to avoid the NullPointerException
.
with_flatMap_andMerge_optional.java
1 2 3 4 5 6 7 8 9 | public Map<String, Object> with_flatMap_andMerge_optional( final List<Map<String, Object>> listMaps) { BinaryOperator<Object> mergeFunction = (existingValue, newValue) -> mergeValue(existingValue, newValue); Map<String, Object> flattenedMap = listMaps.stream().flatMap(map -> map.entrySet().stream()).collect(Collectors .toMap(entry -> entry.getKey(), entry -> Optional.ofNullable(entry.getValue()), mergeFunction)); return flattenedMap; } |
Here is the screenshot of Junit tests results.
8. Conclusion
In this tutorial, l created two Java classes to flatten a stream of maps into a single map with Stream.flatMap()
and Collectors.toMap()
methods. In order to demonstrate java flatten stream map usage, I mocked test data in the Junit tests and examined each transformation. This example also showed how null
and duplicate values are handled during the transformation.
9. Download
This was an example of flatten a Stream of Maps to a single map in a Java maven project.
You can download the full source code of this example here: Flatten a Stream of Maps to a Single Map in Java