The Apache Commons Collections package contains types that extend and augment the Java Collections Framework
—
Bags (also known as MultiSets) are collections that count the number of times an object appears in the collection. Unlike regular sets which only track presence/absence, bags maintain occurrence counts for each unique element.
The primary interface for bag collections that count element occurrences.
import org.apache.commons.collections4.Bag;
import org.apache.commons.collections4.bag.HashBag;
import java.util.Set;
Bag<String> bag = new HashBag<>();
// Add elements (returns true if collection changed)
boolean added = bag.add("apple"); // Count: apple=1
bag.add("apple", 3); // Count: apple=4
bag.add("banana", 2); // Count: banana=2
// Query counts
int appleCount = bag.getCount("apple"); // Returns 4
int bananaCount = bag.getCount("banana"); // Returns 2
int orangeCount = bag.getCount("orange"); // Returns 0 (not present)
// Collection properties
int totalSize = bag.size(); // Returns 6 (4+2)
Set<String> unique = bag.uniqueSet(); // Returns {"apple", "banana"}
int uniqueCount = unique.size(); // Returns 2
// Remove elements
boolean removed = bag.remove("apple"); // Removes 1, count now 3
bag.remove("apple", 2); // Removes 2, count now 1
bag.remove("banana", 5); // Removes all (only 2 existed)
// Check presence
boolean hasApple = bag.contains("apple"); // true (count > 0)
boolean hasBanana = bag.contains("banana"); // false (count = 0)A bag that maintains elements in sorted order by their natural ordering or a provided comparator.
import org.apache.commons.collections4.SortedBag;
import org.apache.commons.collections4.bag.TreeBag;
import java.util.Comparator;
// Natural ordering
SortedBag<String> sortedBag = new TreeBag<>();
sortedBag.add("zebra", 2);
sortedBag.add("apple", 3);
sortedBag.add("banana", 1);
// Elements maintain sorted order
String first = sortedBag.first(); // Returns "apple"
String last = sortedBag.last(); // Returns "zebra"
// Custom comparator (reverse order)
SortedBag<String> reverseBag = new TreeBag<>(Comparator.reverseOrder());
reverseBag.addAll(sortedBag);
String firstReverse = reverseBag.first(); // Returns "zebra"
String lastReverse = reverseBag.last(); // Returns "apple"An alternative interface to Bag with slightly different semantics and additional methods.
import org.apache.commons.collections4.MultiSet;
import org.apache.commons.collections4.multiset.HashMultiSet;
import java.util.Set;
MultiSet<String> multiset = new HashMultiSet<>();
// Add and set counts
multiset.add("item", 5); // Add 5 occurrences
int oldCount = multiset.setCount("item", 3); // Set to 3, returns old count (5)
// Entry iteration with counts
Set<MultiSet.Entry<String>> entries = multiset.entrySet();
for (MultiSet.Entry<String> entry : entries) {
String element = entry.getElement();
int count = entry.getCount();
System.out.println(element + ": " + count);
}
// Unique elements
Set<String> uniqueElements = multiset.uniqueSet();A hash-based bag implementation offering O(1) average case performance for basic operations.
import org.apache.commons.collections4.bag.HashBag;
import java.util.Collection;
import java.util.Arrays;
// Create empty bag
HashBag<Integer> numbers = new HashBag<>();
// Create from existing collection
Collection<String> words = Arrays.asList("the", "quick", "brown", "the", "fox");
HashBag<String> wordCounts = new HashBag<>(words);
// Result: {the=2, quick=1, brown=1, fox=1}
// Bulk operations
numbers.addAll(Arrays.asList(1, 2, 2, 3, 3, 3));
System.out.println(numbers.getCount(1)); // 1
System.out.println(numbers.getCount(2)); // 2
System.out.println(numbers.getCount(3)); // 3A tree-based sorted bag implementation with O(log n) performance and element ordering.
import org.apache.commons.collections4.bag.TreeBag;
import java.util.Comparator;
// Natural ordering
TreeBag<String> words = new TreeBag<>();
words.add("zebra", 1);
words.add("apple", 3);
words.add("banana", 2);
// Iterate in sorted order
for (String word : words.uniqueSet()) {
System.out.println(word + ": " + words.getCount(word));
}
// Output: apple: 3, banana: 2, zebra: 1
// Custom comparator (by string length)
TreeBag<String> byLength = new TreeBag<>(Comparator.comparing(String::length));
byLength.add("a", 1);
byLength.add("hello", 2);
byLength.add("hi", 3);
String shortest = byLength.first(); // "a" (length 1)
String longest = byLength.last(); // "hello" (length 5)Hash-based implementation of the MultiSet interface.
import org.apache.commons.collections4.multiset.HashMultiSet;
HashMultiSet<Character> charCounts = new HashMultiSet<>();
// Count character frequencies
String text = "hello world";
for (char c : text.toCharArray()) {
if (c != ' ') {
charCounts.add(c);
}
}
// Get most frequent character
char mostFrequent = charCounts.entrySet().stream()
.max(Comparator.comparing(MultiSet.Entry::getCount))
.map(MultiSet.Entry::getElement)
.orElse('\0');
System.out.println("Most frequent: " + mostFrequent); // 'l' (count: 3)A bag implementation that wraps an existing collection and maintains element counts.
import org.apache.commons.collections4.bag.CollectionBag;
import java.util.ArrayList;
import java.util.List;
// Wrap an existing collection
List<String> existingList = new ArrayList<>();
existingList.add("apple");
existingList.add("banana");
existingList.add("apple");
CollectionBag<String> collectionBag = new CollectionBag<>(existingList);
// Bag operations work on the underlying collection
int appleCount = collectionBag.getCount("apple"); // 2
collectionBag.add("cherry", 3);
// Changes are reflected in original collection
System.out.println(existingList.size()); // 6 (apple, banana, apple, cherry, cherry, cherry)A sorted bag implementation that wraps an existing sorted collection.
import org.apache.commons.collections4.bag.CollectionSortedBag;
import java.util.TreeSet;
import java.util.Set;
// Wrap an existing sorted collection
Set<String> sortedSet = new TreeSet<>();
sortedSet.add("zebra");
sortedSet.add("apple");
sortedSet.add("banana");
CollectionSortedBag<String> sortedCollectionBag = new CollectionSortedBag<>(sortedSet);
// Maintains sorted order
String first = sortedCollectionBag.first(); // "apple"
String last = sortedCollectionBag.last(); // "zebra"Immutable views of bags that prevent modification.
import org.apache.commons.collections4.bag.UnmodifiableBag;
import org.apache.commons.collections4.bag.UnmodifiableSortedBag;
import org.apache.commons.collections4.BagUtils;
Bag<String> mutableBag = new HashBag<>();
mutableBag.add("apple", 3);
mutableBag.add("banana", 2);
// Create unmodifiable view
Bag<String> immutableBag = UnmodifiableBag.unmodifiableBag(mutableBag);
// Or use utility method
Bag<String> immutableBag2 = BagUtils.unmodifiableBag(mutableBag);
// Reading operations work
int count = immutableBag.getCount("apple"); // 3
// Modification attempts throw UnsupportedOperationException
// immutableBag.add("cherry"); // Throws exception
// For sorted bags
SortedBag<String> mutableSorted = new TreeBag<>();
mutableSorted.add("zebra", 2);
mutableSorted.add("apple", 1);
SortedBag<String> immutableSorted = UnmodifiableSortedBag.unmodifiableSortedBag(mutableSorted);
String first = immutableSorted.first(); // "apple" (reading works)
// immutableSorted.add("banana"); // Throws exceptionimport org.apache.commons.collections4.BagUtils;
import org.apache.commons.collections4.bag.HashBag;
import org.apache.commons.collections4.bag.SynchronizedBag;
// Create thread-safe bag
Bag<String> safeBag = BagUtils.synchronizedBag(new HashBag<>());
// Or create directly
Bag<String> directSafe = SynchronizedBag.synchronizedBag(new HashBag<>());
// Thread-safe operations
safeBag.add("item", 5); // Safe for concurrent access
int count = safeBag.getCount("item"); // Safe for concurrent access
// Note: Iteration requires external synchronization
synchronized(safeBag) {
for (String item : safeBag.uniqueSet()) {
System.out.println(item + ": " + safeBag.getCount(item));
}
}import org.apache.commons.collections4.BagUtils;
import org.apache.commons.collections4.bag.HashBag;
Bag<String> mutableBag = new HashBag<>();
mutableBag.add("read-only", 3);
// Create unmodifiable view
Bag<String> readOnlyBag = BagUtils.unmodifiableBag(mutableBag);
// Reading works fine
int count = readOnlyBag.getCount("read-only"); // Returns 3
boolean contains = readOnlyBag.contains("read-only"); // true
// Modifications throw UnsupportedOperationException
try {
readOnlyBag.add("new-item");
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify unmodifiable bag");
}Bags that validate elements before adding them.
import org.apache.commons.collections4.BagUtils;
import org.apache.commons.collections4.Predicate;
import org.apache.commons.collections4.bag.HashBag;
// Only allow positive integers
Predicate<Integer> positiveOnly = n -> n > 0;
Bag<Integer> positiveBag = BagUtils.predicatedBag(new HashBag<>(), positiveOnly);
positiveBag.add(5, 2); // Succeeds, count: 5=2
positiveBag.add(10); // Succeeds, count: 10=1
try {
positiveBag.add(-1); // Throws IllegalArgumentException
} catch (IllegalArgumentException e) {
System.out.println("Negative numbers not allowed");
}
// String length validation
Predicate<String> maxLength = s -> s.length() <= 10;
Bag<String> shortStrings = BagUtils.predicatedBag(new HashBag<>(), maxLength);Bags that transform elements before storing them.
import org.apache.commons.collections4.BagUtils;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.bag.HashBag;
// Transform to uppercase
Transformer<String, String> upperCase = String::toUpperCase;
Bag<String> upperBag = BagUtils.transformedBag(new HashBag<>(), upperCase);
upperBag.add("hello"); // Stored as "HELLO"
upperBag.add("world"); // Stored as "WORLD"
upperBag.add("Hello"); // Adds to existing "HELLO" count
int helloCount = upperBag.getCount("HELLO"); // Returns 2
int worldCount = upperBag.getCount("WORLD"); // Returns 1
// Numeric transformation
Transformer<Integer, Integer> absolute = Math::abs;
Bag<Integer> absBag = BagUtils.transformedBag(new HashBag<>(), absolute);
absBag.add(-5); // Stored as 5
absBag.add(5); // Adds to existing 5 count
int fiveCount = absBag.getCount(5); // Returns 2Comprehensive utility methods for bag operations.
import org.apache.commons.collections4.BagUtils;
import org.apache.commons.collections4.bag.HashBag;
// Empty immutable bag
Bag<String> empty = BagUtils.emptyBag();
System.out.println(empty.isEmpty()); // true
// Create collection-backed bag
Bag<String> baseBag = new HashBag<>();
baseBag.add("item1", 2);
Bag<String> collectionBag = BagUtils.collectionBag(baseBag);
// Synchronization
Bag<Integer> concurrent = BagUtils.synchronizedBag(new HashBag<>());
// Sorted bag synchronization
SortedBag<String> sortedConcurrent = BagUtils.synchronizedSortedBag(new TreeBag<>());
// Predicate validation
Predicate<String> nonEmpty = s -> !s.isEmpty();
Bag<String> validatedBag = BagUtils.predicatedBag(new HashBag<>(), nonEmpty);
// Transformation
Transformer<String, String> trim = String::trim;
Bag<String> trimmedBag = BagUtils.transformingBag(new HashBag<>(), trim);import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
// Count word frequencies in text
List<String> words = Arrays.asList("the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog", "the");
Bag<String> frequencies = new HashBag<>(words);
// Find most common word
String mostCommon = frequencies.uniqueSet().stream()
.max(Comparator.comparing(frequencies::getCount))
.orElse(null);
System.out.println("Most common: " + mostCommon); // "the" (appears 3 times)
// Get frequency distribution
Map<String, Integer> distribution = frequencies.uniqueSet().stream()
.collect(Collectors.toMap(
word -> word,
frequencies::getCount
));import java.util.DoubleSummaryStatistics;
Bag<Integer> scores = new HashBag<>();
scores.add(85, 3); // 3 students scored 85
scores.add(90, 2); // 2 students scored 90
scores.add(75, 1); // 1 student scored 75
scores.add(95, 1); // 1 student scored 95
// Calculate statistics considering frequencies
DoubleSummaryStatistics stats = scores.uniqueSet().stream()
.flatMapToInt(score -> IntStream.generate(() -> score).limit(scores.getCount(score)))
.summaryStatistics();
double average = stats.getAverage(); // 85.71
int totalStudents = (int)stats.getCount(); // 7
int maxScore = (int)stats.getMax(); // 95
int minScore = (int)stats.getMin(); // 75public class Inventory {
private final Bag<String> stock = new HashBag<>();
public void addStock(String product, int quantity) {
stock.add(product, quantity);
}
public boolean sellProduct(String product, int quantity) {
if (stock.getCount(product) >= quantity) {
stock.remove(product, quantity);
return true;
}
return false;
}
public int getStockLevel(String product) {
return stock.getCount(product);
}
public Set<String> getProductsInStock() {
return stock.uniqueSet();
}
public int getTotalItems() {
return stock.size();
}
public boolean isInStock(String product) {
return stock.contains(product);
}
}HashBag: Use for fast lookups and when element ordering is not important
TreeBag: Use when you need sorted iteration or range operations
HashMultiSet: Use when you need MultiSet interface semantics
// Memory-efficient for high counts
Bag<String> efficient = new HashBag<>();
efficient.add("repeated-item", 1_000_000); // Stores count, not 1M objects
// Less efficient with standard collections
List<String> inefficient = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
inefficient.add("repeated-item"); // Stores 1M string references
}// For high-concurrency scenarios, consider external synchronization
ConcurrentHashMap<String, AtomicInteger> concurrentCounts = new ConcurrentHashMap<>();
// Atomic increment
concurrentCounts.computeIfAbsent("key", k -> new AtomicInteger(0)).incrementAndGet();
// Or use synchronized bag with careful synchronization
Bag<String> syncBag = BagUtils.synchronizedBag(new HashBag<>());Bags and MultiSets provide powerful abstractions for counting and frequency analysis scenarios. Choose the appropriate implementation based on your performance requirements, thread safety needs, and whether you need ordered iteration.
Install with Tessl CLI
npx tessl i tessl/maven-org-apache-commons--commons-collections4