Apache Commons Collection – MultiValuedMap

The first time I needed a one-to-many map in Java, I reached for Map<String, List<String>> and wrote the same computeIfAbsent boilerplate I’d written a dozen times before. Apache Commons Collections’ MultiValuedMap exists specifically to kill that boilerplate — it’s a Map variant that lets a single key hold multiple values natively, without you manually managing the inner list. This post covers what it actually buys you over rolling your own, how it stacks up against Guava’s Multimap, and a realistic use case: grouping form-validation errors by field name.

Here’s the minimal example to see the API shape:

import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;

public class MultiValuedMapDemo {
    public static void main(String[] args) {
        MultiValuedMap<String, String> multiValuedMap = new ArrayListValuedHashMap<>();
        multiValuedMap.put("Key1", "Value1");
        multiValuedMap.put("Key1", "Value2");
        multiValuedMap.put("Key2", "Value3");
        System.out.println(multiValuedMap.get("Key1"));
    }
}

How the Code Works

ArrayListValuedHashMap is the concrete implementation backing each key with an ArrayList of values (duplicates allowed, insertion order preserved). Calling put("Key1", "Value1") followed by put("Key1", "Value2") doesn’t overwrite — unlike a normal Map.put() — it appends to the internal collection for that key. get("Key1") returns a live, unmodifiable view of all values stored under that key, in the order they were added.

Sample output:

[Value1, Value2]

MultiValuedMap vs Guava Multimap vs Map<K, List<V>>

If you’ve used Guava, Multimap will look familiar — both libraries solve the same problem, but with different defaults and dependency footprints. Here’s how the three options actually compare:

AspectCommons MultiValuedMapGuava MultimapMap<K, List<V>> + computeIfAbsent
Dependencycommons-collections4guavaJDK only, no dependency
Duplicate values per keyYes (ArrayListValuedHashMap) or no (SetValuedHashMap)Yes (ArrayListMultimap) or no (HashMultimap)Depends entirely on what collection you choose manually
Empty key cleanupAutomatic — removing the last value removes the keyAutomatic — same behaviorManual — you must remove the key yourself or it lingers as an empty list
Null-key handlingImplementation-dependent, generally permissiveDisallowed in immutable variants, allowed in mutable onesWhatever the backing Map allows (HashMap allows one null key)
API ergonomics for groupingDirect: put() appends automaticallyDirect: put() appends automaticallyRequires explicit computeIfAbsent(key, k -> new ArrayList<>()).add(value)
Immutable viewsLimited supportStrong support (ImmutableListMultimap, etc.)Wrap manually with Collections.unmodifiableMap
Maturity / ecosystemStable, less actively developedVery actively maintained, widely usedAlways available, zero extra learning curve

What surprised me when I actually benchmarked this for a side project: the JDK-only computeIfAbsent approach isn’t meaningfully slower for typical web-app workloads, so the decision usually comes down to whether you already have Guava or Commons Collections on the classpath — pulling in a new dependency just for this one data structure is rarely worth it if you don’t have one already.

Real-World Use Case: Grouping Form-Validation Errors by Field

A one-to-many map’s most common real use isn’t a toy example — it’s collecting multiple validation errors per form field so you can render them all instead of stopping at the first failure. Here’s a small validator that returns every error per field, not just the first:

import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;

public class RegistrationValidator {

    public MultiValuedMap<String, String> validate(RegistrationForm form) {
        MultiValuedMap<String, String> errors = new ArrayListValuedHashMap<>();

        if (form.email() == null || form.email().isBlank()) {
            errors.put("email", "Email is required");
        } else if (!form.email().contains("@")) {
            errors.put("email", "Email must contain an @ symbol");
        }

        if (form.password() == null || form.password().length() < 8) {
            errors.put("password", "Password must be at least 8 characters");
        }
        if (form.password() != null && form.password().chars().noneMatch(Character::isDigit)) {
            errors.put("password", "Password must contain at least one digit");
        }

        if (form.age() < 13) {
            errors.put("age", "You must be at least 13 years old");
        }

        return errors;
    }

    record RegistrationForm(String email, String password, int age) {}
}

Calling it with a deliberately bad form:

var validator = new RegistrationValidator();
var form = new RegistrationValidator.RegistrationForm("not-an-email", "short", 10);
var errors = validator.validate(form);

for (String field : errors.keySet()) {
    System.out.println(field + ": " + errors.get(field));
}

How the Code Works

Each validation rule that fails calls errors.put(field, message) independently — the password field can accumulate two separate messages (“too short” and “no digit”) without any of the rules needing to know about each other or coordinate a shared list. That decoupling is the actual value proposition: each validation rule is a one-line, side-effect-free check that appends to a shared structure, and the structure itself handles the aggregation.

Sample output:

email: [Email must contain an @ symbol]
password: [Password must be at least 8 characters, Password must contain at least one digit]
age: [You must be at least 13 years old]

Note that email only has one message, not two — the else if short-circuits once the field is found blank, since reporting “must contain @” on top of “is required” would be noise rather than help.

FAQs

Is MultiValuedMap thread-safe?

No, the standard implementations (ArrayListValuedHashMap, HashSetValuedHashMap) are not synchronized, same as HashMap. Wrap with MultiMapUtils.unmodifiableMultiValuedMap() for read-only safety, or handle synchronization externally for concurrent writes — there’s no concurrent variant analogous to ConcurrentHashMap in Commons Collections.

Does MultiValuedMap implement java.util.Map?

No. MultiValuedMap<K, V> is a separate Commons Collections interface, not a java.util.Map subtype — its get() returns a Collection<V> instead of a single V, which is incompatible with the standard Map contract. You can get a Map<K, Collection<V>> view via asMap() if you need to interoperate with code expecting a regular Map.

When should I use SetValuedHashMap instead of ArrayListValuedHashMap?

Use HashSetValuedHashMap when duplicate values per key don’t make sense for your domain — for example, tagging a document with categories, where assigning the same category twice should be a no-op rather than stored twice. Use ArrayListValuedHashMap (as in the validation example above) when duplicates and insertion order both matter, such as accumulating multiple error messages.

Why not just use Guava if it’s more actively maintained?

If you’re starting a new project with no existing dependency on either library, Guava’s Multimap is the better default purely on maintenance activity and the strength of its immutable collection variants. This post uses Commons Collections because plenty of legacy enterprise codebases (the kind this site covers) already depend on it for other utilities, and swapping libraries just for one map type isn’t worth the dependency churn.

See Also

Conclusion

MultiValuedMap isn’t a must-have — a Map<K, List<V>> with computeIfAbsent gets you 90% of the way with zero extra dependencies. But if Commons Collections is already on your classpath, or you’re maintaining code that already grouped data into Map<K, List<V>> manually, switching to ArrayListValuedHashMap removes a recurring source of null-check and initialization bugs around the inner collection. The form-validation pattern above is the use case I reach for it most: any time you’re collecting zero-or-more results keyed by something, instead of stopping at the first match.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.