🧩 Collections in Java
📚 Introduction to the Java Collections Framework
The Java Collections Framework is a unified architecture for representing and manipulating collections of objects. It provides a set of interfaces, implementations, and algorithms that allow you to store, retrieve, manipulate, and communicate aggregate data efficiently.
Before the Collections Framework was introduced in Java 1.2, developers had to create their own data structures or use limited classes like Vector
, Stack
, and Hashtable
. The Collections Framework standardized how collections are handled in Java, making code more reusable and interoperable.
🌟 Key Components of Java Collections Framework
The Java Collections Framework consists of:
- Interfaces: Abstract data types representing collections
- Implementations: Concrete implementations of the collection interfaces
- Algorithms: Methods that perform useful computations on collections
- Utility Classes: Helper classes that provide additional functionality
🏗️ Architecture of Java Collections Framework
The Collections Framework is built around a set of core interfaces:
Collection
|
-------------------------
| | |
List Set Queue
| | |
ArrayList HashSet PriorityQueue
LinkedList TreeSet etc.
etc. etc.
And a separate interface for key-value pairs:
Map
|
----------------------------
| | |
HashMap TreeMap LinkedHashMap
etc. etc. etc.
In this tutorial, we'll focus on the foundational aspects of the Collections Framework, including the Collection
interface, the Map
interface, and the utility classes that support them. The specific collection types (List, Queue, Set) will be covered in a separate chapter.
🔍 The Collection Interface
The Collection
interface is the root of the collection hierarchy. It represents a group of objects known as elements. Some collections allow duplicate elements, while others don't. Some are ordered, and others are unordered.
Java Collection Interface: Core Methods and Usage
public interface Collection<E> extends Iterable<E> {
// Basic operations
boolean add(E e);
boolean remove(Object o);
boolean contains(Object o);
boolean isEmpty();
int size();
void clear();
// Bulk operations
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
boolean containsAll(Collection<?> c);
// Array operations
Object[] toArray();
<T> T[] toArray(T[] a);
// Iterator
Iterator<E> iterator();
}
Let's explore these methods with examples:
Basic Java Collection Operations
import java.util.*;
public class CollectionDemo {
public static void main(String[] args) {
// Create a collection (using ArrayList implementation)
Collection<String> fruits = new ArrayList<>();
// Add elements
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
System.out.println("Fruits collection: " + fruits);
// Check if an element exists
boolean containsApple = fruits.contains("Apple");
System.out.println("Contains Apple? " + containsApple);
// Remove an element
boolean removed = fruits.remove("Banana");
System.out.println("Removed Banana? " + removed);
System.out.println("Fruits after removal: " + fruits);
// Get the size
int size = fruits.size();
System.out.println("Number of fruits: " + size);
// Check if empty
boolean isEmpty = fruits.isEmpty();
System.out.println("Is the collection empty? " + isEmpty);
// Clear the collection
fruits.clear();
System.out.println("Fruits after clearing: " + fruits);
System.out.println("Is the collection empty now? " + fruits.isEmpty());
}
}
Output:
Fruits collection: [Apple, Banana, Cherry]
Contains Apple? true
Removed Banana? true
Fruits after removal: [Apple, Cherry]
Number of fruits: 2
Is the collection empty? false
Fruits after clearing: []
Is the collection empty now? true
Bulk Operations in Java Collections
Bulk operations allow you to perform operations on entire collections:
import java.util.*;
public class BulkOperationsDemo {
public static void main(String[] args) {
// Create two collections
Collection<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
Collection<String> moreFruits = new ArrayList<>();
moreFruits.add("Dragonfruit");
moreFruits.add("Elderberry");
// Add all elements from another collection
fruits.addAll(moreFruits);
System.out.println("After addAll: " + fruits);
// Check if collection contains all elements from another collection
Collection<String> somefruits = new ArrayList<>();
somefruits.add("Apple");
somefruits.add("Cherry");
boolean containsAll = fruits.containsAll(somefruits);
System.out.println("Contains [Apple, Cherry]? " + containsAll);
// Remove all elements that exist in another collection
Collection<String> toRemove = new ArrayList<>();
toRemove.add("Apple");
toRemove.add("Elderberry");
fruits.removeAll(toRemove);
System.out.println("After removeAll [Apple, Elderberry]: " + fruits);
// Retain only elements that exist in another collection
Collection<String> toRetain = new ArrayList<>();
toRetain.add("Banana");
fruits.retainAll(toRetain);
System.out.println("After retainAll [Banana]: " + fruits);
}
}
Output:
After addAll: [Apple, Banana, Cherry, Dragonfruit, Elderberry]
Contains [Apple, Cherry]? true
After removeAll [Apple, Elderberry]: [Banana, Cherry, Dragonfruit]
After retainAll [Banana]: [Banana]
Array Operations with Java Collections
You can convert collections to arrays:
import java.util.*;
public class ArrayOperationsDemo {
public static void main(String[] args) {
Collection<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
// Convert to Object array
Object[] fruitArray = fruits.toArray();
System.out.println("Object array length: " + fruitArray.length);
System.out.println("First element: " + fruitArray[0]);
// Convert to String array
String[] stringArray = fruits.toArray(new String[0]);
System.out.println("String array length: " + stringArray.length);
System.out.println("Elements: " + Arrays.toString(stringArray));
// Pre-sized array
String[] preSizedArray = fruits.toArray(new String[5]);
System.out.println("Pre-sized array length: " + preSizedArray.length);
System.out.println("Elements with nulls: " + Arrays.toString(preSizedArray));
}
}
Output:
Object array length: 3
First element: Apple
String array length: 3
Elements: [Apple, Banana, Cherry]
Pre-sized array length: 5
Elements with nulls: [Apple, Banana, Cherry, null, null]
Iterating Through Java Collections
The Collection
interface extends Iterable
, which means all collections can be iterated using the enhanced for loop or an iterator:
import java.util.*;
public class IterationDemo {
public static void main(String[] args) {
Collection<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
// Using enhanced for loop
System.out.println("Using enhanced for loop:");
for (String fruit : fruits) {
System.out.println(fruit);
}
// Using iterator
System.out.println("\nUsing iterator:");
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}
// Using iterator to remove elements
System.out.println("\nRemoving elements that start with 'B':");
iterator = fruits.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
if (fruit.startsWith("B")) {
iterator.remove();
}
}
System.out.println("After removal: " + fruits);
}
}
Output:
Using enhanced for loop:
Apple
Banana
Cherry
Using iterator:
Apple
Banana
Cherry
Removing elements that start with 'B':
After removal: [Apple, Cherry]
🗺️ Java Map Interface: Key-Value Data Structures
The Map
interface represents a mapping between keys and values. Each key can map to at most one value, and keys must be unique within a map.
Core Methods of Java Map Interface
public interface Map<K, V> {
// Basic operations
V put(K key, V value);
V get(Object key);
V remove(Object key);
boolean containsKey(Object key);
boolean containsValue(Object value);
int size();
boolean isEmpty();
void clear();
// Bulk operations
void putAll(Map<? extends K, ? extends V> m);
// Collection views
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
// Entry interface
interface Entry<K, V> {
K getKey();
V getValue();
V setValue(V value);
}
}
Let's explore these methods with examples:
Basic Java Map Operations
import java.util.*;
public class MapDemo {
public static void main(String[] args) {
// Create a map (using HashMap implementation)
Map<String, Integer> fruitInventory = new HashMap<>();
// Add key-value pairs
fruitInventory.put("Apple", 100);
fruitInventory.put("Banana", 150);
fruitInventory.put("Cherry", 75);
System.out.println("Fruit inventory: " + fruitInventory);
// Get a value by key
int appleCount = fruitInventory.get("Apple");
System.out.println("Apple count: " + appleCount);
// Get a value with a default if key not found
int grapeCount = fruitInventory.getOrDefault("Grape", 0);
System.out.println("Grape count: " + grapeCount);
// Check if a key exists
boolean hasBanana = fruitInventory.containsKey("Banana");
System.out.println("Has Banana? " + hasBanana);
// Check if a value exists
boolean has75 = fruitInventory.containsValue(75);
System.out.println("Has inventory of 75? " + has75);
// Remove a key-value pair
int removedValue = fruitInventory.remove("Banana");
System.out.println("Removed Banana count: " + removedValue);
System.out.println("Inventory after removal: " + fruitInventory);
// Get the size
int size = fruitInventory.size();
System.out.println("Number of fruit types: " + size);
// Clear the map
fruitInventory.clear();
System.out.println("Inventory after clearing: " + fruitInventory);
}
}
Output:
Fruit inventory: {Apple=100, Cherry=75, Banana=150}
Apple count: 100
Grape count: 0
Has Banana? true
Has inventory of 75? true
Removed Banana count: 150
Inventory after removal: {Apple=100, Cherry=75}
Number of fruit types: 2
Inventory after clearing: {}
Java Map Views: Keys, Values, and Entries
Maps provide three collection views: keys, values, and entries (key-value pairs):
import java.util.*;
public class MapViewsDemo {
public static void main(String[] args) {
Map<String, Integer> fruitInventory = new HashMap<>();
fruitInventory.put("Apple", 100);
fruitInventory.put("Banana", 150);
fruitInventory.put("Cherry", 75);
// Get the set of keys
Set<String> keys = fruitInventory.keySet();
System.out.println("Keys: " + keys);
// Get the collection of values
Collection<Integer> values = fruitInventory.values();
System.out.println("Values: " + values);
// Get the set of entries
Set<Map.Entry<String, Integer>> entries = fruitInventory.entrySet();
System.out.println("Entries: " + entries);
// Iterate through entries
System.out.println("\nIterating through entries:");
for (Map.Entry<String, Integer> entry : entries) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " => " + value);
}
// Modify values through the entry
System.out.println("\nModifying values through entries:");
for (Map.Entry<String, Integer> entry : entries) {
// Increase each inventory by 10
entry.setValue(entry.getValue() + 10);
}
System.out.println("Updated inventory: " + fruitInventory);
// Remove elements through the key set
System.out.println("\nRemoving 'Apple' through the key set:");
keys.remove("Apple");
System.out.println("Inventory after removal: " + fruitInventory);
}
}
Output:
Keys: [Apple, Cherry, Banana]
Values: [100, 75, 150]
Entries: [Apple=100, Cherry=75, Banana=150]
Iterating through entries:
Apple => 100
Cherry => 75
Banana => 150
Modifying values through entries:
Updated inventory: {Apple=110, Cherry=85, Banana=160}
Removing 'Apple' through the key set:
Inventory after removal: {Cherry=85, Banana=160}
Java Map Implementations Compared
The Java Collections Framework provides several implementations of the Map
interface:
- HashMap: Uses a hash table for storage. Provides constant-time performance for basic operations.
- TreeMap: Stores entries in a sorted order based on the keys. Provides log(n) time for most operations.
- LinkedHashMap: Maintains insertion order or access order of entries.
- Hashtable: Similar to HashMap but synchronized (thread-safe).
- ConcurrentHashMap: A thread-safe variant of HashMap designed for concurrent access.
Let's compare some of these implementations:
import java.util.*;
public class MapImplementationsDemo {
public static void main(String[] args) {
// HashMap - no guaranteed order
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("C", 3);
hashMap.put("A", 1);
hashMap.put("B", 2);
System.out.println("HashMap: " + hashMap);
// TreeMap - sorted by keys
Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put("C", 3);
treeMap.put("A", 1);
treeMap.put("B", 2);
System.out.println("TreeMap: " + treeMap);
// LinkedHashMap - maintains insertion order
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("C", 3);
linkedHashMap.put("A", 1);
linkedHashMap.put("B", 2);
System.out.println("LinkedHashMap: " + linkedHashMap);
// LinkedHashMap with access order
Map<String, Integer> accessOrderMap = new LinkedHashMap<>(16, 0.75f, true);
accessOrderMap.put("C", 3);
accessOrderMap.put("A", 1);
accessOrderMap.put("B", 2);
System.out.println("Access order map (initial): " + accessOrderMap);
// Access some elements
accessOrderMap.get("A");
accessOrderMap.get("C");
System.out.println("Access order map (after access): " + accessOrderMap);
}
}
Output:
HashMap: {A=1, B=2, C=3}
TreeMap: {A=1, B=2, C=3}
LinkedHashMap: {C=3, A=1, B=2}
Access order map (initial): {C=3, A=1, B=2}
Access order map (after access): {B=2, A=1, C=3}
🧰 Java Collections Utility Classes
The Collections Framework includes utility classes that provide useful algorithms and functionality for working with collections.
The Collections Class
The Collections
class provides static methods for operating on collections:
import java.util.*;
public class CollectionsUtilityDemo {
public static void main(String[] args) {
// Create a list
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
System.out.println("Original list: " + fruits);
// Sorting
Collections.sort(fruits);
System.out.println("Sorted list: " + fruits);
// Binary search (on sorted list)
int position = Collections.binarySearch(fruits, "Banana");
System.out.println("'Banana' found at position: " + position);
// Reverse
Collections.reverse(fruits);
System.out.println("Reversed list: " + fruits);
// Shuffle
Collections.shuffle(fruits);
System.out.println("Shuffled list: " + fruits);
// Find min and max
String min = Collections.min(fruits);
String max = Collections.max(fruits);
System.out.println("Min: " + min + ", Max: " + max);
// Fill with a value
Collections.fill(fruits, "Fruit");
System.out.println("After fill: " + fruits);
// Create unmodifiable collections
List<String> unmodifiableList = Collections.unmodifiableList(new ArrayList<>(Arrays.asList("A", "B", "C")));
System.out.println("Unmodifiable list: " + unmodifiableList);
// Create singleton collections
Set<String> singletonSet = Collections.singleton("Only Element");
System.out.println("Singleton set: " + singletonSet);
// Create empty collections
List<String> emptyList = Collections.emptyList();
System.out.println("Empty list: " + emptyList);
}
}
Output:
Original list: [Apple, Banana, Cherry]
Sorted list: [Apple, Banana, Cherry]
'Banana' found at position: 1
Reversed list: [Cherry, Banana, Apple]
Shuffled list: [Banana, Apple, Cherry]
Min: Apple, Max: Cherry
After fill: [Fruit, Fruit, Fruit]
Unmodifiable list: [A, B, C]
Singleton set: [Only Element]
Empty list: []
The Arrays Class
The Arrays
class provides utility methods for working with arrays, including converting between arrays and collections:
import java.util.*;
public class ArraysUtilityDemo {
public static void main(String[] args) {
// Create an array
String[] fruitArray = {"Apple", "Banana", "Cherry"};
System.out.println("Original array: " + Arrays.toString(fruitArray));
// Sort the array
Arrays.sort(fruitArray);
System.out.println("Sorted array: " + Arrays.toString(fruitArray));
// Binary search
int position = Arrays.binarySearch(fruitArray, "Banana");
System.out.println("'Banana' found at position: " + position);
// Fill the array
Arrays.fill(fruitArray, "Fruit");
System.out.println("After fill: " + Arrays.toString(fruitArray));
// Create a new array
fruitArray = new String[]{"Apple", "Banana", "Cherry"};
// Convert array to list
List<String> fruitList = Arrays.asList(fruitArray);
System.out.println("Array as list: " + fruitList);
// Note: The list is backed by the array
fruitArray[0] = "Apricot";
System.out.println("List after changing array: " + fruitList);
// Convert list to array
String[] newArray = fruitList.toArray(new String[0]);
System.out.println("List back to array: " + Arrays.toString(newArray));
// Create a modifiable ArrayList from an array
List<String> modifiableList = new ArrayList<>(Arrays.asList(fruitArray));
modifiableList.add("Dragonfruit");
System.out.println("Modifiable list: " + modifiableList);
}
}
Output:
Original array: [Apple, Banana, Cherry]
Sorted array: [Apple, Banana, Cherry]
'Banana' found at position: 1
After fill: [Fruit, Fruit, Fruit]
Array as list: [Apricot, Banana, Cherry]
List after changing array: [Apricot, Banana, Cherry]
List back to array: [Apricot, Banana, Cherry]
Modifiable list: [Apricot, Banana, Cherry, Dragonfruit]
🔄 Comparison Interfaces
The Collections Framework includes two interfaces for comparing objects:
- Comparable: For objects that have a natural ordering
- Comparator: For defining custom ordering
Comparable Interface
The Comparable
interface is implemented by a class to define its natural ordering:
import java.util.*;
// A class that implements Comparable
class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return name + " (" + age + ")";
}
// Define natural ordering based on age
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
public class ComparableDemo {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 25));
people.add(new Person("Charlie", 35));
System.out.println("Original list: " + people);
// Sort using natural ordering (by age)
Collections.sort(people);
System.out.println("Sorted by age: " + people);
// Find min and max
Person youngest = Collections.min(people);
Person oldest = Collections.max(people);
System.out.println("Youngest: " + youngest);
System.out.println("Oldest: " + oldest);
}
}
Output:
Original list: [Alice (30), Bob (25), Charlie (35)]
Sorted by age: [Bob (25), Alice (30), Charlie (35)]
Youngest: Bob (25)
Oldest: Charlie (35)
Comparator Interface
The Comparator
interface allows you to define custom ordering:
import java.util.*;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return name + " (" + age + ")";
}
}
public class ComparatorDemo {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 25));
people.add(new Person("Charlie", 35));
System.out.println("Original list: " + people);
// Sort by age using a Comparator
Comparator<Person> ageComparator = new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return Integer.compare(p1.getAge(), p2.getAge());
}
};
Collections.sort(people, ageComparator);
System.out.println("Sorted by age: " + people);
// Sort by name using a lambda expression
Collections.sort(people, (p1, p2) -> p1.getName().compareTo(p2.getName()));
System.out.println("Sorted by name: " + people);
// Sort by name using method reference
Collections.sort(people, Comparator.comparing(Person::getName));
System.out.println("Sorted by name (method reference): " + people);
// Reverse order
Collections.sort(people, Comparator.comparing(Person::getName).reversed());
System.out.println("Sorted by name (reversed): " + people);
// Multiple criteria: sort by age, then by name
Collections.sort(people, Comparator.comparing(Person::getAge)
.thenComparing(Person::getName));
System.out.println("Sorted by age, then name: " + people);
}
}
Output:
Original list: [Alice (30), Bob (25), Charlie (35)]
Sorted by age: [Bob (25), Alice (30), Charlie (35)]
Sorted by name: [Alice (30), Bob (25), Charlie (35)]
Sorted by name (method reference): [Alice (30), Bob (25), Charlie (35)]
Sorted by name (reversed): [Charlie (35), Bob (25), Alice (30)]
Sorted by age, then name: [Bob (25), Alice (30), Charlie (35)]
🧠 Advanced Collection Concepts
Generic Collections
Generics provide type safety for collections:
import java.util.*;
public class GenericCollectionsDemo {
public static void main(String[] args) {
// Non-generic collection (not recommended)
List rawList = new ArrayList();
rawList.add("string");
rawList.add(42); // Mixed types
// This will cause ClassCastException at runtime
try {
String s = (String) rawList.get(1); // Trying to cast Integer to String
} catch (ClassCastException e) {
System.out.println("ClassCastException caught: " + e.getMessage());
}
// Generic collection (type-safe)
List<String> stringList = new ArrayList<>();
stringList.add("string");
// stringList.add(42); // Compile-time error
// No casting needed, type safety guaranteed
String s = stringList.get(0);
System.out.println("Retrieved string: " + s);
// Wildcards
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
// Using wildcard for methods that work with any type of list
printCollection(stringList);
printCollection(intList);
// Using bounded wildcard
List<Number> numberList = new ArrayList<>();
numberList.add(1);
numberList.add(2.5);
addNumbers(intList); // Works with Integer
addNumbers(numberList); // Works with Number
// addNumbers(stringList); // Compile-time error
}
// Method that accepts any type of collection
public static void printCollection(Collection<?> collection) {
for (Object obj : collection) {
System.out.println(obj);
}
}
// Method that accepts collections of Number or its subclasses
public static void addNumbers(Collection<? extends Number> numbers) {
double sum = 0;
for (Number number : numbers) {
sum += number.doubleValue();
}
System.out.println("Sum: " + sum);
}
}
Output:
ClassCastException caught: java.lang.Integer cannot be cast to java.lang.String
Retrieved string: string
string
1
2
Sum: 3.0
Sum: 3.5
Thread-Safe Collections
The Collections Framework provides several ways to create thread-safe collections:
import java.util.*;
import java.util.concurrent.*;
public class ThreadSafeCollectionsDemo {
public static void main(String[] args) {
// Synchronized collections
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
// Concurrent collections
List<String> concurrentList = new CopyOnWriteArrayList<>();
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
Queue<String> concurrentQueue = new ConcurrentLinkedQueue<>();
// Example of using a concurrent map
concurrentMap.put("A", 1);
concurrentMap.put("B", 2);
// Safe to iterate without explicit synchronization
for (Map.Entry<String, Integer> entry : concurrentMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// Atomic operations
concurrentMap.putIfAbsent("C", 3); // Add only if key doesn't exist
concurrentMap.replace("B", 2, 20); // Replace only if current value matches
System.out.println("After atomic operations: " + concurrentMap);
}
}
Output:
A: 1
B: 2
After atomic operations: {A=1, B=20, C=3}
Immutable Collections
Immutable collections cannot be modified after creation:
import java.util.*;
public class ImmutableCollectionsDemo {
public static void main(String[] args) {
// Using Collections utility methods
List<String> unmodifiableList = Collections.unmodifiableList(
new ArrayList<>(Arrays.asList("A", "B", "C"))
);
try {
unmodifiableList.add("D"); // Will throw exception
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify unmodifiable list");
}
// Using factory methods (Java 9+)
List<String> immutableList = List.of("A", "B", "C");
Set<String> immutableSet = Set.of("A", "B", "C");
Map<String, Integer> immutableMap = Map.of("A", 1, "B", 2, "C", 3);
System.out.println("Immutable list: " + immutableList);
System.out.println("Immutable set: " + immutableSet);
System.out.println("Immutable map: " + immutableMap);
// For larger maps
Map<String, Integer> largeImmutableMap = Map.ofEntries(
Map.entry("A", 1),
Map.entry("B", 2),
Map.entry("C", 3),
Map.entry("D", 4),
Map.entry("E", 5)
);
System.out.println("Large immutable map: " + largeImmutableMap);
}
}
Output:
Cannot modify unmodifiable list
Immutable list: [A, B, C]
Immutable set: [A, B, C]
Immutable map: {A=1, B=2, C=3}
Large immutable map: {A=1, B=2, C=3, D=4, E=5}
🚫 Common Pitfalls and Gotchas
1. Modifying Collections During Iteration
One of the most common mistakes is modifying a collection while iterating through it:
import java.util.*;
public class IterationPitfallDemo {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
// INCORRECT: Will throw ConcurrentModificationException
try {
for (String fruit : fruits) {
if (fruit.startsWith("B")) {
fruits.remove(fruit); // Modifying while iterating
}
}
} catch (ConcurrentModificationException e) {
System.out.println("ConcurrentModificationException caught!");
}
// CORRECT: Use Iterator's remove method
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
if (fruit.startsWith("B")) {
iterator.remove(); // Safe way to remove during iteration
}
}
System.out.println("After safe removal: " + fruits);
// CORRECT: Use removeIf (Java 8+)
fruits.add("Banana"); // Add back for demonstration
fruits.removeIf(fruit -> fruit.startsWith("B"));
System.out.println("After removeIf: " + fruits);
}
}
Output:
ConcurrentModificationException caught!
After safe removal: [Apple, Cherry]
After removeIf: [Apple, Cherry]
2. Failing to Override equals() and hashCode()
When using objects as keys in maps or elements in sets, you must properly override equals()
and hashCode()
:
import java.util.*;
class BadKey {
private int id;
public BadKey(int id) {
this.id = id;
}
// No equals() or hashCode() override
}
class GoodKey {
private int id;
public GoodKey(int id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GoodKey goodKey = (GoodKey) o;
return id == goodKey.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
public class EqualsHashCodeDemo {
public static void main(String[] args) {
// Using BadKey
Map<BadKey, String> badMap = new HashMap<>();
BadKey key1 = new BadKey(1);
BadKey key2 = new BadKey(1); // Same value as key1
badMap.put(key1, "Value");
System.out.println("Bad key lookup: " + badMap.get(key2)); // Returns null
// Using GoodKey
Map<GoodKey, String> goodMap = new HashMap<>();
GoodKey goodKey1 = new GoodKey(1);
GoodKey goodKey2 = new GoodKey(1); // Same value as goodKey1
goodMap.put(goodKey1, "Value");
System.out.println("Good key lookup: " + goodMap.get(goodKey2)); // Returns "Value"
}
}
Output:
Bad key lookup: null
Good key lookup: Value
3. Not Understanding Collection Views
Collection views like keySet()
, values()
, and entrySet()
are backed by the original map:
import java.util.*;
public class CollectionViewPitfallDemo {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
// Get a view of the keys
Set<String> keys = map.keySet();
System.out.println("Original map: " + map);
System.out.println("Keys view: " + keys);
// Removing from the view affects the map
keys.remove("B");
System.out.println("Map after removing 'B' from keys view: " + map);
// Adding to the map affects the view
map.put("D", 4);
System.out.println("Keys view after adding 'D' to map: " + keys);
// But you cannot add directly to the keySet view
try {
// This would throw UnsupportedOperationException
// keys.add("E");
System.out.println("Cannot add directly to keySet view");
} catch (UnsupportedOperationException e) {
System.out.println("UnsupportedOperationException would be thrown");
}
}
}
Output:
Original map: {A=1, B=2, C=3}
Keys view: [A, B, C]
Map after removing 'B' from keys view: {A=1, C=3}
Keys view after adding 'D' to map: [A, C, D]
Cannot add directly to keySet view
4. Arrays.asList() Limitations
The list returned by Arrays.asList()
has fixed size and is backed by the original array:
import java.util.*;
public class ArraysAsListPitfallDemo {
public static void main(String[] args) {
String[] array = {"A", "B", "C"};
List<String> list = Arrays.asList(array);
// The list is backed by the array
array[0] = "X";
System.out.println("List after changing array: " + list);
list.set(1, "Y");
System.out.println("Array after changing list: " + Arrays.toString(array));
// Cannot resize the list
try {
list.add("D"); // Will throw UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("Cannot add to fixed-size list");
}
try {
list.remove(0); // Will throw UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("Cannot remove from fixed-size list");
}
// Solution: Create a new ArrayList
List<String> modifiableList = new ArrayList<>(Arrays.asList(array));
modifiableList.add("D"); // Works fine
System.out.println("Modifiable list: " + modifiableList);
}
}
Output:
List after changing array: [X, B, C]
Array after changing list: [X, Y, C]
Cannot add to fixed-size list
Cannot remove from fixed-size list
Modifiable list: [X, Y, C, D]
🏆 Best Practices and Rules
1. Choose the Right Collection Type
Select the appropriate collection type based on your requirements:
- ArrayList: Fast random access, but slow insertions/deletions in the middle
- LinkedList: Fast insertions/deletions, but slow random access
- HashSet: Fast lookups, no ordering guarantees
- TreeSet: Ordered elements, but slower operations
- HashMap: Fast key-based lookups
- TreeMap: Keys in sorted order
import java.util.*;
public class CollectionChoiceDemo {
public static void main(String[] args) {
// Scenario 1: Frequent random access, infrequent modifications
List<Integer> randomAccessList = new ArrayList<>(); // Good choice
// Scenario 2: Frequent insertions/deletions, infrequent random access
List<Integer> frequentModificationList = new LinkedList<>(); // Good choice
// Scenario 3: Need to maintain unique elements with fast lookups
Set<Integer> uniqueElementsSet = new HashSet<>(); // Good choice
// Scenario 4: Need unique elements in sorted order
Set<Integer> sortedUniqueElementsSet = new TreeSet<>(); // Good choice
// Scenario 5: Need key-value mappings with fast lookups
Map<String, Integer> fastLookupMap = new HashMap<>(); // Good choice
// Scenario 6: Need key-value mappings in key order
Map<String, Integer> orderedMap = new TreeMap<>(); // Good choice
// Scenario 7: Need to maintain insertion order
Map<String, Integer> insertionOrderMap = new LinkedHashMap<>(); // Good choice
}
}
2. Use Generics for Type Safety
Always use generics with collections to ensure type safety:
import java.util.*;
public class GenericsDemo {
public static void main(String[] args) {
// BAD: Raw type (not using generics)
List badList = new ArrayList();
badList.add("string");
badList.add(42); // Mixed types
// Need explicit casting, risk of ClassCastException
try {
String s = (String) badList.get(1); // Runtime error
} catch (ClassCastException e) {
System.out.println("Runtime error with raw types");
}
// GOOD: Using generics
List<String> goodList = new ArrayList<>();
goodList.add("string");
// goodList.add(42); // Compile-time error
// No casting needed, type safety guaranteed
String s = goodList.get(0);
System.out.println("Type-safe access: " + s);
}
}
3. Use Factory Methods for Immutable Collections
In Java 9 and later, use factory methods to create immutable collections:
import java.util.*;
public class ImmutableCollectionsDemo {
public static void main(String[] args) {
// Pre-Java 9 approach
List<String> oldImmutableList = Collections.unmodifiableList(
new ArrayList<>(Arrays.asList("A", "B", "C"))
);
// Java 9+ approach
List<String> list = List.of("A", "B", "C");
Set<String> set = Set.of("A", "B", "C");
Map<String, Integer> map = Map.of(
"A", 1,
"B", 2,
"C", 3
);
// For larger maps
Map<String, Integer> largeMap = Map.ofEntries(
Map.entry("A", 1),
Map.entry("B", 2),
Map.entry("C", 3),
Map.entry("D", 4)
);
System.out.println("Immutable list: " + list);
System.out.println("Immutable set: " + set);
System.out.println("Immutable map: " + map);
}
}
4. Use Enhanced For Loop or Iterator for Iteration
Use the enhanced for loop or iterator for cleaner code:
import java.util.*;
public class IterationDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
// BAD: Old-style for loop with index
System.out.println("Using index-based for loop:");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// GOOD: Enhanced for loop
System.out.println("\nUsing enhanced for loop:");
for (String item : list) {
System.out.println(item);
}
// GOOD: Iterator
System.out.println("\nUsing iterator:");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
// BEST (Java 8+): Stream API
System.out.println("\nUsing Stream API:");
list.forEach(System.out::println);
}
}
5. Use removeIf() for Safe Removal During Iteration
In Java 8 and later, use removeIf()
for safe removal during iteration:
import java.util.*;
public class RemoveIfDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("Apple", "Banana", "Cherry"));
// Remove elements starting with 'B'
list.removeIf(item -> item.startsWith("B"));
System.out.println("After removeIf: " + list);
}
}
🎯 Why It Matters / Use Cases
The Java Collections Framework is essential for almost all Java applications. Here are some common use cases:
1. Data Storage and Retrieval
Collections provide efficient ways to store and retrieve data:
import java.util.*;
public class DataStorageDemo {
public static void main(String[] args) {
// Store user information
Map<String, User> userDatabase = new HashMap<>();
// Add users
userDatabase.put("john123", new User("John", "john@example.com"));
userDatabase.put("alice456", new User("Alice", "alice@example.com"));
// Retrieve a user
User user = userDatabase.get("john123");
System.out.println("Found user: " + user);
// Check if a user exists
boolean exists = userDatabase.containsKey("bob789");
System.out.println("User bob789 exists: " + exists);
}
static class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
@Override
public String toString() {
return name + " (" + email + ")";
}
}
}
2. Maintaining Unique Items
Sets are perfect for ensuring uniqueness:
import java.util.*;
public class UniqueItemsDemo {
public static void main(String[] args) {
// Track unique visitors to a website
Set<String> uniqueVisitors = new HashSet<>();
// Add visitor IPs
uniqueVisitors.add("192.168.1.1");
uniqueVisitors.add("10.0.0.1");
uniqueVisitors.add("192.168.1.1"); // Duplicate, won't be added
System.out.println("Unique visitors: " + uniqueVisitors);
System.out.println("Number of unique visitors: " + uniqueVisitors.size());
}
}
3. Ordering and Sorting
Collections provide various ways to maintain order:
import java.util.*;
public class OrderingDemo {
public static void main(String[] args) {
// Maintain insertion order
List<String> todoList = new ArrayList<>();
todoList.add("Buy groceries");
todoList.add("Pay bills");
todoList.add("Call mom");
System.out.println("Todo list (insertion order): " + todoList);
// Sort alphabetically
Collections.sort(todoList);
System.out.println("Todo list (alphabetical): " + todoList);
// Priority queue (natural ordering)
Queue<Task> taskQueue = new PriorityQueue<>();
taskQueue.add(new Task("Pay bills", 2));
taskQueue.add(new Task("Buy groceries", 1)); // Higher priority
taskQueue.add(new Task("Call mom", 3));
System.out.println("\nTasks in priority order:");
while (!taskQueue.isEmpty()) {
System.out.println(taskQueue.poll());
}
}
static class Task implements Comparable<Task> {
private String description;
private int priority; // Lower number = higher priority
public Task(String description, int priority) {
this.description = description;
this.priority = priority;
}
@Override
public int compareTo(Task other) {
return Integer.compare(this.priority, other.priority);
}
@Override
public String toString() {
return description + " (priority: " + priority + ")";
}
}
}
4. Caching
Maps are excellent for implementing caches:
import java.util.*;
public class CachingDemo {
public static void main(String[] args) {
// Create a simple cache
Map<String, String> dataCache = new HashMap<>();
// Function that simulates expensive data retrieval
System.out.println("First call for 'key1':");
String data1 = getDataWithCaching("key1", dataCache);
System.out.println("Second call for 'key1' (should use cache):");
String data2 = getDataWithCaching("key1", dataCache);
System.out.println("First call for 'key2':");
String data3 = getDataWithCaching("key2", dataCache);
}
public static String getDataWithCaching(String key, Map<String, String> cache) {
// Check if data is in cache
if (cache.containsKey(key)) {
System.out.println("Cache hit for " + key);
return cache.get(key);
}
// Simulate expensive operation
System.out.println("Cache miss for " + key + ", retrieving data...");
String data = fetchDataFromDatabase(key);
// Store in cache for future use
cache.put(key, data);
return data;
}
public static String fetchDataFromDatabase(String key) {
// Simulate database access
try {
Thread.sleep(1000); // Simulate delay
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Data for " + key;
}
}
5. Counting and Statistics
Collections can be used for counting occurrences and gathering statistics:
import java.util.*;
public class CountingDemo {
public static void main(String[] args) {
String text = "to be or not to be that is the question";
String[] words = text.split(" ");
// Count word frequencies
Map<String, Integer> wordCounts = new HashMap<>();
for (String word : words) {
// Increment count or initialize to 1 if not present
wordCounts.put(word, wordCounts.getOrDefault(word, 0) + 1);
}
System.out.println("Word counts: " + wordCounts);
// Find the most frequent word
String mostFrequentWord = Collections.max(
wordCounts.entrySet(),
Map.Entry.comparingByValue()
).getKey();
System.out.println("Most frequent word: " + mostFrequentWord +
" (appears " + wordCounts.get(mostFrequentWord) + " times)");
}
}
📝 Summary and Key Takeaways
The Java Collections Framework provides a comprehensive set of interfaces and classes for storing and manipulating groups of objects. Here are the key takeaways:
-
Core Interfaces:
Collection
is the root interface, withMap
as a separate interface for key-value pairs. -
Common Implementations:
- Lists:
ArrayList
,LinkedList
- Sets:
HashSet
,TreeSet
,LinkedHashSet
- Maps:
HashMap
,TreeMap
,LinkedHashMap
- Queues:
PriorityQueue
,ArrayDeque
- Lists:
-
Utility Classes:
Collections
andArrays
provide useful algorithms and methods for working with collections. -
Comparison Interfaces:
Comparable
andComparator
enable sorting and ordering of objects. -
Best Practices:
- Choose the right collection type for your needs
- Use generics for type safety
- Override
equals()
andhashCode()
when using objects as keys or in sets - Use enhanced for loops or iterators for cleaner code
- Use
removeIf()
for safe removal during iteration
-
Common Pitfalls to Avoid:
- Modifying collections during iteration without using an iterator
- Failing to override
equals()
andhashCode()
- Not understanding the limitations of collection views
- Misunderstanding the behavior of
Arrays.asList()
By mastering the Collections Framework, you'll have powerful tools at your disposal for solving a wide range of programming problems efficiently.
🏋️ Exercises and Mini-Projects
Exercise 1: Word Frequency Counter
Create a program that reads a text file and counts the frequency of each word, then displays the top 10 most common words.
import java.io.*;
import java.util.*;
import java.util.stream.*;
public class WordFrequencyCounter {
public static void main(String[] args) {
// Sample text (in a real program, this would come from a file)
String text = "It was the best of times, it was the worst of times, " +
"it was the age of wisdom, it was the age of foolishness, " +
"it was the epoch of belief, it was the epoch of incredulity, " +
"it was the season of Light, it was the season of Darkness.";
// Convert to lowercase and split by non-word characters
String[] words = text.toLowerCase().split("\\W+");
// Count word frequencies
Map<String, Integer> wordCounts = new HashMap<>();
for (String word : words) {
if (!word.isEmpty()) { // Skip empty strings
wordCounts.put(word, wordCounts.getOrDefault(word, 0) + 1);
}
}
// Find the top 10 most frequent words
List<Map.Entry<String, Integer>> sortedEntries = wordCounts.entrySet()
.stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.limit(10)
.collect(Collectors.toList());
// Display results
System.out.println("Top 10 most frequent words:");
System.out.printf("%-15s %s%n", "Word", "Frequency");
System.out.println("-".repeat(30));
for (Map.Entry<String, Integer> entry : sortedEntries) {
System.out.printf("%-15s %d%n", entry.getKey(), entry.getValue());
}
}
}
Output:
Top 10 most frequent words:
Word Frequency
------------------------------
it 8
was 8
the 8
of 8
times 2
age 2
epoch 2
season 2
best 1
worst 1
Practice Exercise: Modify the program to:
- Read from an actual text file
- Ignore common words (like "the", "a", "an")
- Display the results as a simple bar chart
Mini-Project: Contact Management System
Let's create a simple contact management system that demonstrates the use of various collections:
import java.util.*;
import java.util.stream.*;
class Contact {
private String name;
private String email;
private String phone;
private Set<String> tags;
public Contact(String name, String email, String phone) {
this.name = name;
this.email = email;
this.phone = phone;
this.tags = new HashSet<>();
}
// Getters and setters
public String getName() { return name; }
public String getEmail() { return email; }
public String getPhone() { return phone; }
public Set<String> getTags() { return Collections.unmodifiableSet(tags); }
public void addTag(String tag) {
tags.add(tag.toLowerCase());
}
public boolean hasTag(String tag) {
return tags.contains(tag.toLowerCase());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Contact contact = (Contact) o;
return Objects.equals(email, contact.email);
}
@Override
public int hashCode() {
return Objects.hash(email);
}
@Override
public String toString() {
return String.format("%-20s %-30s %-15s Tags: %s",
name, email, phone, String.join(", ", tags));
}
}
class ContactManager {
private Map<String, Contact> contactsByEmail;
private Map<String, Set<Contact>> contactsByTag;
public ContactManager() {
contactsByEmail = new HashMap<>();
contactsByTag = new HashMap<>();
}
public void addContact(Contact contact) {
contactsByEmail.put(contact.getEmail(), contact);
// Update tag index
for (String tag : contact.getTags()) {
contactsByTag.computeIfAbsent(tag, k -> new HashSet<>()).add(contact);
}
}
public void removeContact(String email) {
Contact contact = contactsByEmail.remove(email);
if (contact != null) {
// Remove from tag index
for (String tag : contact.getTags()) {
Set<Contact> contacts = contactsByTag.get(tag);
if (contacts != null) {
contacts.remove(contact);
if (contacts.isEmpty()) {
contactsByTag.remove(tag);
}
}
}
}
}
public Contact findByEmail(String email) {
return contactsByEmail.get(email);
}
public List<Contact> findByTag(String tag) {
Set<Contact> contacts = contactsByTag.getOrDefault(tag.toLowerCase(), Collections.emptySet());
return new ArrayList<>(contacts);
}
public List<Contact> findByName(String namePart) {
return contactsByEmail.values().stream()
.filter(c -> c.getName().toLowerCase().contains(namePart.toLowerCase()))
.collect(Collectors.toList());
}
public void addTagToContact(String email, String tag) {
Contact contact = contactsByEmail.get(email);
if (contact != null) {
String lowerTag = tag.toLowerCase();
if (!contact.hasTag(lowerTag)) {
contact.addTag(lowerTag);
contactsByTag.computeIfAbsent(lowerTag, k -> new HashSet<>()).add(contact);
}
}
}
public List<Contact> getAllContacts() {
List<Contact> allContacts = new ArrayList<>(contactsByEmail.values());
allContacts.sort(Comparator.comparing(Contact::getName));
return allContacts;
}
public Set<String> getAllTags() {
return new TreeSet<>(contactsByTag.keySet());
}
}
public class ContactManagementSystem {
public static void main(String[] args) {
ContactManager manager = new ContactManager();
// Add some contacts
Contact john = new Contact("John Smith", "john@example.com", "555-1234");
john.addTag("friend");
john.addTag("work");
Contact alice = new Contact("Alice Johnson", "alice@example.com", "555-5678");
alice.addTag("family");
Contact bob = new Contact("Bob Williams", "bob@example.com", "555-9012");
bob.addTag("work");
manager.addContact(john);
manager.addContact(alice);
manager.addContact(bob);
// Display all contacts
System.out.println("All contacts:");
for (Contact contact : manager.getAllContacts()) {
System.out.println(contact);
}
// Find contacts by tag
System.out.println("\nWork contacts:");
List<Contact> workContacts = manager.findByTag("work");
for (Contact contact : workContacts) {
System.out.println(contact);
}
// Find contact by email
System.out.println("\nLooking up by email:");
Contact foundContact = manager.findByEmail("alice@example.com");
System.out.println(foundContact);
// Add a tag to a contact
System.out.println("\nAdding 'important' tag to Alice:");
manager.addTagToContact("alice@example.com", "important");
foundContact = manager.findByEmail("alice@example.com");
System.out.println(foundContact);
// Find by name
System.out.println("\nContacts with 'john' in the name:");
List<Contact> johnContacts = manager.findByName("john");
for (Contact contact : johnContacts) {
System.out.println(contact);
}
// Show all tags
System.out.println("\nAll tags: " + manager.getAllTags());
// Remove a contact
System.out.println("\nRemoving Bob:");
manager.removeContact("bob@example.com");
System.out.println("\nAll contacts after removal:");
for (Contact contact : manager.getAllContacts()) {
System.out.println(contact);
}
}
}
Output:
All contacts:
Alice Johnson alice@example.com 555-5678 Tags: family
Bob Williams bob@example.com 555-9012 Tags: work
John Smith john@example.com 555-1234 Tags: friend, work
Work contacts:
Bob Williams bob@example.com 555-9012 Tags: work
John Smith john@example.com 555-1234 Tags: friend, work
Looking up by email:
Alice Johnson alice@example.com 555-5678 Tags: family
Adding 'important' tag to Alice:
Alice Johnson alice@example.com 555-5678 Tags: family, important
Contacts with 'john' in the name:
John Smith john@example.com 555-1234 Tags: friend, work
All tags: [family, friend, important, work]
Removing Bob:
All contacts after removal:
Alice Johnson alice@example.com 555-5678 Tags: family, important
John Smith john@example.com 555-1234 Tags: friend, work
Practice Exercise: Extend the Contact Management System to:
- Add persistence (save/load contacts to/from a file)
- Create a simple command-line interface for user interaction
- Add more search capabilities (e.g., by phone number, partial matches)
- Implement contact groups (a contact can belong to multiple groups)
📚 Further Reading and Resources
To deepen your understanding of the Java Collections Framework, here are some recommended resources:
Official Documentation
Books
- "Effective Java" by Joshua Bloch (especially the chapters on collections and generics)
- "Java Generics and Collections" by Maurice Naftalin and Philip Wadler
- "Java Performance: The Definitive Guide" by Scott Oaks (for understanding collection performance characteristics)
Advanced Topics
For more advanced usage, explore:
- Concurrent collections in
java.util.concurrent
- The Stream API for functional-style operations on collections
- Custom collection implementations for specialized needs
🔍 Glossary of Terms
- Collection: A group of objects, known as elements.
- List: An ordered collection that allows duplicate elements.
- Set: A collection that does not allow duplicate elements.
- Map: A collection of key-value pairs where keys are unique.
- Queue: A collection designed for holding elements prior to processing.
- Deque: A double-ended queue that supports element insertion and removal at both ends.
- Iterator: An object that enables traversal through a collection.
- Comparable: An interface for objects that have a natural ordering.
- Comparator: An interface for defining custom ordering of objects.
- Hash Code: A numeric value used to identify objects in collections like HashMap and HashSet.
- Load Factor: A measure of how full a hash table is allowed to get before its capacity is automatically increased.
- Fail-Fast: Behavior of iterators that throw ConcurrentModificationException if the collection is modified during iteration.
- Fail-Safe: Behavior of iterators that don't throw exceptions when the collection is modified during iteration.
By mastering the Java Collections Framework, you'll have a powerful toolkit for solving a wide range of programming problems efficiently and elegantly.