Index

Core Interfaces and Their Implementations

Java Collections

2.1 Interface Overview

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.

The Role of the Collection Interface

The 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.

Key Methods in the Collection Interface

Polymorphism in Action

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.

Summary

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.

Index

2.2 Interface and Implementations (ArrayList, LinkedList)

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.

Characteristics of List

ArrayList vs LinkedList: Internal Structures

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)

Typical Use Cases

Runnable Code Example

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);
        }
    }
}

Explanation of the Code

The output will be:

ArrayList contents:
Apple
Date
Cherry

LinkedList contents:
Apple
Date
Cherry

Summary

Both ArrayList and LinkedList implement the List interface but differ internally and in performance:

Understanding these differences helps you choose the right list implementation for your specific needs.

Index

2.3 Interface and Implementations (HashSet, LinkedHashSet, TreeSet)

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.

Uniqueness and Ordering in Sets

One of the main distinctions among Set implementations is how they handle element ordering:

Java provides three commonly used Set implementations, each with a different ordering behavior:

HashSet

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]

LinkedHashSet

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]

TreeSet

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]
Click to view full runnable Code

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);
    }
}

Summary and When to Use Which

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

Practical Advice

By understanding these differences, you can choose the appropriate Set implementation to meet your program’s needs efficiently and clearly.

Index

2.4 Interface and Implementations (LinkedList, PriorityQueue)

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.

Common Queue Implementations

Two widely used implementations of the Queue interface are:

FIFO Behavior with LinkedList

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.

Priority Ordering with PriorityQueue

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).

Use Cases

Summary

By understanding these two implementations, you can effectively manage tasks and events in your Java applications.

Index

2.5 Examples: Creating and manipulating Lists, Sets, and Queues

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
    }
}

Explanation and Tips

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.

Index