Java Collections Framework

Imagine you're in a busy kitchen, using different tools to prepare a meal—this is similar to the Java Collections Framework. It provides a set of classes and interfaces to store, manage, and retrieve data in your programs. Just like you have different kitchen tools for different tasks, the framework offers collections like Lists, Sets, and Maps for various needs. Each collection is designed for specific tasks, ensuring your program runs well.

Lets deep dive into them now-

Custom Classes

In Java, a custom class is a user-defined class that you create to represent your own data type. Custom classes are often used when the data you need to manage doesn't fit into the standard Java data types like Integer, String, etc. For example, if you want to store Person objects in a collection, you need a custom Person class.

public class Employee {
    private String name;
    private int age;
    private String department;

    // Constructor
    public Employee(String name, int age, String department) {
        this.name = name;
        this.age = age;
        this.department = department;
    }

    // Getters and setters
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getDepartment() {
        return department;
    }

    public void setDepartment(String department) {
        this.department = department;
    }

    @Override
    public String toString() {
        return "Employee [name=" + name + ", age=" + age + ", department=" + department + "]";
    }
}

// You can store and work with Employee objects in collections like a List:

public static void main (String[]args) {

List<Employee> employees = new ArrayList<>();
employees.add(new Employee("Alice", 30, "HR"));
employees.add(new Employee("Bob", 25, "IT"));
employees.add(new Employee("Charlie", 28, "Marketing"));
}

Collections Interface

A. List

A List is an ordered collection that allows duplicate elements and provides access via indices.

Types:

  • ArrayList: A dynamic array that allows fast access to elements by index.

  • LinkedList: A doubly-linked list that provides efficient insertion and removal of elements from both ends.

  • Stack: A LIFO (Last In, First Out) implementation that stores elements in a stack structure.

  • Vector: Similar to ArrayList but is synchronized, making it thread-safe.

"Thread-safe" means that multiple threads (or parts of a program running at the same time) can access the data without causing errors or corrupting the data. In a thread-safe collection like Vector, the program makes sure that only one thread can modify the data at a time, preventing conflicts.

import java.util.*;

public class ListOperationsExample {

    public static void main(String[] args) {

        // 1. Operations on ArrayList
        List<String> arrayList = new ArrayList<>();

        // Adding elements
        arrayList.add("Apple");
        arrayList.add("Banana");
        arrayList.add("Cherry");

        // Adding an element at a specific index
        arrayList.add(1, "Orange");

        // Accessing elements by index
        System.out.println("ArrayList - Element at index 2: " + arrayList.get(2)); // Cherry

        // Checking if the list contains a specific element
        System.out.println("ArrayList contains 'Banana'? " + arrayList.contains("Banana")); // true

        // Removing an element by value
        arrayList.remove("Banana");

        // Removing an element by index
        arrayList.remove(0);  // Removes "Apple"

        // Checking size of the ArrayList
        System.out.println("ArrayList size after removal: " + arrayList.size()); // 2

        // Iterating over the ArrayList
        System.out.println("ArrayList elements:");
        for (String fruit : arrayList) {
            System.out.println(fruit);
        }

        System.out.println();

        // 2. Operations on LinkedList
        List<String> linkedList = new LinkedList<>();

        // Adding elements
        linkedList.add("Dog");
        linkedList.add("Cat");
        linkedList.add("Elephant");

        // Adding an element at the beginning
        linkedList.addFirst("Lion");

        // Adding an element at the end
        linkedList.addLast("Tiger");

        // Accessing elements by index
        System.out.println("LinkedList - Element at index 3: " + linkedList.get(3)); // Elephant

        // Removing an element by value
        linkedList.remove("Cat");

        // Removing an element by index
        linkedList.remove(2);  // Removes "Elephant"

        // Removing the first and last elements
        linkedList.removeFirst();
        linkedList.removeLast();

        // Checking size of the LinkedList
        System.out.println("LinkedList size after removals: " + linkedList.size()); // 1

        // Iterating over the LinkedList
        System.out.println("LinkedList elements:");
        for (String animal : linkedList) {
            System.out.println(animal);
        }

        System.out.println();

        // 3. Operations on Stack
        Stack<String> stack = new Stack<>();

        // Pushing elements onto the stack
        stack.push("Red");
        stack.push("Green");
        stack.push("Blue");

        // Peeking at the top element (without removing it)
        System.out.println("Stack - Peek top element: " + stack.peek()); // Blue

        // Popping elements from the stack (LIFO order)
        System.out.println("Stack - Popped element: " + stack.pop()); // Blue
        System.out.println("Stack - Popped element: " + stack.pop()); // Green

        // Checking if the stack contains an element
        System.out.println("Stack contains 'Red'? " + stack.contains("Red")); // true

        // Checking if the stack is empty
        System.out.println("Is the stack empty? " + stack.isEmpty()); // false

        // Popping the last element
        System.out.println("Stack - Popped element: " + stack.pop()); // Red

        // Checking if the stack is empty now
        System.out.println("Is the stack empty now? " + stack.isEmpty()); // true
    }
}

