Getting your Trinity Audio player ready...
|
In Java applications, java.util.Map
is far more than a simple key-value store. Whether you’re managing configuration files, forming API responses, or calculating grouped statistics, working with structured data is essential.
At work, I often need to use advanced Map
operations. When they fit the need, I apply them. Hence, sharing the learnings in this article.
Grouping and Aggregating Data from a List of Maps
A common requirement is to group a list of records, each stored as a Map<String, Object>
, by a specific attribute and then aggregate another attribute (e.g., sum, average, or count) within each group.
Scenario
You have a list of products, each stored as a Map<String, Object>
. You want to group them by category and calculate the average price per category.
Code
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class MapAggregationExample {
public static void main(String[] args) {
List<Map<String, Object>> products = Arrays.asList(
createProduct("Laptop", "Electronics", 1200.00),
createProduct("Mouse", "Electronics", 25.00),
createProduct("Keyboard", "Electronics", 75.00),
createProduct("T-Shirt", "Apparel", 20.00),
createProduct("Jeans", "Apparel", 60.00),
createProduct("Book - Java", "Books", 45.00),
createProduct("Book - Design Patterns", "Books", 55.00)
);
Map<String, Double> averagePriceByCategory = products.stream()
.collect(Collectors.groupingBy(
product -> (String) product.get("category"),
Collectors.averagingDouble(product -> (Double) product.get("price"))
));
System.out.println("Average price by category:");
averagePriceByCategory.forEach((category, avgPrice) ->
System.out.printf(" %s: $%.2f%n", category, avgPrice));
}
private static Map<String, Object> createProduct(String name, String category, Double price) {
Map<String, Object> product = new java.util.HashMap<>();
product.put("name", name);
product.put("category", category);
product.put("price", price);
return product;
}
}
Output
Average price by category:
Electronics: $433.33
Apparel: $40.00
Books: $50.00
We start with a list of product maps. Each map holds a category
and a price
.
Using Java’s Stream API, we group products by category and calculate the average price in each group using Collectors.averagingDouble.
This gives a Map<String, Double>
with category names as keys and their average prices as values.
How it helps
This approach is compact, readable, and practical for building reports or dashboards.
Casting (like (String)
and (Double)
) assumes that data is always clean and well-typed. That’s risky. In production code, use validation or, better yet — define a proper Product class instead of using Map<String, Double>
.
Deep Merging of Nested Maps
Merging two maps is common in configuration management or API updates, but a simple putAll
fails when maps are nested. A recursive merge is needed to combine nested structures intelligently.
Scenario
Merge a base configuration map with an update map, where:
— Update map values override base map values for matching keys.
— Nested maps are merged recursively.
— Non-map values in the update map replace those in the base map.
Code
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@SuppressWarnings("unchecked")
public class DeepMergeMapsExample {
public static Map<String, Object> deepMerge(Map<String, Object> baseMap, Map<String, Object> updateMap) {
Map<String, Object> result = new LinkedHashMap<>(baseMap);
updateMap.forEach((key, updateValue) -> {
if (result.containsKey(key) && result.get(key) instanceof Map && updateValue instanceof Map) {
result.put(key, deepMerge((Map<String, Object>) result.get(key), (Map<String, Object>) updateValue));
} else {
result.put(key, updateValue);
}
});
return result;
}
public static void main(String[] args) {
Map<String, Object> baseConfig = new HashMap<>();
baseConfig.put("applicationName", "BaseApp");
baseConfig.put("version", "1.0");
Map<String, Object> baseDbSettings = new HashMap<>();
baseDbSettings.put("host", "localhost");
baseDbSettings.put("port", 5432);
baseDbSettings.put("username", "base_user");
baseConfig.put("database", baseDbSettings);
Map<String, Object> baseApiCalls = new HashMap<>();
baseApiCalls.put("timeout", 5000);
baseConfig.put("api", baseApiCalls);
Map<String, Object> updateConfig = new HashMap<>();
updateConfig.put("version", "1.1");
Map<String, Object> updateDbSettings = new HashMap<>();
updateDbSettings.put("port", 5433);
updateDbSettings.put("password", "updated_pass");
updateConfig.put("database", updateDbSettings);
updateConfig.put("logLevel", "DEBUG");
Map<String, Object> newFeatureSettings = new HashMap<>();
newFeatureSettings.put("enabled", true);
updateConfig.put("newFeature", newFeatureSettings);
System.out.println("Base Config: " + baseConfig);
System.out.println("Update Config: " + updateConfig);
Map<String, Object> mergedConfig = deepMerge(baseConfig, updateConfig);
System.out.println("\nMerged Config: " + mergedConfig);
}
}
Output
Merged Config: {
applicationName=BaseApp,
version=1.1,
database={host=localhost, port=5433, username=base_user, password=updated_pass},
api={timeout=5000},
logLevel=DEBUG,
newFeature={enabled=true}
}
We start with two maps: base and update. We clone the base into a new LinkedHashMap
(to preserve order).
For each key in the update if both base and update values are maps, we merge them recursively and if not, we overwrite with the update’s value.
How it helps
This is perfect for merging nested configuration files or JSON responses. You get a clean, recursive way to merge without mutating the original inputs. For extra safety in multithreaded environments, consider using ConcurrentHashMap
, or make the maps immutable.
Transforming a Flat Map to a Nested Map
Flat maps with delimited keys (e.g., user.address.city) are common in configuration files or legacy APIs. Transforming these into nested maps is a powerful technique for restructuring data.
Scenario
You’re working with a flat map where keys use dot-notation: "user.address.city" → "Gotham"
. You want to convert this into a proper nested map structure.
Code
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@SuppressWarnings("unchecked")
public class FlattenedToNestedMapExample {
public static Map<String, Object> unflatten(Map<String, Object> flatMap) {
Map<String, Object> nestedMap = new LinkedHashMap<>();
flatMap.forEach((flatKey, value) -> {
String[] keys = flatKey.split("\\.");
Map<String, Object> currentLevel = nestedMap;
for (int i = 0; i < keys.length - 1; i++) {
currentLevel = (Map<String, Object>) currentLevel.computeIfAbsent(keys[i], k -> new LinkedHashMap<>());
}
currentLevel.put(keys[keys.length - 1], value);
});
return nestedMap;
}
public static void main(String[] args) {
Map<String, Object> flatData = new LinkedHashMap<>();
flatData.put("user.name", "Bruce");
flatData.put("user.email", "bruce@wayne.com");
flatData.put("user.address.street", "4587 Main St");
flatData.put("user.address.city", "Gotham");
flatData.put("user.address.zip", "12345");
flatData.put("config.theme", "dark");
flatData.put("config.notifications.enabled", true);
System.out.println("Flat Data: " + flatData);
Map<String, Object> nestedData = unflatten(flatData);
System.out.println("\nNested Data: " + nestedData);
Map<String, Object> userMap = (Map<String, Object>) nestedData.get("user");
if (userMap != null) {
Map<String, Object> addressMap = (Map<String, Object>) userMap.get("address");
if (addressMap != null) {
System.out.println("\nUser City: " + addressMap.get("city")); // Gotham
}
}
}
}
Output
Nested Data: {
user={name=Bruce, email=bruce@wayne.com, address={street=4587 Main St, city=Gotham, zip=12345}},
config={theme=dark, notifications={enabled=true}}
}
User City: Gotham
Each key in the flat map, like "user.address.city"
, is split by dots into parts like ["user", "address", "city"]
. These parts define a path in the nested structure.
The algorithm walks through this path, and at each level, it either moves deeper into the map or creates a new nested LinkedHashMap
using computeIfAbsent
. Once it reaches the final part of the path, it inserts the value.
This process reconstructs the full nested structure from a flattened key format, preserving hierarchy and readability.
How it helps
If you’ve ever worked with .properties
files or APIs that flatten JSON, you’ll run into this.
This method makes the data usable again, no manual get(get(get(...)))
logic. It’s clean, extendable, and avoids boilerplate.
Closing Thoughts
Maps are powerful, especially when you treat them as dynamic data structures instead of just dumb key-value stores.
Whether you’re grouping records, merging configurations, or untangling flat keys into real structure, these patterns show how far you can go with Map<String, Object>
and a bit of Stream API and recursion.
Lastly, use typed classes when possible, validate before casting, and wrap utilities like these in reusable methods or services.
If this article provided you with value, please support me by buying me a coffee—only if you can afford it. Thank you!