The Apache Commons Collections package contains types that extend and augment the Java Collections Framework
—
Multi-valued maps allow multiple values to be associated with each key. Unlike regular maps which have a 1:1 key-value relationship, multi-valued maps support 1:many relationships where each key can map to a collection of values.
The primary interface for multi-valued maps that associates collections of values with keys.
import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
import java.util.Collection;
MultiValuedMap<String, String> multiMap = new ArrayListValuedHashMap<>();
// Add multiple values for the same key
multiMap.put("colors", "red");
multiMap.put("colors", "green");
multiMap.put("colors", "blue");
multiMap.put("animals", "cat");
multiMap.put("animals", "dog");
// Get all values for a key (returns Collection<V>)
Collection<String> colors = multiMap.get("colors"); // ["red", "green", "blue"]
Collection<String> animals = multiMap.get("animals"); // ["cat", "dog"]
Collection<String> empty = multiMap.get("plants"); // [] (empty collection)
// Check presence
boolean hasColors = multiMap.containsKey("colors"); // true
boolean hasRed = multiMap.containsValue("red"); // true
boolean hasMapping = multiMap.containsMapping("colors", "red"); // true
// Size operations
int totalMappings = multiMap.size(); // 5 (total key-value pairs)
int uniqueKeys = multiMap.keySet().size(); // 2 (colors, animals)
boolean isEmpty = multiMap.isEmpty(); // false
// Remove operations
boolean removed = multiMap.removeMapping("colors", "red"); // Remove specific mapping
Collection<String> removedColors = multiMap.remove("colors"); // Remove all values for keyA multi-valued map where each key maps to a List of values, preserving order and allowing duplicates.
import org.apache.commons.collections4.ListValuedMap;
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
import java.util.List;
ListValuedMap<String, Integer> scores = new ArrayListValuedHashMap<>();
// Add scores in order (duplicates allowed)
scores.put("Alice", 85);
scores.put("Alice", 92);
scores.put("Alice", 88);
scores.put("Alice", 92); // Duplicate allowed
// Get as List (preserves order and duplicates)
List<Integer> aliceScores = scores.get("Alice"); // [85, 92, 88, 92]
// List-specific operations on values
aliceScores.add(95); // Modifies underlying multimap
int firstScore = aliceScores.get(0); // 85
int lastScore = aliceScores.get(aliceScores.size() - 1); // 95A multi-valued map where each key maps to a Set of values, ensuring uniqueness.
import org.apache.commons.collections4.SetValuedMap;
import org.apache.commons.collections4.multimap.HashSetValuedHashMap;
import java.util.Set;
SetValuedMap<String, String> permissions = new HashSetValuedHashMap<>();
// Add permissions (duplicates automatically ignored)
permissions.put("admin", "read");
permissions.put("admin", "write");
permissions.put("admin", "delete");
permissions.put("admin", "read"); // Duplicate ignored
// Get as Set (no duplicates)
Set<String> adminPerms = permissions.get("admin"); // ["read", "write", "delete"]
// Set-specific operations
boolean hasReadPerm = adminPerms.contains("read"); // true
int uniquePerms = adminPerms.size(); // 3HashMap-based implementation that stores values in ArrayList collections.
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
import java.util.Arrays;
import java.util.List;
ArrayListValuedHashMap<String, String> topics = new ArrayListValuedHashMap<>();
// Bulk operations
topics.putAll("java", Arrays.asList("collections", "streams", "generics"));
topics.putAll("python", Arrays.asList("lists", "dictionaries", "comprehensions"));
// Values maintain insertion order and allow duplicates
topics.put("java", "collections"); // Duplicate allowed
List<String> javaTopics = topics.get("java"); // ["collections", "streams", "generics", "collections"]
// Capacity optimization for known sizes
ArrayListValuedHashMap<String, Integer> optimized = new ArrayListValuedHashMap<>();
// Internal ArrayLists start with default capacityHashMap-based implementation that stores values in HashSet collections.
import org.apache.commons.collections4.multimap.HashSetValuedHashMap;
import java.util.Arrays;
import java.util.Set;
HashSetValuedHashMap<String, String> tags = new HashSetValuedHashMap<>();
// Bulk operations with automatic deduplication
tags.putAll("article1", Arrays.asList("java", "tutorial", "beginner", "java")); // Duplicate "java" ignored
tags.putAll("article2", Arrays.asList("python", "advanced", "tutorial"));
// Values are unique within each key
Set<String> article1Tags = tags.get("article1"); // ["java", "tutorial", "beginner"]
// Fast contains operations (Set performance)
boolean hasJavaTag = tags.containsMapping("article1", "java"); // true - O(1) average caseMultiValuedMap<String, String> contacts = new ArrayListValuedHashMap<>();
// Single value additions
contacts.put("John", "john@work.com");
contacts.put("John", "john@personal.com");
contacts.put("John", "john@mobile.com");
// Bulk additions
contacts.putAll("Jane", Arrays.asList("jane@work.com", "jane@home.com"));
// Add to existing collection
Collection<String> johnEmails = contacts.get("John");
johnEmails.add("john@backup.com"); // Modifies underlying multimap
// Conditional addition
if (!contacts.containsMapping("John", "john@spam.com")) {
contacts.put("John", "john@spam.com");
}// Remove specific key-value mapping
boolean removed = contacts.removeMapping("John", "john@mobile.com"); // true
boolean notRemoved = contacts.removeMapping("John", "nonexistent"); // false
// Remove all values for a key
Collection<String> janeEmails = contacts.remove("Jane"); // Returns and removes all values
// janeEmails = ["jane@work.com", "jane@home.com"]
// Remove through collection view
Collection<String> johnEmails = contacts.get("John");
johnEmails.remove("john@work.com"); // Modifies underlying multimap
// Clear all mappings
contacts.clear();MultiValuedMap<String, Integer> studentGrades = new ArrayListValuedHashMap<>();
studentGrades.putAll("Alice", Arrays.asList(85, 92, 78, 95));
studentGrades.putAll("Bob", Arrays.asList(88, 76, 82));
// Key iteration
for (String student : studentGrades.keySet()) {
System.out.println("Student: " + student);
}
// Value iteration (all values)
for (Integer grade : studentGrades.values()) {
System.out.println("Grade: " + grade);
}
// Entry iteration (key-value pairs)
for (Map.Entry<String, Integer> entry : studentGrades.entries()) {
System.out.println(entry.getKey() + " scored " + entry.getValue());
}
// Key-collection iteration
for (Map.Entry<String, Collection<Integer>> entry : studentGrades.asMap().entrySet()) {
String student = entry.getKey();
Collection<Integer> grades = entry.getValue();
double average = grades.stream().mapToInt(Integer::intValue).average().orElse(0.0);
System.out.println(student + " average: " + average);
}import java.util.stream.Collectors;
public class StudentManager {
private final MultiValuedMap<String, Student> studentsByGrade = new ArrayListValuedHashMap<>();
private final MultiValuedMap<String, Student> studentsBySubject = new HashSetValuedHashMap<>();
public void addStudent(Student student) {
// Group by grade level
studentsByGrade.put(student.getGrade(), student);
// Group by subjects (Set semantics - each student appears once per subject)
for (String subject : student.getSubjects()) {
studentsBySubject.put(subject, student);
}
}
public List<Student> getStudentsInGrade(String grade) {
return new ArrayList<>(studentsByGrade.get(grade));
}
public Set<Student> getStudentsInSubject(String subject) {
return new HashSet<>(studentsBySubject.get(subject));
}
public Map<String, Long> getGradeDistribution() {
return studentsByGrade.asMap().entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> (long) entry.getValue().size()
));
}
}public class ApplicationConfig {
private final MultiValuedMap<String, String> properties = new ArrayListValuedHashMap<>();
public void loadConfig(Properties props) {
props.forEach((key, value) -> {
String keyStr = key.toString();
String valueStr = value.toString();
// Support comma-separated values
if (valueStr.contains(",")) {
String[] values = valueStr.split(",");
for (String v : values) {
properties.put(keyStr, v.trim());
}
} else {
properties.put(keyStr, valueStr);
}
});
}
public List<String> getPropertyValues(String key) {
return new ArrayList<>(properties.get(key));
}
public String getFirstPropertyValue(String key) {
Collection<String> values = properties.get(key);
return values.isEmpty() ? null : values.iterator().next();
}
public void addPropertyValue(String key, String value) {
properties.put(key, value);
}
public boolean hasProperty(String key) {
return properties.containsKey(key);
}
public boolean hasPropertyValue(String key, String value) {
return properties.containsMapping(key, value);
}
}public class DirectedGraph<T> {
private final MultiValuedMap<T, T> adjacencyList = new HashSetValuedHashMap<>();
public void addEdge(T from, T to) {
adjacencyList.put(from, to);
}
public void removeEdge(T from, T to) {
adjacencyList.removeMapping(from, to);
}
public Set<T> getNeighbors(T vertex) {
return new HashSet<>(adjacencyList.get(vertex));
}
public Set<T> getAllVertices() {
Set<T> vertices = new HashSet<>(adjacencyList.keySet());
vertices.addAll(adjacencyList.values());
return vertices;
}
public int getOutDegree(T vertex) {
return adjacencyList.get(vertex).size();
}
public int getInDegree(T vertex) {
return (int) adjacencyList.entries().stream()
.filter(entry -> entry.getValue().equals(vertex))
.count();
}
public boolean hasPath(T from, T to) {
if (from.equals(to)) return true;
Set<T> visited = new HashSet<>();
Queue<T> queue = new LinkedList<>();
queue.offer(from);
visited.add(from);
while (!queue.isEmpty()) {
T current = queue.poll();
for (T neighbor : getNeighbors(current)) {
if (neighbor.equals(to)) return true;
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
}
}
}
return false;
}
}public class InvertedIndex {
private final MultiValuedMap<String, Document> termToDocuments = new HashSetValuedHashMap<>();
private final MultiValuedMap<Document, String> documentToTerms = new HashSetValuedHashMap<>();
public void addDocument(Document document) {
Set<String> terms = tokenize(document.getContent());
for (String term : terms) {
termToDocuments.put(term.toLowerCase(), document);
documentToTerms.put(document, term.toLowerCase());
}
}
public Set<Document> search(String term) {
return new HashSet<>(termToDocuments.get(term.toLowerCase()));
}
public Set<Document> searchMultiple(String... terms) {
if (terms.length == 0) return Collections.emptySet();
Set<Document> result = search(terms[0]);
for (int i = 1; i < terms.length; i++) {
result.retainAll(search(terms[i])); // Intersection
}
return result;
}
public Set<Document> searchAny(String... terms) {
Set<Document> result = new HashSet<>();
for (String term : terms) {
result.addAll(search(term)); // Union
}
return result;
}
public void removeDocument(Document document) {
Collection<String> terms = documentToTerms.remove(document);
for (String term : terms) {
termToDocuments.removeMapping(term, document);
}
}
private Set<String> tokenize(String content) {
// Simple tokenization - in practice, use proper text processing
return Arrays.stream(content.toLowerCase().split("\\W+"))
.filter(s -> !s.isEmpty())
.collect(Collectors.toSet());
}
}import org.apache.commons.collections4.MultiMapUtils;
// Create empty multimaps
MultiValuedMap<String, String> listMap = MultiMapUtils.newListValuedHashMap();
MultiValuedMap<String, String> setMap = MultiMapUtils.newSetValuedHashMap();
// Check if empty (null-safe)
boolean isEmpty = MultiMapUtils.isEmpty(null); // true
boolean isNotEmpty = MultiMapUtils.isEmpty(listMap); // false (depends on content)
// Create unmodifiable views
MultiValuedMap<String, String> unmodifiable = MultiMapUtils.unmodifiableMultiValuedMap(listMap);
// Create empty immutable multimap
MultiValuedMap<String, String> empty = MultiMapUtils.emptyMultiValuedMap();
// Transform multimaps
Transformer<String, String> upperCase = String::toUpperCase;
Transformer<String, String> addPrefix = s -> "prefix_" + s;
MultiValuedMap<String, String> transformed = MultiMapUtils.transformedMultiValuedMap(
listMap,
upperCase, // Key transformer
addPrefix // Value transformer
);MultiValuedMap<String, Integer> scores = new ArrayListValuedHashMap<>();
scores.putAll("Alice", Arrays.asList(85, 92, 78));
scores.putAll("Bob", Arrays.asList(88, 76));
// Get as regular Map<K, Collection<V>>
Map<String, Collection<Integer>> asMap = scores.asMap();
// Modifications through map view affect original
asMap.get("Alice").add(95); // Adds to original multimap
Collection<Integer> bobScores = asMap.remove("Bob"); // Removes from original
// Create defensive copies
Map<String, List<Integer>> copyMap = new HashMap<>();
for (Map.Entry<String, Collection<Integer>> entry : asMap.entrySet()) {
copyMap.put(entry.getKey(), new ArrayList<>(entry.getValue()));
}// Use ArrayListValuedHashMap when:
// - Order of values matters
// - Duplicates are needed
// - Values are accessed by index
ListValuedMap<String, String> orderedWithDuplicates = new ArrayListValuedHashMap<>();
// Use HashSetValuedHashMap when:
// - Uniqueness of values is required
// - Fast contains() operations needed
// - Order doesn't matter
SetValuedMap<String, String> uniqueValues = new HashSetValuedHashMap<>();// Memory efficient for sparse data (few keys, many values per key)
MultiValuedMap<String, String> sparse = new ArrayListValuedHashMap<>();
sparse.putAll("key1", Arrays.asList("v1", "v2", "v3", /* ... many values ... */));
// Less efficient for dense data (many keys, few values per key)
MultiValuedMap<String, String> dense = new ArrayListValuedHashMap<>();
for (int i = 0; i < 1000; i++) {
dense.put("key" + i, "value" + i); // Creates many small collections
}
// For dense data, consider regular Map<K, V> instead
Map<String, String> regularMap = new HashMap<>();MultiValuedMap<String, Integer> numbers = new ArrayListValuedHashMap<>();
// Efficient bulk addition
List<Integer> manyNumbers = IntStream.range(1, 1000).boxed().collect(Collectors.toList());
numbers.putAll("range", manyNumbers); // Single operation
// Less efficient individual additions
for (int i = 1; i < 1000; i++) {
numbers.put("range2", i); // Multiple operations, more overhead
}
// Efficient collection manipulation
Collection<Integer> existing = numbers.get("range");
existing.addAll(Arrays.asList(1001, 1002, 1003)); // Direct collection modificationMultiValuedMaps are not thread-safe by default. For concurrent access:
// Option 1: External synchronization
MultiValuedMap<String, String> multiMap = new ArrayListValuedHashMap<>();
MultiValuedMap<String, String> syncMultiMap = MultiMapUtils.synchronizedMultiValuedMap(multiMap);
// Option 2: Custom concurrent wrapper
public class ConcurrentMultiValuedMap<K, V> {
private final MultiValuedMap<K, V> map = new ArrayListValuedHashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public boolean put(K key, V value) {
lock.writeLock().lock();
try {
return map.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
public Collection<V> get(K key) {
lock.readLock().lock();
try {
return new ArrayList<>(map.get(key)); // Defensive copy
} finally {
lock.readLock().unlock();
}
}
}
// Option 3: Use ConcurrentHashMap with CopyOnWriteArrayList
Map<String, Collection<String>> concurrent = new ConcurrentHashMap<>();
String key = "example";
concurrent.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add("value");Multi-valued maps provide powerful abstractions for one-to-many relationships and are particularly useful for grouping, classification, and graph-like data structures. Choose between List and Set semantics based on whether you need ordering/duplicates or uniqueness guarantees.
Install with Tessl CLI
npx tessl i tessl/maven-org-apache-commons--commons-collections4