//ArrayList - Element at index 2: Cherry
//ArrayList contains 'Banana'? true
//ArrayList size after removal: 2
//ArrayList elements:
//Orange
//Cherry

//LinkedList - Element at index 3: Elephant
//LinkedList size after removals: 1
//LinkedList elements:
//Lion

//Stack - Peek top element: Blue
//Stack - Popped element: Blue
//Stack - Popped element: Green
//Stack contains 'Red'? true
//Is the stack empty? false
//Stack - Popped element: Red
//Is the stack empty now? true

Understanding those Operations

1. ArrayList Operations:

  • Adding elements: add() adds elements to the end of the list.

  • Adding at a specific index: add(index, element) inserts an element at a given index.

  • Accessing by index: get(index) retrieves an element at a specified position.

  • Checking if contains a value: contains(element) checks if the list contains a specific element.

  • Removing by value: remove(element) removes the first occurrence of the specified element.

  • Removing by index: remove(index) removes the element at the specified index.

  • Size of the list: size() returns the number of elements in the list.

  • Iterating over the list: Using a for-each loop to print all elements.

2. LinkedList Operations:

  • Adding elements: add() adds an element at the end of the list.

  • Adding at the beginning: addFirst(element) adds an element at the front.

  • Adding at the end: addLast(element) adds an element at the end.

  • Accessing by index: get(index) retrieves an element at a specified position.

  • Removing by value: remove(element) removes the first occurrence of the specified element.

  • Removing by index: remove(index) removes the element at the specified index.

  • Removing the first/last element: removeFirst() and removeLast() remove elements from the start and end of the list.

  • Size of the list: size() returns the number of elements in the list.

3. Stack Operations:

  • Pushing elements: push(element) adds elements to the top of the stack.

  • Peeking at the top element: peek() allows you to look at the top element without removing it.

  • Popping elements: pop() removes and returns the top element of the stack.

  • Checking if contains an element: contains(element) checks if the stack has a specific element.

  • Checking if the stack is empty: isEmpty() checks if the stack has no elements.

B. Set

A Set is an unordered collection that does not allow duplicate elements. It’s ideal for storing unique items.

Implementations:

  • HashSet: A set that uses hashing to store elements. It provides fast lookups but does not maintain any order.

  • TreeSet: A set that uses a red-black tree to store elements in a sorted order.

When to Use:

  • HashSet: When you want fast lookups and don't care about the order of elements.

  • TreeSet: When you need elements to be ordered (either natural order or custom order).

import java.util.HashSet;
import java.util.TreeSet;

public class SetExample {

