How To Use Advanced Map Operations in Java

Getting your Trinity Audio player ready...

In Java applications, java.util.Mapis 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 putAllfails 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!