The Apache Commons Collections package contains types that extend and augment the Java Collections Framework
—
Bidirectional maps allow efficient lookup in both directions - from key to value and from value to key. They maintain 1:1 relationships between keys and values, ensuring that both keys and values are unique within the map.
The primary interface for bidirectional maps.
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
BidiMap<String, Integer> bidiMap = new DualHashBidiMap<>();
// Standard map operations (key -> value)
bidiMap.put("one", 1);
bidiMap.put("two", 2);
bidiMap.put("three", 3);
Integer value = bidiMap.get("two"); // Returns 2
// Reverse lookup operations (value -> key)
String key = bidiMap.getKey(2); // Returns "two"
String removedKey = bidiMap.removeValue(3); // Removes and returns "three"
// Get inverse view
BidiMap<Integer, String> inverse = bidiMap.inverseBidiMap();
String keyFromInverse = inverse.get(1); // Returns "one"
// Both views reflect the same underlying data
bidiMap.put("four", 4);
Integer newValue = inverse.get("four"); // Returns 4A bidirectional map that maintains insertion order.
import org.apache.commons.collections4.OrderedBidiMap;
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
import org.apache.commons.collections4.OrderedMapIterator;
OrderedBidiMap<String, Integer> orderedBidi = new DualLinkedHashBidiMap<>();
orderedBidi.put("first", 1);
orderedBidi.put("second", 2);
orderedBidi.put("third", 3);
// Navigate in insertion order
String firstKey = orderedBidi.firstKey(); // Returns "first"
String lastKey = orderedBidi.lastKey(); // Returns "third"
String nextKey = orderedBidi.nextKey("first"); // Returns "second"
String prevKey = orderedBidi.previousKey("third"); // Returns "second"
// Ordered iteration
OrderedMapIterator<String, Integer> iterator = orderedBidi.mapIterator();
while (iterator.hasNext()) {
String key = iterator.next();
Integer value = iterator.getValue();
System.out.println(key + " -> " + value);
}A bidirectional map that maintains elements in sorted order.
import org.apache.commons.collections4.SortedBidiMap;
import org.apache.commons.collections4.bidimap.DualTreeBidiMap;
import java.util.Comparator;
// Natural ordering
SortedBidiMap<String, Integer> sortedBidi = new DualTreeBidiMap<>();
sortedBidi.put("zebra", 26);
sortedBidi.put("alpha", 1);
sortedBidi.put("beta", 2);
// Elements are maintained in sorted order
String firstKey = sortedBidi.firstKey(); // Returns "alpha"
String lastKey = sortedBidi.lastKey(); // Returns "zebra"
// Custom comparators for keys and values
Comparator<String> keyComp = Comparator.reverseOrder();
Comparator<Integer> valueComp = Comparator.naturalOrder();
SortedBidiMap<String, Integer> customSorted = new DualTreeBidiMap<>(keyComp, valueComp);Hash-based implementation using two HashMap instances for O(1) performance.
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import java.util.Map;
import java.util.HashMap;
// Create empty bidirectional map
DualHashBidiMap<String, String> countryCapital = new DualHashBidiMap<>();
// Populate with data
countryCapital.put("USA", "Washington");
countryCapital.put("France", "Paris");
countryCapital.put("Japan", "Tokyo");
countryCapital.put("Germany", "Berlin");
// Efficient bidirectional lookups
String capital = countryCapital.get("France"); // "Paris"
String country = countryCapital.getKey("Tokyo"); // "Japan"
// Create from existing map
Map<String, String> existingMap = new HashMap<>();
existingMap.put("Italy", "Rome");
existingMap.put("Spain", "Madrid");
DualHashBidiMap<String, String> fromMap = new DualHashBidiMap<>(existingMap);
// Duplicate value detection
try {
countryCapital.put("UK", "Paris"); // Throws IllegalArgumentException
} catch (IllegalArgumentException e) {
System.out.println("Paris is already mapped to France");
}LinkedHashMap-based implementation that maintains insertion order.
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
DualLinkedHashBidiMap<Integer, String> priorities = new DualLinkedHashBidiMap<>();
// Add in specific order
priorities.put(1, "High");
priorities.put(2, "Medium");
priorities.put(3, "Low");
// Maintains insertion order during iteration
for (Map.Entry<Integer, String> entry : priorities.entrySet()) {
System.out.println("Priority " + entry.getKey() + ": " + entry.getValue());
}
// Output: Priority 1: High, Priority 2: Medium, Priority 3: Low
// Inverse also maintains corresponding order
BidiMap<String, Integer> inversePriorities = priorities.inverseBidiMap();
for (String level : inversePriorities.keySet()) {
System.out.println(level + " has priority " + inversePriorities.get(level));
}
// Output: High has priority 1, Medium has priority 2, Low has priority 3TreeMap-based implementation that maintains sorted order for both keys and values.
import org.apache.commons.collections4.bidimap.DualTreeBidiMap;
import java.util.Comparator;
// Natural ordering for both keys and values
DualTreeBidiMap<String, Integer> grades = new DualTreeBidiMap<>();
grades.put("Alice", 95);
grades.put("Bob", 87);
grades.put("Charlie", 92);
// Keys sorted alphabetically
System.out.println("First student: " + grades.firstKey()); // "Alice"
System.out.println("Last student: " + grades.lastKey()); // "Charlie"
// Values also sorted in inverse map
SortedBidiMap<Integer, String> byGrades = grades.inverseBidiMap();
System.out.println("Lowest grade: " + byGrades.firstKey()); // 87
System.out.println("Highest grade: " + byGrades.lastKey()); // 95
// Custom comparators
Comparator<String> nameComp = (a, b) -> a.length() - b.length(); // By name length
Comparator<Integer> gradeComp = Comparator.reverseOrder(); // Descending grades
DualTreeBidiMap<String, Integer> customOrder = new DualTreeBidiMap<>(nameComp, gradeComp);Red-black tree implementation for Comparable objects with excellent performance.
import org.apache.commons.collections4.bidimap.TreeBidiMap;
import java.time.LocalDate;
import java.util.Map;
import java.util.HashMap;
// For Comparable types
TreeBidiMap<LocalDate, String> events = new TreeBidiMap<>();
events.put(LocalDate.of(2024, 1, 1), "New Year");
events.put(LocalDate.of(2024, 7, 4), "Independence Day");
events.put(LocalDate.of(2024, 12, 25), "Christmas");
// Sorted by date
LocalDate firstDate = events.firstKey(); // 2024-01-01
LocalDate lastDate = events.lastKey(); // 2024-12-25
// Create from existing map
Map<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("Alpha", 100);
scoreMap.put("Beta", 85);
scoreMap.put("Gamma", 92);
TreeBidiMap<String, Integer> scores = new TreeBidiMap<>(scoreMap);BidiMaps enforce 1:1 relationships between keys and values.
BidiMap<String, String> usernames = new DualHashBidiMap<>();
// Initial mapping
usernames.put("john.doe", "John Doe");
usernames.put("jane.smith", "Jane Smith");
// Attempting to add duplicate value removes existing mapping
String oldKey = usernames.put("j.doe", "John Doe"); // oldKey is null
// Now "John Doe" maps to "j.doe", "john.doe" mapping is removed
System.out.println(usernames.getKey("John Doe")); // Returns "j.doe"
System.out.println(usernames.get("john.doe")); // Returns null
// Using putAll with conflicting values
Map<String, String> newMappings = Map.of(
"admin", "Administrator",
"jane.doe", "Jane Smith" // Conflicts with existing value
);
usernames.putAll(newMappings);
// "Jane Smith" now maps to "jane.doe", "jane.smith" is removedInverse maps provide a flipped view of the same underlying data.
BidiMap<String, Integer> original = new DualHashBidiMap<>();
original.put("A", 1);
original.put("B", 2);
BidiMap<Integer, String> inverse = original.inverseBidiMap();
// Both views share the same data
System.out.println(original.size()); // 2
System.out.println(inverse.size()); // 2
// Modifications through inverse affect original
inverse.put(3, "C");
System.out.println(original.get("C")); // Returns 3
// Removing through inverse
String removed = inverse.remove(2); // Returns "B"
System.out.println(original.containsKey("B")); // false
// Getting inverse of inverse returns original
BidiMap<String, Integer> doubleInverse = inverse.inverseBidiMap();
System.out.println(doubleInverse == original); // trueEfficient iteration over bidirectional maps.
import org.apache.commons.collections4.MapIterator;
BidiMap<String, Double> stockPrices = new DualHashBidiMap<>();
stockPrices.put("AAPL", 150.25);
stockPrices.put("GOOGL", 2750.80);
stockPrices.put("MSFT", 305.15);
// Iterate with MapIterator
MapIterator<String, Double> iterator = stockPrices.mapIterator();
while (iterator.hasNext()) {
String symbol = iterator.next();
Double price = iterator.getValue();
// Update prices with 5% increase
iterator.setValue(price * 1.05);
System.out.println(symbol + ": $" + iterator.getValue());
}
// Iterate inverse map
MapIterator<Double, String> priceIterator = stockPrices.inverseBidiMap().mapIterator();
while (priceIterator.hasNext()) {
Double price = priceIterator.next();
String symbol = priceIterator.getValue();
System.out.println("$" + price + " -> " + symbol);
}public class UserManager {
private final BidiMap<String, Long> usernameToId = new DualHashBidiMap<>();
public void registerUser(String username, Long userId) {
// BidiMap ensures both username and userId are unique
usernameToId.put(username, userId);
}
public Long getUserId(String username) {
return usernameToId.get(username);
}
public String getUsername(Long userId) {
return usernameToId.getKey(userId);
}
public boolean isUsernameTaken(String username) {
return usernameToId.containsKey(username);
}
public boolean isUserIdTaken(Long userId) {
return usernameToId.containsValue(userId);
}
public void deleteUser(String username) {
usernameToId.remove(username);
}
public void deleteUserById(Long userId) {
usernameToId.removeValue(userId);
}
}public enum OrderState { PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED }
public enum StatusCode { 100, 200, 300, 400, 500 }
public class OrderStateMachine {
private final BidiMap<OrderState, StatusCode> stateCodes = new DualHashBidiMap<>();
public OrderStateMachine() {
stateCodes.put(OrderState.PENDING, StatusCode.100);
stateCodes.put(OrderState.CONFIRMED, StatusCode.200);
stateCodes.put(OrderState.SHIPPED, StatusCode.300);
stateCodes.put(OrderState.DELIVERED, StatusCode.400);
stateCodes.put(OrderState.CANCELLED, StatusCode.500);
}
public StatusCode getStatusCode(OrderState state) {
return stateCodes.get(state);
}
public OrderState getState(StatusCode code) {
return stateCodes.getKey(code);
}
public Set<OrderState> getAllStates() {
return stateCodes.keySet();
}
public Set<StatusCode> getAllCodes() {
return stateCodes.values();
}
}public class ConfigurationManager {
private final OrderedBidiMap<String, String> properties = new DualLinkedHashBidiMap<>();
public void loadConfiguration(Properties props) {
props.forEach((key, value) -> properties.put(key.toString(), value.toString()));
}
public String getProperty(String key) {
return properties.get(key);
}
public String getPropertyKey(String value) {
return properties.getKey(value);
}
public void setProperty(String key, String value) {
properties.put(key, value);
}
// Maintain insertion order for configuration display
public Map<String, String> getAllProperties() {
return new LinkedHashMap<>(properties);
}
// Find all keys with a specific value pattern
public Set<String> getKeysForValue(String valuePattern) {
return properties.entrySet().stream()
.filter(entry -> entry.getValue().matches(valuePattern))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
}| Implementation | Key Lookup | Value Lookup | Ordering | Memory Overhead |
|---|---|---|---|---|
| DualHashBidiMap | O(1) | O(1) | None | Low |
| DualLinkedHashBidiMap | O(1) | O(1) | Insertion | Medium |
| DualTreeBidiMap | O(log n) | O(log n) | Sorted | Medium |
| TreeBidiMap | O(log n) | O(log n) | Natural | Low |
// DualHashBidiMap uses two HashMap instances
BidiMap<String, Integer> hash = new DualHashBidiMap<>();
// Memory: ~2x HashMap overhead + entry objects
// TreeBidiMap uses single tree structure
BidiMap<String, Integer> tree = new TreeBidiMap<>();
// Memory: ~1.5x TreeMap overhead + navigation pointers
// For memory-critical applications with large datasets
BidiMap<Integer, Integer> compact = new TreeBidiMap<>(); // More memory efficient
BidiMap<String, String> spacious = new DualHashBidiMap<>(); // Faster but more memoryBidiMaps are not thread-safe by default. For concurrent access:
// Option 1: External synchronization
BidiMap<String, Integer> bidiMap = new DualHashBidiMap<>();
Map<String, Integer> syncMap = Collections.synchronizedMap(bidiMap);
// Option 2: Concurrent wrapper (custom implementation needed)
public class ConcurrentBidiMap<K, V> {
private final BidiMap<K, V> map;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public ConcurrentBidiMap(BidiMap<K, V> map) {
this.map = map;
}
public V get(K key) {
lock.readLock().lock();
try {
return map.get(key);
} finally {
lock.readLock().unlock();
}
}
public K getKey(V value) {
lock.readLock().lock();
try {
return map.getKey(value);
} finally {
lock.readLock().unlock();
}
}
public V put(K key, V value) {
lock.writeLock().lock();
try {
return map.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
}// Fast lookups, no ordering requirements
BidiMap<String, Integer> fast = new DualHashBidiMap<>();
// Need to maintain insertion order
BidiMap<String, Integer> ordered = new DualLinkedHashBidiMap<>();
// Need sorted iteration, have Comparable keys/values
BidiMap<String, Integer> sorted = new DualTreeBidiMap<>();
// Memory efficient for Comparable types
BidiMap<String, Integer> efficient = new TreeBidiMap<>();BidiMap<String, String> errorProne = new DualHashBidiMap<>();
try {
errorProne.put("key1", "value1");
errorProne.put("key2", "value1"); // May remove key1 mapping
} catch (Exception e) {
// Handle potential issues
}
// Safer approach - check before inserting
public boolean safePut(BidiMap<String, String> map, String key, String value) {
if (map.containsValue(value)) {
String existingKey = map.getKey(value);
if (!key.equals(existingKey)) {
System.out.println("Warning: Value '" + value + "' already mapped to '" + existingKey + "'");
return false;
}
}
map.put(key, value);
return true;
}Bidirectional maps provide efficient two-way lookup capabilities while maintaining data consistency through 1:1 key-value relationships. Choose the appropriate implementation based on your performance requirements and ordering needs.
Install with Tessl CLI
npx tessl i tessl/maven-org-apache-commons--commons-collections4