    public static void main(String[] args) {
        // Example using HashSet
        System.out.println("HashSet Example:");
        HashSet<String> hashSet = new HashSet<>();

        // Adding elements
        hashSet.add("Apple");
        hashSet.add("Banana");
        hashSet.add("Cherry");
        hashSet.add("Apple"); // Duplicate element, will not be added

        // Display the HashSet (order is not guaranteed)
        System.out.println("HashSet elements: " + hashSet);

        // Checking for an element
        System.out.println("HashSet contains 'Apple': " + hashSet.contains("Apple"));
        System.out.println("HashSet contains 'Mango': " + hashSet.contains("Mango"));

        // Removing an element
        hashSet.remove("Banana");
        System.out.println("HashSet after removing 'Banana': " + hashSet);

        // Iterating over HashSet
        System.out.println("Iterating over HashSet:");
        for (String fruit : hashSet) {
            System.out.println(fruit);
        }

        // Size of HashSet
        System.out.println("Size of HashSet: " + hashSet.size());

        // Clearing the HashSet
        hashSet.clear();
        System.out.println("HashSet after clearing: " + hashSet);
        System.out.println();

        // Example using TreeSet
        System.out.println("TreeSet Example:");
        TreeSet<Integer> treeSet = new TreeSet<>();

        // Adding elements
        treeSet.add(15);
        treeSet.add(10);
        treeSet.add(30);
        treeSet.add(20);
        treeSet.add(10); // Duplicate element, will not be added

        // Display the TreeSet (elements are sorted)
        System.out.println("TreeSet elements: " + treeSet);

        // Checking for an element
        System.out.println("TreeSet contains 15: " + treeSet.contains(15));
        System.out.println("TreeSet contains 50: " + treeSet.contains(50));

        // Removing an element
        treeSet.remove(10);
        System.out.println("TreeSet after removing 10: " + treeSet);

        // Iterating over TreeSet
        System.out.println("Iterating over TreeSet:");
        for (Integer number : treeSet) {
            System.out.println(number);
        }

        // Using higher() and lower()
        System.out.println("Element higher than 20: " + treeSet.higher(20));
        System.out.println("Element lower than 20: " + treeSet.lower(20));

        // Using first() and last()
        System.out.println("First element: " + treeSet.first());
        System.out.println("Last element: " + treeSet.last());

        // Size of TreeSet
        System.out.println("Size of TreeSet: " + treeSet.size());

        // Clearing the TreeSet
        treeSet.clear();
        System.out.println("TreeSet after clearing: " + treeSet);
    }
}

Explanation:

  1. HashSet Operations:

    • Add elements (duplicates are ignored).

    • Check if an element exists (contains).

    • Remove elements.

    • Iterate over elements.

    • Clear all elements.

  2. TreeSet Operations:

    • Add elements (automatically sorted).

    • Use higher, lower, first, and last for advanced navigation.

    • Remove elements.

    • Iterate over elements.

C. Queue

A Queue is a collection used to store elements in a specific order, typically in a FIFO (First In, First Out) manner.

Implementations:

  • ArrayDeque: A resizable array implementation of the Deque interface (double-ended queue).

  • LinkedList: Can be used as a queue (implementing the Queue interface).

  • PriorityQueue: A queue that orders elements based on their priority.

When to Use:

  • ArrayDeque: When you need a fast FIFO queue with the ability to add and remove elements from both ends.

  • PriorityQueue: When you need to process elements based on their priority.

  • LinkedList: When you need to implement a queue that allows insertion/removal at both ends.

import java.util.ArrayDeque;

public class ArrayDequeExample {
    public static void main(String[] args) {
        ArrayDeque<Integer> arrayDeque = new ArrayDeque<>();

        // Adding elements
        arrayDeque.offer(10);
        arrayDeque.offer(20);
        arrayDeque.offer(30); // Adds elements to the end of the queue
        System.out.println("ArrayDeque after adding elements: " + arrayDeque);

        // Adding to the front
        arrayDeque.offerFirst(5);
        System.out.println("ArrayDeque after adding to the front: " + arrayDeque);

        // Adding to the end
        arrayDeque.offerLast(40);
        System.out.println("ArrayDeque after adding to the end: " + arrayDeque);

        // Accessing elements
        System.out.println("Peek first: " + arrayDeque.peekFirst()); // First element
        System.out.println("Peek last: " + arrayDeque.peekLast()); // Last element

        // Removing elements
        arrayDeque.poll(); // Removes the first element
        System.out.println("ArrayDeque after polling: " + arrayDeque);

        arrayDeque.pollLast(); // Removes the last element
        System.out.println("ArrayDeque after polling last: " + arrayDeque);

        // Iterating through the queue
        System.out.println("Iterating through ArrayDeque:");
        for (Integer number : arrayDeque) {
            System.out.println(number);
        }
    }
}
import java.util.PriorityQueue;

