Generic Collections in Java
Introduction
In Java, arrays have fixed sizes, which can be restrictive when dealing with dynamic data. The Java Collections Framework provides an architecture to store and manipulate a group of objects. With the introduction of Generics in Java 5, collections became type-safe. Generic Collections allow developers to define collections that can only hold a specific type of object, eliminating the need for casting and reducing runtime errors (ClassCastException).
This guide explores the various generic collections available in Java, focusing on their usage, performance characteristics, and internal workings.
Core Concept: Deep Dive and Architecture
What is a Generic Collection?
Before generics, a standard Java ArrayList stored Object types. You could put a String and an Integer in the same list, which could lead to crashes if not carefully managed. Generic collections enforce compile-time type checking.
When you declare List<String> list = new ArrayList<>();, you are explicitly stating that this list will only contain strings. The compiler guarantees this contract.
Type Erasure
It is crucial to understand that Java implements generics using Type Erasure. This means the generic type information is only available at compile time. The Java compiler replaces all generic types with their bounds or Object if they are unbounded. This was done to ensure backward compatibility with older Java versions that didn't have generics.
Because of type erasure, you cannot determine the generic type of a collection at runtime (e.g., list instanceof List<String> is illegal).
Core Collection Interfaces
The Java Collections Framework is built around several core interfaces:
List<E>: An ordered collection (also known as a sequence). Examples:ArrayList<E>,LinkedList<E>.Set<E>: A collection that contains no duplicate elements. Examples:HashSet<E>,TreeSet<E>.Map<K, V>: An object that maps keys to values. Examples:HashMap<K, V>,TreeMap<K, V>.
Code Examples
1. The List Interface (ArrayList)
ArrayList is the most commonly used list implementation, backed by a dynamic array.
import java.util.ArrayList;
import java.util.List;
public class ListExample {
public static void main(String[] args) {
// Creating a generic list of strings
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
// names.add(10); // Compile-time error!
for (String name : names) {
System.out.println(name); // No casting required
}
}
}
2. The Set Interface (HashSet)
HashSet is used when you want a collection of unique elements. It is backed by a HashMap internally.
import java.util.HashSet;
import java.util.Set;
public class SetExample {
public static void main(String[] args) {
Set<Integer> uniqueNumbers = new HashSet<>();
uniqueNumbers.add(10);
uniqueNumbers.add(20);
uniqueNumbers.add(10); // Duplicate, will be ignored
System.out.println("Set size: " + uniqueNumbers.size()); // Outputs 2
}
}
3. The Map Interface (HashMap)
HashMap stores key-value pairs. It uses a hash table internally for fast lookups.
import java.util.HashMap;
import java.util.Map;
public class MapExample {
public static void main(String[] args) {
Map<String, Integer> wordCounts = new HashMap<>();
wordCounts.put("apple", 1);
wordCounts.put("banana", 2);
int count = wordCounts.getOrDefault("apple", 0);
System.out.println("Apple count: " + count);
}
}
Advanced Features
Bounded Wildcards in Collections
Sometimes you want a method to accept a collection of a certain type or any of its subclasses. This is where wildcards come into play.
- Upper Bounded Wildcard
<? extends T>: Allows reading. Represents a type that isTor a subclass ofT. - Lower Bounded Wildcard
<? super T>: Allows writing. Represents a type that isTor a superclass ofT.
public void printNumbers(List<? extends Number> numbers) {
for (Number n : numbers) {
System.out.println(n);
}
}
// Can be called with List<Integer> or List<Double>
Common Mistakes Beginners Make
1. Using Raw Types
The Mistake: Writing List list = new ArrayList(); instead of List<String> list = new ArrayList<>();.
The Fix: Always use generic type parameters to get compile-time safety and avoid ClassCastException.
2. Modifying Collections While Iterating
The Mistake: Using a standard for-each loop to remove elements from a collection, causing a ConcurrentModificationException.
The Fix: Use an explicit Iterator and its remove() method, or use Java 8's removeIf() method.
Summary
- Generic Collections provide type safety and eliminate the need for casting.
- The core interfaces are List, Set, and Map.
- Java implements generics using Type Erasure, meaning generic type information is removed at compile time.
- Use wildcards (
? extends T,? super T) to increase API flexibility when writing methods that process collections.