At the heart of the Java Collections Framework lies the Collection
interface. This interface serves as the fundamental building block for most collection types in Java, defining the core operations that all collections must support. Understanding Collection
is key to mastering Java’s collection hierarchy and how different collections relate to each other.
Collection
InterfaceThe Collection
interface models a group of objects, known as elements, and specifies common behaviors such as adding, removing, checking membership, and querying the size of the collection. It acts as a contract that all concrete collection classes must fulfill, allowing them to be used interchangeably through polymorphism.
For example, List
, Set
, and Queue
interfaces all extend Collection
, inheriting its basic methods while adding their own specialized behaviors. This design means you can write code that works with any collection type simply by referencing the Collection
interface, making your programs more flexible and reusable.
Collection
Interfaceboolean add(E e)
Adds the specified element to the collection. Returns true
if the collection changed as a result.
boolean remove(Object o)
Removes a single instance of the specified element, if present. Returns true
if an element was removed.
boolean contains(Object o)
Returns true
if the collection contains at least one instance of the specified element.
int size()
Returns the number of elements in the collection.
boolean isEmpty()
Returns true
if the collection contains no elements.
Iterator<E> iterator()
Returns an iterator over the elements, enabling traversal.
Because many collection types share these core methods, you can write flexible code using the Collection
interface type. For example:
import java.util.*;
public class CollectionPolymorphism {
public static void printCollection(Collection<String> col) {
System.out.println("Collection size: " + col.size());
for (String item : col) {
System.out.println(item);
}
}
public static void main(String[] args) {
Collection<String> list = new ArrayList<>();
Collection<String> set = new HashSet<>();
list.add("Apple");
list.add("Banana");
list.add("Apple"); // Lists allow duplicates
set.add("Apple");
set.add("Banana");
set.add("Apple"); // Sets ignore duplicates
System.out.println("List:");
printCollection(list);
System.out.println("\nSet:");
printCollection(set);
}
}
Here, the method printCollection
accepts any collection that implements Collection
. The same method works for both a List
and a Set
, illustrating polymorphism enabled by the shared interface.
The Collection
interface forms the backbone of Java’s collections hierarchy, defining essential operations and allowing diverse implementations to be used interchangeably. Its well-defined contracts such as add
, remove
, contains
, and size
provide a common language for manipulating groups of objects, which higher-level interfaces extend with more specific behaviors. Mastering this interface is your first step to leveraging the full power of Java Collections.
The List
interface in Java represents an ordered collection that allows duplicate elements. Elements in a List
have a specific position (index), and you can access, insert, or remove elements based on their index. This makes List
ideal for use cases where order matters, such as maintaining sequences of items, playlists, or queues.
Two of the most common List
implementations in Java are ArrayList
and LinkedList
. While they both implement the List
interface and support the same core operations, their internal structures and performance characteristics differ significantly.
List
get(int index)
.Feature | ArrayList | LinkedList |
---|---|---|
Internal Data Structure | Resizable array | Doubly linked list |
Access by index | Fast (O(1)) | Slow (O(n)) |
Insert/remove at end | Fast amortized (O(1)) | Fast (O(1)) |
Insert/remove at middle | Slow (O(n), due to shifting) | Fast (O(1) after traversal) |
Memory overhead | Lower (just array storage) | Higher (nodes store references) |
ArrayList
stores elements in a contiguous array. This allows fast random access because the position directly corresponds to an array index. However, inserting or removing elements in the middle requires shifting subsequent elements, which is slower.
LinkedList
stores elements as nodes connected by pointers to the next and previous nodes. Accessing an element by index requires traversing the list, making it slower. But adding or removing elements at the beginning or middle is efficient because it involves only updating pointers.
Use ArrayList
when:
Use LinkedList
when:
import java.util.*;
public class ListExample {
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
List<String> linkedList = new LinkedList<>();
// Adding elements
arrayList.add("Apple");
arrayList.add("Banana");
arrayList.add("Cherry");
linkedList.addAll(arrayList);
// Inserting element at index 1
arrayList.add(1, "Date");
linkedList.add(1, "Date");
// Removing element by value
arrayList.remove("Banana");
linkedList.remove("Banana");
// Iterating and printing elements
System.out.println("ArrayList contents:");
for (String fruit : arrayList) {
System.out.println(fruit);
}
System.out.println("\nLinkedList contents:");
for (String fruit : linkedList) {
System.out.println(fruit);
}
}
}
ArrayList
and a LinkedList
, adding the same elements to both."Date"
at index 1 in both lists."Banana"
by value.The output will be:
ArrayList contents:
Apple
Date
Cherry
LinkedList contents:
Apple
Date
Cherry
Both ArrayList
and LinkedList
implement the List
interface but differ internally and in performance:
ArrayList
is generally preferred for most applications due to fast random access and lower memory use.LinkedList
can be advantageous when your application requires frequent insertions or removals from anywhere other than the end.Understanding these differences helps you choose the right list implementation for your specific needs.
The Set
interface in Java represents a collection that contains no duplicate elements. Unlike List
, which allows duplicates, a Set
ensures uniqueness, meaning that if you try to add an element that already exists in the set, the operation will not change the set.
One of the main distinctions among Set
implementations is how they handle element ordering:
Set
implementations do not guarantee any particular order of elements.Java provides three commonly used Set
implementations, each with a different ordering behavior:
O(1)
) average performance for add, remove, and contains operations.Use case: When you want fast operations and don’t care about the order of elements.
Example:
Set<String> hashSet = new HashSet<>();
hashSet.add("Banana");
hashSet.add("Apple");
hashSet.add("Orange");
hashSet.add("Apple"); // Duplicate ignored
System.out.println("HashSet: " + hashSet);
Output (order may vary):
HashSet: [Banana, Orange, Apple]
HashSet
due to linked list overhead but still provides constant-time operations.Use case: When you need uniqueness and want to maintain the order in which elements were added.
Example:
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("Banana");
linkedHashSet.add("Apple");
linkedHashSet.add("Orange");
linkedHashSet.add("Apple"); // Duplicate ignored
System.out.println("LinkedHashSet: " + linkedHashSet);
Output:
LinkedHashSet: [Banana, Apple, Orange]
Comparator
if provided).O(log n)
) for add, remove, and contains operations.Use case: When you need uniqueness and sorted elements.
Example:
Set<String> treeSet = new TreeSet<>();
treeSet.add("Banana");
treeSet.add("Apple");
treeSet.add("Orange");
treeSet.add("Apple"); // Duplicate ignored
System.out.println("TreeSet: " + treeSet);
Output:
TreeSet: [Apple, Banana, Orange]
import java.util.*;
public class SetOrderingDemo {
public static void main(String[] args) {
// HashSet example: no guaranteed order
Set<String> hashSet = new HashSet<>();
hashSet.add("Banana");
hashSet.add("Apple");
hashSet.add("Orange");
hashSet.add("Apple"); // Duplicate ignored
System.out.println("HashSet (no specific order): " + hashSet);
// LinkedHashSet example: maintains insertion order
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("Banana");
linkedHashSet.add("Apple");
linkedHashSet.add("Orange");
linkedHashSet.add("Apple"); // Duplicate ignored
System.out.println("LinkedHashSet (insertion order): " + linkedHashSet);
// TreeSet example: natural sorted order
Set<String> treeSet = new TreeSet<>();
treeSet.add("Banana");
treeSet.add("Apple");
treeSet.add("Orange");
treeSet.add("Apple"); // Duplicate ignored
System.out.println("TreeSet (sorted order): " + treeSet);
}
}
Implementation | Ordering | Performance | Use Case |
---|---|---|---|
HashSet |
No order | Fast (O(1)) | Fast operations, order not needed |
LinkedHashSet |
Insertion order | Slightly slower | Maintain insertion order |
TreeSet |
Sorted order | Slower (O(log n)) | Need sorted unique elements |
HashSet
when performance is critical and order doesn’t matter.LinkedHashSet
when you want to preserve the order in which elements were added, such as keeping a history of user actions.TreeSet
when you need your set to be automatically sorted, like maintaining a sorted list of usernames or numbers.By understanding these differences, you can choose the appropriate Set
implementation to meet your program’s needs efficiently and clearly.
The Queue
interface in Java represents a collection designed for holding elements prior to processing, typically following the FIFO (First-In, First-Out) principle. This means that elements are added (enqueued) at the end of the queue and removed (dequeued) from the front, similar to a line of customers waiting their turn.
Two widely used implementations of the Queue
interface are:
LinkedList
: A versatile doubly linked list that implements both List
and Queue
. It supports FIFO behavior, allowing you to add elements at the tail and remove them from the head efficiently.
PriorityQueue
: A specialized queue that orders elements not just by insertion order but by their priority. The element with the highest priority (according to natural ordering or a provided comparator) is dequeued first, regardless of when it was added.
Using LinkedList
as a queue is straightforward:
import java.util.*;
public class QueueExample {
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
// Enqueue elements
queue.add("Task1");
queue.add("Task2");
queue.add("Task3");
System.out.println("Queue: " + queue);
// Dequeue elements (FIFO)
String first = queue.poll(); // Removes and returns head
System.out.println("Dequeued: " + first);
System.out.println("Queue after dequeue: " + queue);
}
}
Output:
Queue: [Task1, Task2, Task3]
Dequeued: Task1
Queue after dequeue: [Task2, Task3]
Here, add()
adds elements at the end of the queue, and poll()
removes elements from the front, ensuring the first element added is the first removed.
Unlike LinkedList
, PriorityQueue
does not guarantee FIFO order. Instead, it organizes elements according to their priority — the smallest element (by natural ordering or comparator) is always dequeued first.
Example:
import java.util.*;
public class PriorityQueueExample {
public static void main(String[] args) {
PriorityQueue<Integer> pq = new PriorityQueue<>();
// Enqueue elements with different priorities (values)
pq.add(50);
pq.add(10);
pq.add(30);
System.out.println("PriorityQueue: " + pq);
// Dequeue elements by priority
while (!pq.isEmpty()) {
System.out.println("Dequeued: " + pq.poll());
}
}
}
Output:
PriorityQueue: [10, 50, 30]
Dequeued: 10
Dequeued: 30
Dequeued: 50
Despite adding 50
first, 10
is dequeued first because it has the highest priority (lowest numeric value).
LinkedList
as Queue: Suitable for simple task scheduling where tasks must be processed in the order they arrive, such as printing jobs or customer service lines.
PriorityQueue
: Ideal for scenarios where certain tasks require higher priority handling, like CPU job scheduling, event-driven systems, or any situation where tasks have varying importance.
Queue
interface models FIFO behavior, primarily implemented by LinkedList
.PriorityQueue
offers priority-based ordering, breaking FIFO for prioritized processing.add
/offer
) and dequeue (poll
/remove
).By understanding these two implementations, you can effectively manage tasks and events in your Java applications.
import java.util.*;
public class CollectionsExamples {
public static void main(String[] args) {
// === List Example ===
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Apple"); // Lists allow duplicates
System.out.println("List contents (allows duplicates, maintains order):");
for (String fruit : list) {
System.out.println(fruit);
}
// Remove element by value
list.remove("Apple"); // removes first occurrence
System.out.println("List after removing one 'Apple': " + list);
// === Set Example ===
Set<String> set = new HashSet<>();
set.add("Apple");
set.add("Banana");
set.add("Apple"); // Duplicate ignored in Set
System.out.println("\nSet contents (no duplicates, no guaranteed order):");
for (String fruit : set) {
System.out.println(fruit);
}
// Attempt to remove element not present
boolean removed = set.remove("Orange"); // returns false if element not found
System.out.println("Attempt to remove 'Orange' from set: " + removed);
// === Queue Example ===
Queue<String> queue = new LinkedList<>();
queue.add("Task1");
queue.add("Task2");
queue.add("Task3");
System.out.println("\nQueue contents (FIFO order): " + queue);
// Process elements in FIFO order
String task = queue.poll(); // Retrieves and removes the head
System.out.println("Polled from queue: " + task);
System.out.println("Queue after polling: " + queue);
// Common pitfall: Using remove() without checking emptiness causes exception
while (!queue.isEmpty()) {
System.out.println("Processing " + queue.remove());
}
// If we tried queue.remove() again now, it would throw NoSuchElementException
}
}
List: Allows duplicates and maintains insertion order. When removing by value (remove("Apple")
), only the first matching element is removed. Tip: Use list.removeIf()
or loops to remove all occurrences if needed.
Set: Enforces uniqueness, so duplicates are ignored on insertion. Ordering is unpredictable with HashSet
. Tip: If order matters, consider LinkedHashSet
or TreeSet
.
Queue: Maintains FIFO order. Use add()
or offer()
to enqueue, and poll()
or remove()
to dequeue. Tip: poll()
returns null
if the queue is empty, but remove()
throws an exception—always check with isEmpty()
before removing.
These simple examples demonstrate how the core collections behave differently with respect to duplicates, ordering, and element processing. They also highlight some common methods and pitfalls to watch for when manipulating these collections in real applications.