public class PriorityQueueExample {
    public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();

        // Adding elements
        priorityQueue.offer(30);
        priorityQueue.offer(20);
        priorityQueue.offer(50);
        priorityQueue.offer(10); // Adds elements based on priority
        System.out.println("PriorityQueue after adding elements: " + priorityQueue);

        // Accessing the highest priority element
        System.out.println("Peek (highest priority): " + priorityQueue.peek());

        // Removing elements
        System.out.println("Polling elements (in priority order):");
        while (!priorityQueue.isEmpty()) {
            System.out.println(priorityQueue.poll()); // Removes elements in priority order
        }

        // Checking if empty
        System.out.println("Is PriorityQueue empty? " + priorityQueue.isEmpty());
    }
}

Explaination:

  1. ArrayDeque:

    • Supports FIFO and LIFO (deque) operations.

    • Allows adding/removing elements from both ends using offerFirst, offerLast, pollFirst, and pollLast.

  2. PriorityQueue:

    • Orders elements based on natural ordering or a custom comparator.

    • Ideal for priority-based processing.

    • Does not support operations from both ends.

3. Map

A Map is a collection of key-value pairs where each key is unique. Maps are useful when you need to associate values with keys (e.g., storing a person's name with their age).

Common Implementations:

  • HashMap: A hash table-based implementation of the Map interface.

  • TreeMap: A Map implementation based on a red-black tree that stores keys in sorted order.

When to Use:

  • HashMap: When you need fast lookups, insertions, and deletions.

  • TreeMap: When you need the keys to be sorted.

import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        HashMap<String, Integer> hashMap = new HashMap<>();

        // Adding key-value pairs
        hashMap.put("Alice", 25);
        hashMap.put("Bob", 30);
        hashMap.put("Charlie", 35);
        System.out.println("HashMap after adding elements: " + hashMap);

        // Accessing a value by key
        System.out.println("Value for key 'Alice': " + hashMap.get("Alice"));

        // Checking if a key or value exists
        System.out.println("HashMap contains key 'Bob': " + hashMap.containsKey("Bob"));
        System.out.println("HashMap contains value 40: " + hashMap.containsValue(40));

        // Removing a key-value pair
        hashMap.remove("Charlie");
        System.out.println("HashMap after removing key 'Charlie': " + hashMap);

        // Iterating through the HashMap
        System.out.println("Iterating through HashMap:");
        for (String key : hashMap.keySet()) {
            System.out.println("Key: " + key + ", Value: " + hashMap.get(key));
        }

        // Size of HashMap
        System.out.println("Size of HashMap: " + hashMap.size());

        // Clearing the HashMap
        hashMap.clear();
        System.out.println("HashMap after clearing: " + hashMap);
    }
}
import java.util.TreeMap;

public class TreeMapExample {
    public static void main(String[] args) {
        TreeMap<String, Integer> treeMap = new TreeMap<>();

        // Adding key-value pairs
        treeMap.put("Charlie", 35);
        treeMap.put("Alice", 25);
        treeMap.put("Bob", 30);
        System.out.println("TreeMap after adding elements: " + treeMap); // Keys are sorted

        // Accessing a value by key
        System.out.println("Value for key 'Bob': " + treeMap.get("Bob"));

        // Checking if a key or value exists
        System.out.println("TreeMap contains key 'Alice': " + treeMap.containsKey("Alice"));
        System.out.println("TreeMap contains value 40: " + treeMap.containsValue(40));

        // Removing a key-value pair
        treeMap.remove("Alice");
        System.out.println("TreeMap after removing key 'Alice': " + treeMap);

        // Iterating through the TreeMap
        System.out.println("Iterating through TreeMap:");
        for (String key : treeMap.keySet()) {
            System.out.println("Key: " + key + ", Value: " + treeMap.get(key));
        }

        // Accessing first and last keys
        System.out.println("First key: " + treeMap.firstKey());
        System.out.println("Last key: " + treeMap.lastKey());

        // Size of TreeMap
        System.out.println("Size of TreeMap: " + treeMap.size());

        // Clearing the TreeMap
        treeMap.clear();
        System.out.println("TreeMap after clearing: " + treeMap);
    }
}

