DEV Community

Cover image for Java Collections Framework - A Quick and In-Depth Look (Part - 1)
Aman Gupta
Aman Gupta

Posted on

Java Collections Framework - A Quick and In-Depth Look (Part - 1)

Introduction

Hey folks, the Collections Framework is a really important thing in Java. Think of it as a treasure chest where you can store and manage your data. It includes interfaces like Collection, Map, and Iterator. Inside the Collection interface, you'll find interfaces like List, Set, and Queue, which organize data based on different properties.

For example, if you want to store data together, you can use List. If you need unique values, go for Set. And if you want to prioritize elements, Queue is your choice.

After that, there are classes like ArrayList, Vector, LinkedList, PriorityQueue, HashSet, LinkedHashSet, TreeSet that implement these interfaces.

So, the Collections Framework is a powerful tool that helps you with data management. Learning it is crucial if you're into Java coding.

Why do we need to use Java Collections

Now, picture this: when you're coding, you often need to store data or perform operations on it, like searching, deleting, or modifying. That's where the Collections Framework comes into play. It saves you the hassle of creating your own data structures because all these things are already available.

Imagine you're preparing for an interview or participating in a coding contest. Now, let's say you have a problem where you need to shift the element of nth index of an array to its right for m times. Doing this work using a loop can make it complex, time-consuming, and performance-inefficient. So Java Collections not only saves you time, but the data structures and algorithms involved are highly optimized. This means they work really efficiently.

Collections Hierarchy

The Java Collections Framework features a structured hierarchy that facilitates a quick understanding of the relationships between classes and interfaces.
Collection Hirarchy

Collections Methods

The Java Collections Framework provides a wide range of methods to manipulate and work with collections of objects.

Here is some methods are listed below:

Method Return Type Parameter Description
add(E e) boolean E e Adds an element to the collection.
addAll(Collection<? extends E> c) boolean Collection<? extends E> c Adds all elements from another collection to the current collection.
remove(Object o) boolean Object o Removes the first occurrence of the specified element from the collection.
removeAll(Collection<?> c) boolean Collection<?> c Removes all elements in the current collection that are also in another collection.
size() int N/A Returns the number of elements in the collection.
iterator() Iterator N/A Returns an iterator to iterate through the elements.
isEmpty() boolean N/A Checks if the collection is empty.
contains(Object o) boolean Object o Checks if the collection contains a specified element.
containsAll(Collection<?> c) boolean Collection<?> c Checks if the collection contains all elements from another collection.
clear() void N/A Removes all elements from the collection.

Now we will see one by one every Interfaces.

Iterator Interface

An iterator in Java is an interface that provides a way to access elements of a collection (like lists, sets, or maps) one at a time, without exposing the underlying details of the collection's structure. It allows you to traverse through the elements in a collection sequentially in forward direction only.

It has some important methods:

  • hasNext(): Checks if there are more elements to iterate over and returns a boolean value.
  • next(): Returns the next element in the collection.
  • remove(): Removes the last element returned by next().

Here is a small program to demostrate use of Iterator




import java.util.ArrayList;
import java.util.Iterator;

public class IteratorExample {
    public static void main(String[] args) {
        // Create an ArrayList to store student names
        ArrayList<String> studentList = new ArrayList<>();

        // Add student names to the ArrayList
        studentList.add("Aman");
        studentList.add("Virat");
        studentList.add("Rohit");
        studentList.add("Jasprit");

        // Create an iterator to go through the student names
        Iterator<String> iterator = studentList.iterator();

        // Use a while loop to iterate through the ArrayList
        while (iterator.hasNext()) {
            // Get the next student name from the iterator
            String studentName = iterator.next();

            // Check if the student's name is "Jasprit"
            if (studentName.equals("Jasprit")) {
                // If it is "Jasprit," remove this student from the list
                iterator.remove();
                System.out.println("Removed student: " + studentName);
            } else {
                // Print the current student name to the console
                System.out.println("Student Name: " + studentName);
            }
        }

        // Print the updated list after removing "Jasprit"
        System.out.println("Updated Student List: " + studentList);
    }
}



Enter fullscreen mode Exit fullscreen mode

List Interface

The List interface represents an ordered collection of elements. It allows duplicate elements and provides methods to access, insert, update, and remove elements at specific positions within the list.The classes ArrayList, LinkedList, Vector, and Stack implements the List Interface.

Here is some common methods used in list interface :

Method Description
add(E element) Appends the specified element to the end.
add(int index, E element) Inserts the element at the specified position.
get(int index) Returns the element at the specified position.
set(int index, E element) Replaces the element at the specified position.
remove(int index) Removes the element at the specified position.
indexOf(Object o) Returns the index of the first occurrence.
lastIndexOf(Object o) Returns the index of the last occurrence.
subList(int from, int to) Returns a view of a portion of the list.

ArrayList

An ArrayList in Java is a dynamic and ordered collection that can grow or shrink in size as needed. It is used to store and manage a list of elements. ArrayList allows for easy insertion, retrieval, and removal of elements, making it a versatile data structure for working with collections of data in Java programs. It provides the benefits of an array with the added flexibility of automatic resizing, making it a popular choice for many programming tasks.

ArrayList

Think of an ArrayList in Java like having a list of items you need for a trip – you can add more stuff as you remember, remove things if you change your mind, and easily find an item by just knowing its place in the list. So, an ArrayList is like your trusty bag for storing and managing a bunch of items, and it's super handy when you're working with lots of data in your Java programs.

Here is a program to demonstrate ArrayList in Java:



import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ArrayListExample {
    public static void main(String[] args) {
        // Create an ArrayList of integers
        List<Integer> numbers = new ArrayList<>();

        // Add elements to the list
        numbers.add(10);
        numbers.add(20);
        numbers.add(30);

        // Access elements by index
        int secondNumber = numbers.get(1); // Accessing the second element (20)

        // Update an element
        numbers.set(0, 15); // Replacing the first element (10) with 15

        // Remove an element
        numbers.remove(2); // Removing the third element (30)

        // Check if an element is in the list
        boolean containsTwenty = numbers.contains(20); // false

        // Returns the size of the list
        int size = numbers.size(); // Size is now 2

        // Create an iterator
        Iterator<Integer> iterator = numbers.iterator();

        // Print the elements of the list 
        while (iterator.hasNext()) {
            int number = iterator.next();
            System.out.println(number);
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

LinkedList

A Linked List is a linear data structure in Java that consists of a sequence of elements, where each element points to the next one in the sequence. It's made up of nodes, where each node holds both data and a reference (or link) to the next node in the list. Unlike an array, a Linked List can efficiently insert or remove elements at any position in the list, making it a flexible choice for certain data manipulation tasks.Linked Lists are often used when dynamic data storage and efficient insertions and deletions are required in Java programs.

Linked List

Imagine a train with multiple compartments, each connected to the next through a small door. These compartments can represent a real-life example of a linked list. Each compartment (node) holds passengers (data) and has a door (pointer) that connects it to the next compartment. Passengers can easily enter or leave any compartment, and the train conductor can add or remove compartments when needed. This linked structure allows for efficient operations, such as adding a new compartment in the middle of the train or removing a specific compartment, making it a useful concept for tasks that involve dynamic data management, like a train ride with ever-changing passenger counts.

Here is a program to demonstrate LinkedList:




import java.util.Scanner;

// Define a Node class to represent elements in the linked list
class Node {
    int data;    // Data stored in the node
    Node next;   // Reference to the next node

    // Constructor to initialize a node with data
    Node(int data) {
        this.data = data;
        this.next = null; // Initially, the next node is set to null
    }
}

public class LinkedListExample {
    public static Node insert(Node head, int data) {
        // Function to insert a new node at the end of the linked list
        if (head == null) {
            // If the linked list is empty, create a new node and set it as the head
            return new Node(data);
        }
        Node current = head;
        while (current.next != null) {
            // Traverse the list to find the last node
            current = current.next;
        }
        // Create a new node with the given data and add it to the end of the list
        current.next = new Node(data);
        return head;
    }

    public static Node delete(Node head, int data) {
        // Function to delete a node with the specified data from the linked list
        if (head == null) {
            // If the linked list is empty, nothing to delete
            return null;
        }
        if (head.data == data) {
            // If the head node contains the data, update the head to the next node
            return head.next;
        }
        Node current = head;
        while (current.next != null && current.next.data != data) {
            // Traverse the list to find the node to be deleted
            current = current.next;
        }
        if (current.next != null) {
            // Update the next reference to skip the node to be deleted
            current.next = current.next.next;
        }
        return head;
    }

    public static void display(Node head) {
        // Function to display the elements of the linked list
        Node current = head;
        while (current != null) {
            // Traverse the list and print each element
            System.out.print(current.data + " -> ");
            current = current.next;
        }
        System.out.println("null");
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Node head = null; // Initialize the linked list as empty

        while (true) {
            // Display menu options
            System.out.println("Linked List Operations:");
            System.out.println("1. Insert");
            System.out.println("2. Delete");
            System.out.println("3. Display");
            System.out.println("4. Quit");
            System.out.print("Enter your choice: ");
            int choice = scanner.nextInt();

            switch (choice) {
                case 1:
                    // Insert operation
                    System.out.print("Enter element to insert: ");
                    int insertData = scanner.nextInt();
                    head = insert(head, insertData);
                    break;
                case 2:
                    // Delete operation
                    System.out.print("Enter element to delete: ");
                    int deleteData = scanner.nextInt();
                    head = delete(head, deleteData);
                    break;
                case 3:
                    // Display operation
                    display(head);
                    break;
                case 4:
                    // Exit the program
                    System.out.println("Exiting program.");
                    scanner.close();
                    return;
                default:
                    // Invalid choice
                    System.out.println("Invalid choice. Try again.");
            }
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Vector

A Vector in Java is a dynamic, resizable array-like data structure.It's similar to an ArrayList but is synchronized, which means it is thread-safe. Vectors can grow or shrink in size as needed, making them suitable for situations where the size of the collection may change dynamically. They are primarily used when you need a collection that can be accessed by multiple threads without the risk of data corruption.

Vector

Think of a Vector like a line of people waiting for their turn to do something. People can join the line at the end, and others can leave from the front. The Vector keeps everyone in an orderly queue and makes sure there are no clashes if multiple people try to join or leave at the same time. It's like a well-organized waiting line, ensuring fairness and order.

Here is a program to demonstrate Vector:




import java.util.Vector;

public class VectorExample {
    public static void main(String[] args) {
        // Create a Vector to store integers
        Vector<Integer> numbers = new Vector<>();

        // Add elements to the Vector
        numbers.add(10);
        numbers.add(20);
        numbers.add(30);

        // Access elements by index
        int secondNumber = numbers.get(1); // Accessing the second element (20)

        // Update an element
        numbers.set(0, 15); // Replacing the first element (10) with 15

        // Remove an element
        numbers.remove(2); // Removing the third element (30)

        // Check if an element is in the Vector
        boolean containsTwenty = numbers.contains(20); // true

        // Print the size of the Vector
        int size = numbers.size(); // Size is now 2

        // Print the elements of the Vector
        for (int number : numbers) {
            System.out.println(number);
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Stack

A stack in Java is a linear data structure that follows the Last-In-First-Out (LIFO) principle. It is the subclass of Vector and operates like a collection of items where you can only add or remove elements from the top, similar to a stack of plates. Elements are added to the top of the stack and removed from the top as well. This means that the last element added to the stack is the first one to be removed. Stacks are often used for tasks that require keeping track of the order of elements, such as managing function calls in recursion, undo functionality in applications, or browser history navigation.

Stack

Think of a stack like a stack of trays in a cafeteria. You add trays to the top, and when someone wants a tray, they take the one from the top. It's a "Last-In-First-Out" (LIFO) system. In programming, it's similar; you add and remove items from the top of the stack, often used for tracking function calls, undo operations, or backtracking in algorithms, ensuring the last item added is the first to be processed or removed.

Here's a simple Java program that demonstrates the basic operations of a stack (push, pop, and peek):



import java.util.Stack;

public class StackExample {
    public static void main(String[] args) {
        Stack<String> stack = new Stack<>();

        // Push elements onto the stack
        stack.push("Apple");
        stack.push("Banana");
        stack.push("Cherry");

        // Print the stack
        System.out.println("Stack: " + stack);

        // Pop an element from the stack
        String poppedElement = stack.pop();
        System.out.println("Popped Element: " + poppedElement);
        System.out.println("Updated Stack: " + stack);

        // Peek at the top element without removing it
        String topElement = stack.peek();
        System.out.println("Top Element: " + topElement);
        System.out.println("Stack after Peek: " + stack);
    }
}


Enter fullscreen mode Exit fullscreen mode

Now we will look into Queue Interface.

Queue Interface

The Queue interface in Java represents a collection that holds elements in a linear order and allows adding elements at one end (enqueue) and removing elements from the other end (dequeue). It follows the "first-in-first-out" (FIFO) principle, similar to a real-world queue. Queues are often used for tasks like managing tasks in a printer queue or processing tasks in a concurrent system.

Queue Interface

PriorityQueue Class

The PriorityQueue class in Java is an implementation of the Queue interface that provides a priority-based ordering of elements. Elements in a PriorityQueue are ordered based on their natural order or according to a specified comparator. The element with the highest priority (according to the defined order) is always at the front and will be the first to be removed when dequeued. This makes PriorityQueue suitable for tasks like task scheduling, job processing, or managing elements with priorities.

PriorityQueue

Here's a brief example of using a PriorityQueue with integers:



import java.util.PriorityQueue;

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

        // Enqueue elements with different priorities
        priorityQueue.add(20);
        priorityQueue.add(10);
        priorityQueue.add(30);

        // Print the priority queue
        System.out.println("Priority Queue: " + priorityQueue);

        // Dequeue elements (highest priority first)
        int dequeuedElement = priorityQueue.poll();
        System.out.println("Dequeued Element: " + dequeuedElement);
        System.out.println("Updated Priority Queue: " + priorityQueue);
    }
}


Enter fullscreen mode Exit fullscreen mode

In this example, we create a PriorityQueue of integers, enqueue elements with varying priorities, and then dequeue elements. The PriorityQueue automatically orders elements in ascending order by default (lowest value first).

Deque Interface

The Deque (pronounced as "deck") interface in Java stands for "Double Ended Queue." It is a linear collection that supports adding and removing elements from both ends, i.e., both the front and the rear. A Deque interface extends the Queue interface, adding methods for operations at both ends of the queue, making it versatile for various data manipulation scenarios.

You can use a Deque in situations where you need to efficiently insert or remove elements from the front and rear of a collection, such as implementing stacks, queues, or double-ended queues. Java provides several implementations of the Deque interface, including ArrayDeque and LinkedList.

Deque

ArrayDeque

An ArrayDeque in Java is a double-ended queue (Deque) implemented as a resizable array. It allows you to add and remove elements from both ends efficiently, making it a versatile choice for various data structure operations. Unlike a traditional array, an ArrayDeque can dynamically resize itself to accommodate elements as they are added. This data structure is often used in scenarios where you need a queue or stack-like behavior, and it provides constant time (O(1)) performance for basic operations like adding and removing elements from both ends.

Here's a brief example of using a ArrayDeque:



import java.util.ArrayDeque;
import java.util.Deque;

public class DoubleEndedQueueExample {
    public static void main(String[] args) {
        Deque<String> printerQueue = new ArrayDeque<>();

        // Add print jobs to the rear of the queue
        printerQueue.offer("Document1.pdf"); // Rear
        printerQueue.offer("Document2.pdf");
        printerQueue.offer("Document3.pdf");

        // Print jobs are processed from the front of the queue
        String currentJob = printerQueue.poll(); // Front
        System.out.println("Printing: " + currentJob);

        // Add an urgent print job to the front of the queue
        printerQueue.offerFirst("UrgentDocument.pdf"); // Front

        // Print remaining jobs
        while (!printerQueue.isEmpty()) {
            currentJob = printerQueue.poll();
            System.out.println("Printing: " + currentJob);
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

In this example, we use an ArrayDeque as a Deque to manage a double-ended queue for print jobs. We add jobs to the rear using offer() and to the front using offerFirst(). Print jobs are processed from the front using poll(). This demonstrates the versatility of the Deque interface for handling data from both ends.

In this blog, we've explored the foundational elements of the Java Collections Framework, focusing on the List and Queue interfaces. We've seen how this powerful framework simplifies data management by providing a wide range of ready-made data structures and algorithms. The List interface allows us to work with ordered collections, while the Queue interface is ideal for managing items in a First-In-First-Out (FIFO) manner.

Stay tuned for our next blog, where we'll dive deeper into other aspects of the Java Collections Framework, expanding your knowledge and empowering you to tackle a wider array of data management challenges.

Do let me know if you have any queries in comments or you can DM me on Instagram.

Also you can see my blog on Building a Role-Based Access Control System with JWT in Spring Boot here.

Thanks for reading.
Happy Coding 😇

Top comments (1)

Collapse
 
aminmansuri profile image
hidden_dude

It's important to note that Vector and Stack as well as Hashtable are legacy classes that use synchronization. While modern Java may remove the unneeded synchronization code when not needed, if you share the object with several threads it will cause overhead. And if you try to use it in a multithreaded environment it will bring all your 32 threads to a halt on your super server.

That is why it's better to avoid those classes and go with the more modern alternatives:

Vector -> ArrayList
Hashtable -> Map
Stack -> ArrayDeque (or LinkedDeque if appropriate) or ???

It's unfortunate that they used the word "Vector" in the initial versions of Java, since these days using Vectors as in numpy would be a great addition to the Java language. But a List is not a Vector, to be a vector you need to support vector operations like scaling, dot product, etc..