Explanation:

  1. HashMap:

    • Uses hashing for storing key-value pairs.

    • Keys are not ordered.

    • Ideal for fast lookups, insertions, and deletions.

    • Methods include put, get, remove, containsKey, containsValue, and keySet.

  2. TreeMap:

    • Stores keys in sorted (natural or custom) order.

    • Slower than HashMap for operations due to tree traversal.

    • Useful when you need keys to be sorted.

    • Methods include put, get, remove, firstKey, lastKey, and keySet.

4. Common Collection Algorithms

Java's Collections utility class provides several useful static methods for common collection operations, such as sorting, searching, and finding the max/min values.

Common Operations:

  • Sort: Sorts elements in ascending or custom order.

  • Max/Min: Finds the maximum or minimum element.

  • Reverse: Reverses the order of elements.

  • Frequency: Counts the occurrences of a specific element.

  • BinarySearch: Searches for an element in a sorted list.

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class CollectionsUtilityExample {
    public static void main(String[] args) {
        // Create a list of integers
        ArrayList<Integer> numbers = new ArrayList<>();
        Collections.addAll(numbers, 40, 10, 20, 30, 40);

        System.out.println("Original List: " + numbers);

        // **1. Sorting**
        // Sort in ascending order
        Collections.sort(numbers);
        System.out.println("Sorted in Ascending Order: " + numbers);

        // Sort in descending order
        Collections.sort(numbers, Comparator.reverseOrder());
        System.out.println("Sorted in Descending Order: " + numbers);

        // **2. Max and Min**
        int max = Collections.max(numbers);
        int min = Collections.min(numbers);
        System.out.println("Maximum Element: " + max);
        System.out.println("Minimum Element: " + min);

        // **3. Reversing**
        Collections.reverse(numbers);
        System.out.println("Reversed List: " + numbers);

        // **4. Frequency**
        int frequency = Collections.frequency(numbers, 40);
        System.out.println("Frequency of 40: " + frequency);

        // **5. Binary Search**
        // Binary search requires the list to be sorted in ascending order
        Collections.sort(numbers);
        System.out.println("List for Binary Search: " + numbers);

        int indexFound = Collections.binarySearch(numbers, 30);
        System.out.println("Index of 30: " + (indexFound >= 0 ? indexFound : "Not Found"));

        int indexNotFound = Collections.binarySearch(numbers, 25);
        System.out.println("Index of 25: " + (indexNotFound >= 0 ? indexNotFound : "Not Found"));
    }
}
//

// Original List: [40, 10, 20, 30, 40]
// Sorted in Ascending Order: [10, 20, 30, 40, 40]
// Sorted in Descending Order: [40, 40, 30, 20, 10]
// Maximum Element: 40
// Minimum Element: 10
// Reversed List: [10, 20, 30, 40, 40]
// Frequency of 40: 2
// List for Binary Search: [10, 20, 30, 40, 40]
// Index of 30: 2
// Index of 25: Not Found

Conclusion

The Java Collections Framework is a powerful tool that simplifies managing and organizing data in Java. With various options like lists, sets, and maps, it helps you choose the right structure for your needs while keeping your code efficient and clear. By understanding how collections work, you can write better, more organized programs. Take the time to explore and experiment with the framework—it's a skill that will make solving coding challenges much simpler and more enjoyable!

Hope this blog helps you!!

0
Subscribe to my newsletter

Read articles from Vaishnavi Dwivedi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vaishnavi Dwivedi
Vaishnavi Dwivedi