I'm always excited to take on new projects and collaborate with innovative minds.

Email

contact@niteshsynergy.com

Website

https://www.niteshsynergy.com/

Collection Concepts

Java Collections Framework is a unified architecture for managing groups of objects. It simplifies programming by providing data structures and algorithms for collections, making them easier to use and manipulate.

Collection Concepts
image-155.png

 

1. Collection Hierarchy

Java Collections Framework provides a set of interfaces and classes for working with collections.

Core Interfaces

  1. Collection (root interface)
    • Common methods: add(), remove(), size(), iterator(), etc.
  2. List (ordered collection, allows duplicates)
    • Implementations: ArrayList, LinkedList, Vector
  3. Set (unordered, no duplicates)
    • Implementations: HashSet, LinkedHashSet, TreeSet
  4. Queue (FIFO or LIFO order)
    • Implementations: PriorityQueue, ArrayDeque
  5. Map (key-value pairs, no duplicate keys)
    • Implementations: HashMap, LinkedHashMap, TreeMap, Hashtable

2. Classes Overview

Collection TypeCharacteristicsCommon Classes
ListOrdered, duplicates allowedArrayList, LinkedList, Vector
SetNo duplicates, unorderedHashSet, LinkedHashSet, TreeSet
QueueFIFO or priority orderPriorityQueue, ArrayDeque
MapKey-value pairsHashMap, TreeMap, LinkedHashMap

3. Key Methods in Collections

  • Adding Elements: add(element), addAll(collection)
  • Removing Elements: remove(element), clear()
  • Accessing Elements:
    • For Lists: get(index)
    • For Maps: get(key)
  • Iterating:
    • Using Iterator
    • Using forEach loop
    • Using Streams (stream().forEach())

4. Important Classes

ArrayList

  • Dynamic array.
  • Faster access (get() method).
  • Slower at add()/remove() operations in the middle.

LinkedList

  • Doubly linked list.
  • Efficient insertions/removals but slower random access.

HashSet

  • Stores unique elements.
  • Backed by a HashMap.

TreeSet

  • Elements are sorted.
  • Slower than HashSet due to sorting.

HashMap

  • Key-value pairs.
  • Fast lookup (constant time on average).

TreeMap

  • Keys are sorted.
  • Slower than HashMap.

5. Key Features

  1. Generics
    • Collections can be type-safe.
    • Example: List<String> list = new ArrayList<>();
  2. Thread-Safety
    • Not all collections are thread-safe.
    • Use Collections.synchronizedList() or ConcurrentHashMap.
  3. Streams and Lambda
    • Functional programming support via Streams.
    • Example: list.stream().filter(e -> e.startsWith("A")).collect(Collectors.toList());

6. Differences Between Key Classes

FeatureArrayListLinkedListHashSetTreeSet
OrderingYesYesNoYes (sorted)
DuplicatesYesYesNoNo
Access SpeedFast (index)Slow (sequential)Fast (hash)Slow (tree-based)

7. Advanced Concepts

  1. Fail-Fast Iterators
    • Iterators of most collections throw ConcurrentModificationException if the collection is modified while iterating.
  2. Fail-Safe Iterators
    • Concurrent collections like CopyOnWriteArrayList allow modification during iteration.

8. Real-Time Use Cases

  • List: Maintaining ordered lists like students, tasks.
  • Set: Storing unique IDs, tags.
  • Queue: Implementing task scheduling.
  • Map: Managing key-value pairs like dictionaries or caching.

Code Examples

image-158.png
image-159.png

Tips for Using Collections

  1. Choose ArrayList when frequent random access is needed.
  2. Use HashSet for unique, unordered collections.
  3. For thread-safe operations, prefer ConcurrentHashMap over Hashtable.
  4. Use TreeMap or TreeSet for sorted data.

 

 

                                                              Lets start each one

 

The Collection Interface

The Collection interface in Java is part of the Java Collections Framework and represents a group of objects known as elements. It is the root interface of the framework, and all the collection types (e.g., List, Set, Queue) extend this interface, allowing them to share common methods and behaviors.

 

Basic Properties of Collection Interface:

  • Generics: The Collection interface uses generics to define the type of elements it can store, e.g., Collection<T>, where T is the type of the elements.
  • Not a complete collection: It is not a concrete class but an interface, so you cannot instantiate it directly. You use its implementations such as ArrayList, HashSet, etc.
  • Super interface: It is a super interface for all collections like Set, List, Queue, etc.

Methods of the Collection Interface

Here are the core methods provided by the Collection interface, along with their use cases:

  1. boolean add(E e)
    • Use Case: Adds a single element to the collection.
    • Why: Allows you to insert an element into the collection.
    • Example: List<String> names = new ArrayList<>(); names.add("John");
  2. boolean remove(Object o)
    • Use Case: Removes a single occurrence of the specified element from the collection, if it is present.
    • Why: To delete an element from the collection.
    • Example: names.remove("John");
  3. boolean contains(Object o)
    • Use Case: Returns true if the collection contains the specified element.
    • Why: To check whether a specific element is present in the collection.
    • Example: boolean exists = names.contains("John");
  4. boolean isEmpty()
    • Use Case: Returns true if the collection is empty (contains no elements).
    • Why: To check if a collection is empty before performing operations like iteration or removal.
    • Example: boolean empty = names.isEmpty();
  5. int size()
    • Use Case: Returns the number of elements in the collection.
    • Why: To know how many elements are currently in the collection.
    • Example: int count = names.size();
  6. void clear()
    • Use Case: Removes all elements from the collection.
    • Why: To clear the collection of all elements.
    • Example: names.clear();
  7. Object[] toArray()
    • Use Case: Returns an array containing all elements in the collection.
    • Why: To convert the collection to an array for further processing.
    • Example: Object[] arr = names.toArray();
  8. boolean containsAll(Collection<?> c)
    • Use Case: Returns true if the collection contains all elements of the specified collection.
    • Why: To check if one collection contains all the elements of another collection.
    • Example: boolean hasAll = names.containsAll(otherList);
  9. boolean addAll(Collection<? extends E> c)
    • Use Case: Adds all elements from the specified collection to the current collection.
    • Why: To add multiple elements at once to a collection.
    • Example: names.addAll(otherList);
  10. boolean removeAll(Collection<?> c)
    • Use Case: Removes all elements from the collection that are contained in the specified collection.
    • Why: To remove a set of elements from the collection.
    • Example: names.removeAll(otherList);
  11. boolean retainAll(Collection<?> c)
    • Use Case: Retains only the elements in the collection that are also contained in the specified collection.
    • Why: To keep only those elements in the collection that are also present in another collection.
    • Example: names.retainAll(otherList);
  12. Iterator<E> iterator()
    • Use Case: Returns an iterator over the elements in the collection.
    • Why: To iterate over the elements of the collection.
    • Example:

 

Iterator<String> it = names.iterator();
while (it.hasNext()) {
   System.out.println(it.next());
}
 

Why Collection is Given as an Interface:

  • Polymorphism and Flexibility: By defining Collection as an interface, Java allows different types of collections (like List, Set, and Queue) to implement the same set of methods. This makes it possible to write generic code that works with any collection type.
  • Decoupling: Using an interface allows code to be written without depending on specific implementation classes (like ArrayList or HashSet). You can change the underlying implementation without changing the code that uses the collection, making it more maintainable and flexible.
  • Abstracting Common Operations: Collections share common operations (like adding, removing, or checking for existence of elements), and using an interface allows these operations to be abstracted, so developers can work with different collection types in a uniform way.

Example Use Cases:

  • Adding elements:
  • Collection<String> names = new ArrayList<>();
    names.add("Alice");
    names.add("Bob");
    names.add("Charlie");
     

Checking size and emptiness:

System.out.println("Size: " + names.size());
System.out.println("Is empty: " + names.isEmpty());
 

Removing elements:

names.remove("Bob");
 

Clearing all elements:

names.clear();
 

Iterating over the collection:

for (String name : names) {
   System.out.println(name);
}

What is output of this program ?

screenshot-2024-11-19-231804.png


 See & tell me why this CE error. As we return type of toArray() method is Object[] right. 

 

Next → 

ArrayList in Java (Short Definition)

ArrayList is a part of the Java Collections Framework and implements the List interface. It is a resizable array implementation, meaning the size of the array can grow as needed to accommodate new elements. It allows duplicate elements and maintains the insertion order.

Use Case of ArrayList

ArrayList is best suited when you need:

  • Random Access: It provides fast random access (constant time) to elements, as it is backed by an array.
  • Dynamic Resizing: You need a collection that automatically resizes as elements are added.
  • Maintaining Order: The insertion order of elements is maintained.
  • Frequent Reads: If you perform a lot of read operations (retrievals), ArrayList is efficient.

 

Sample Code for ArrayList (Without Generics & With Generics)

import java.util.ArrayList;

public class ArrayListExample {
   public static void main(String[] args) {
       // Creating ArrayList without generics
       ArrayList list = new ArrayList();
       list.add("Apple");
       list.add(100);
       list.add(5.5);
       
       // Retrieving elements
       for (Object element : list) {
           System.out.println(element);
       }
   }
}
 

With Generics:

import java.util.ArrayList;

public class ArrayListExample {
   public static void main(String[] args) {
       // Creating ArrayList with generics
       ArrayList<String> list = new ArrayList<>();
       list.add("Apple");
       list.add("Banana");
       list.add("Cherry");
       
       // Retrieving elements
       for (String element : list) {
           System.out.println(element);
       }
   }
}
 

Time Complexity of ArrayList Operations

  • Add Operation:
    • Amortized O(1): Adding an element to the end of the list is generally a constant-time operation. However, if the underlying array needs to be resized, the operation becomes O(n), where n is the number of elements in the list.
  • Get Operation (Accessing elements):
    • O(1): ArrayList provides constant-time access to elements by index.
  • Remove Operation:
    • O(n): Removing an element from the middle or end requires shifting the remaining elements, which takes linear time.
  • Insert Operation (Insert an element at a specific index):
    • O(n): Inserting an element in the middle requires shifting the subsequent elements.
  • Contains Operation:
    • O(n): Checking if an element exists in the list requires scanning through the list, which takes linear time.
  • Size Operation:
    • O(1): The size is maintained by the ArrayList internally, so it takes constant time to retrieve.
  • Clear Operation:
    • O(n): Removing all elements in the list takes linear time as it has to dereference all elements.
  • Resize Operation:
    • O(n): When the internal array is full and a new element is added, the array needs to be resized, which involves copying the old elements to the new array.

8 Ways to Retrieve Elements from ArrayList

package com.niteshsynergy.collection;

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

public class Demo02AL {
   public static void main(String[] args) {
       // Using generics to avoid raw type warnings
       ArrayList<String> list = new ArrayList<>();
       
       // Adding elements to the ArrayList
       list.add("A");
       list.add("B");
       list.add("C");
       list.add("D");
       list.add("E");
       list.add("F");
       list.add("G");
       list.add("H");
       list.add("I");

       // Printing the ArrayList
       System.out.println(list);

       // 8 ways to retrieve elements from an ArrayList:

       // 1. Using the get() method:
       System.out.println("Using get method...");
       System.out.println(list.get(1));  // Access element at index 1

       // 2. Using an iterator:
       System.out.println("Using Iterator...");
       Iterator<String> iterator = list.iterator(); // Using generics
       while (iterator.hasNext()) {
           System.out.println(iterator.next());
       }
       // Time Complexity: O(n) - iterating through all elements

       // 3. Using enhanced for loop (foreach):
       System.out.println("Using enhanced for loop (foreach)...");
       for (String element : list) { // No need to cast as it's a generic list
           System.out.println(element);
       }
       // Time Complexity: O(n)

       // 4. Using a ListIterator (for bi-directional iteration):
       System.out.println("Using ListIterator...");
       ListIterator<String> listIterator = list.listIterator(); // Using generics
       while (listIterator.hasNext()) {
           System.out.println(listIterator.next());
       }
       // Time Complexity: O(n)

       // 5. Using the stream() method:
       System.out.println("Using stream() method...");
       list.stream().forEach(System.out::println); 
       // Time Complexity: O(n)

       // 6. Using the forEach() method (Java 8+):
       System.out.println("Using forEach() method...");
       list.forEach(System.out::println);
       // Time Complexity: O(n)

       // 7. Using the toArray() method (convert to array):
       System.out.println("Using toArray() method...");
       Object[] array = list.toArray();
       for (Object element : array) {
           System.out.println(element);
       }
       // Time Complexity: O(n) for conversion and iteration

       // 8. Using the for loop with index:
       System.out.println("Using for loop with index...");
       for (int i = 0; i < list.size(); i++) { // Corrected index range
           System.out.println(list.get(i));
       }
       // Time Complexity: O(n) for iteration
   }
}
 

The various ways to access elements from an ArrayList in Java offer different trade-offs in terms of flexibility, performance, and convenience. Below, I’ll explain why each of these methods exists and when they might be preferable:

1. Using the get() method

  • Why: The get() method provides direct access to an element by its index. It’s efficient when you need to retrieve an element at a specific position quickly.
  • When to Use: When you know the index of the element you want to access, and you need fast random access.
  • Performance: O(1) because it directly accesses the element in the underlying array.

2. Using an iterator (Iterator interface)

  • Why: An Iterator provides a way to iterate over the ArrayList without directly using indices. It allows you to traverse the collection in a more abstract way, particularly useful when you don't need to know the index of the current element.
  • When to Use: When you need to iterate through the entire list and possibly modify the collection during iteration (with Iterator.remove()).
  • Performance: O(n) for iteration; however, since it abstracts away the indices, it's more flexible when the underlying data structure changes or if you don't need to track positions explicitly.

3. Using enhanced for loop (foreach)

  • Why: The enhanced for loop provides a simple and readable way to iterate through all elements in the ArrayList. It’s cleaner than using an explicit iterator in simple use cases.
  • When to Use: When you want to iterate over all elements and you don’t need access to the index.
  • Performance: O(n) because it internally uses an iterator under the hood but is simpler to write.

4. Using a ListIterator

  • Why: A ListIterator extends Iterator and allows traversal of the list in both forward and backward directions. It also allows modification of the list during iteration, making it more flexible than a standard iterator.
  • When to Use: When you need bi-directional iteration (can move both forwards and backwards) or if you need to modify the list while iterating.
  • Performance: O(n) for iteration, but ListIterator provides extra functionality compared to a basic iterator.

5. Using stream()

  • Why: The stream() method in Java 8+ provides a functional programming approach to handle elements in a collection. It allows you to chain operations like filtering, mapping, or collecting elements in a fluent style.
  • When to Use: When you need to process elements in a functional style, for example, applying filters or transformations in a concise way.
  • Performance: O(n), but can be slower if not used efficiently or if transformations are complex. Streams offer better readability and flexibility but may incur some overhead compared to a traditional for loop.

6. Using forEach() method (Java 8+)

  • Why: forEach() is a default method in Iterable that simplifies iteration. It allows you to pass a lambda expression or method reference to perform an action on each element of the ArrayList.
  • When to Use: When you want to apply an action to each element in a more declarative way, especially when using lambda expressions.
  • Performance: O(n). Similar to stream(), it's a modern approach to iteration but involves lambda expressions, which can have a slight performance overhead compared to using for loops or Iterator.

7. Using toArray() method

  • Why: toArray() converts the ArrayList to a plain Java array, which can be useful if you need to work with a standard array or perform operations that are easier with arrays (like accessing elements via indices in a non-List context).
  • When to Use: When you need to work with the list as an array for further processing or when passing the collection to code that requires an array.
  • Performance: O(n) for conversion, because the entire list must be copied into an array. After conversion, access time is O(1), but the initial conversion is costly.

8. Using for loop with index

  • Why: The for loop with an index is the traditional way of iterating over a collection by directly accessing elements via their index. It's very familiar and useful when you need to perform specific operations based on the index, such as comparing elements at different positions.
  • When to Use: When you need the index of the current element or want to manipulate elements at specific positions.
  • Performance: O(n) for iteration, but can be efficient if you know the exact positions you're interested in.

Why Multiple Ways Exist:

  • Flexibility: Different access methods provide different levels of flexibility. For example, the ListIterator allows backward iteration, while toArray() allows you to convert to an array for external processing.
  • Readability: Methods like forEach() and stream() provide a functional, declarative approach that may be easier to read, especially in complex transformations.
  • Specific Use Cases: Some methods are more suited to specific needs, such as accessing elements by index (get(), for loop) or processing elements functionally (stream(), forEach()).
  • Performance Considerations: Depending on the size of the collection and the type of operation, some methods may perform better. For example, direct index access (get()) is faster than iterating through the collection when you know the position of the element.
  • Code Modularity: Different methods allow code to be written in a more modular or functional style (like with stream()), which can make maintenance easier in complex applications.

 

                                                     Fail-Safe vs. Fail-Fast Iterators  

Fail-Fast:

  • Definition: A fail-fast iterator immediately throws a ConcurrentModificationException if the collection is modified while it is being iterated over (except through the iterator itself).
  • Why It Occurs: This occurs to prevent inconsistent behavior when a collection is modified during iteration. ArrayList uses fail-fast iterators.
    • Example of Fail-Fast:

       

    • ArrayList<String> list = new ArrayList<>();
      list.add("Apple");
      Iterator<String> it = list.iterator();
      list.add("Banana");
      it.next();  // Throws ConcurrentModificationException

       

      Fail-Safe:

    • Definition: A fail-safe iterator does not throw an exception if the collection is modified during iteration. Instead, it provides a snapshot of the collection at the time the iterator was created.
    • Why It Occurs: This occurs in collections like CopyOnWriteArrayList, which create a copy of the collection for iteration, ensuring that the original collection remains unchanged during iteration.
      • Example of Fail-Safe

         

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("Apple");
Iterator<String> it = list.iterator();
list.add("Banana");  // No ConcurrentModificationException
it.next();
 

  •  

Solution to Fail-Fast Behavior

If you want to avoid ConcurrentModificationException, you have two options:

Use an explicit synchronization mechanism like synchronized blocks to ensure no modification happens while iterating.

Use a CopyOnWriteArrayList if you expect modifications while iterating, as it supports fail-safe iterators.

Example of Synchronization:

synchronized(list) {
   Iterator<String> it = list.iterator();
   while (it.hasNext()) {
       System.out.println(it.next());
   }
}
 

Here’s a concise, tabular format explaining the insertion order, duplicates, and time complexities for ArrayList operations in Java:

PropertyDescriptionTime Complexity
Insertion OrderArrayList maintains the order of insertion. The elements are retrieved in the same sequence as they were added.O(1) for adding an element at the end of the list (amortized).
DuplicatesArrayList allows duplicate elements. You can add the same element multiple times.O(1) for adding a duplicate at the end of the list.
Accessing by IndexYou can access any element by its index. This operation is fast because it is backed by an array.O(1) (constant time).
Adding an ElementAdds an element at the end of the list. If capacity is exceeded, a new array is created.O(1) amortized for most cases, but O(n) if resizing occurs (rare).
Inserting at IndexInserts an element at a specific index. All subsequent elements are shifted to the right.O(n) in the worst case (when inserting at the beginning).
Removing by IndexRemoves an element at a specific index, and shifts subsequent elements to the left.O(n) in the worst case (when removing from the beginning).
Removing by ObjectRemoves the first occurrence of the specified element, shifting subsequent elements.O(n) because it needs to search for the element.
Iterating (Looping)Iterating through the list with an iterator or enhanced for loop preserves the insertion order.O(n) for iterating over all elements.

Key Observations:

  • Insertion Order: The insertion order is preserved. The sequence of elements as they are added to the ArrayList is maintained when accessed or iterated over.
  • Duplicates: ArrayList allows duplicates. The same element can appear multiple times in the list.
  • Time Complexity for add(): Most of the time, adding an element to the end of the list is O(1). However, if the underlying array is full and needs resizing, it takes O(n) because a new array is created and all elements are copied over.
  • Time Complexity for get(): Accessing an element by index is a constant-time operation, i.e., O(1).
     




 For a real-time project, using an ArrayList is common in scenarios that require maintaining insertion order and where you can have duplicates. A complex example could be a task management system that handles the following:

Scenario: Task Management System

Imagine you're building a task management system for a project. In this system:

  • Tasks are assigned to team members.
  • Tasks have deadlines and priorities.
  • Each task can be added and removed dynamically.
  • The task list should preserve the order of task creation (insertion order).
  • A team member can have multiple tasks, so duplicates are allowed.
  • You want to allow searching by task ID and priority.

Use Case:

  1. Task List: You maintain a list of tasks using ArrayList to store tasks.
  2. Task Operations: You allow adding tasks, updating deadlines, changing priorities, and removing tasks.
  3. Task Lookup: You need to search tasks by ID or priority quickly.

Task Class (Model for Tasks):

import java.util.Date;

public class Task {
   private int taskId;
   private String taskName;
   private Date deadline;
   private int priority;  // 1 = High, 2 = Medium, 3 = Low
   private String assignee;

   public Task(int taskId, String taskName, Date deadline, int priority, String assignee) {
       this.taskId = taskId;
       this.taskName = taskName;
       this.deadline = deadline;
       this.priority = priority;
       this.assignee = assignee;
   }

   // Getters and Setters
   public int getTaskId() { return taskId; }
   public String getTaskName() { return taskName; }
   public Date getDeadline() { return deadline; }
   public int getPriority() { return priority; }
   public String getAssignee() { return assignee; }

   @Override
   public String toString() {
       return "Task{" +
              "taskId=" + taskId +
              ", taskName='" + taskName + '\'' +
              ", deadline=" + deadline +
              ", priority=" + priority +
              ", assignee='" + assignee + '\'' +
              '}';
   }
}
 

Task Manager Class (Using ArrayList):

import java.util.ArrayList;
import java.util.List;
import java.util.Date;

public class TaskManager {
   private List<Task> taskList;

   public TaskManager() {
       taskList = new ArrayList<>();
   }

   // Add a task
   public void addTask(Task task) {
       taskList.add(task);
   }

   // Remove a task by taskId
   public boolean removeTaskById(int taskId) {
       for (Task task : taskList) {
           if (task.getTaskId() == taskId) {
               taskList.remove(task);
               return true;
           }
       }
       return false;
   }

   // Search task by priority
   public List<Task> getTasksByPriority(int priority) {
       List<Task> result = new ArrayList<>();
       for (Task task : taskList) {
           if (task.getPriority() == priority) {
               result.add(task);
           }
       }
       return result;
   }

   // Update task deadline
   public void updateTaskDeadline(int taskId, Date newDeadline) {
       for (Task task : taskList) {
           if (task.getTaskId() == taskId) {
               task.setDeadline(newDeadline);
               break;
           }
       }
   }

   // Get all tasks assigned to a specific assignee
   public List<Task> getTasksForAssignee(String assignee) {
       List<Task> result = new ArrayList<>();
       for (Task task : taskList) {
           if (task.getAssignee().equals(assignee)) {
               result.add(task);
           }
       }
       return result;
   }

   // Display all tasks
   public void displayTasks() {
       for (Task task : taskList) {
           System.out.println(task);
       }
   }
}
 

Main Class (Running the Task Manager):

import java.util.Date;

public class TaskManagerApp {
   public static void main(String[] args) {
       // Creating TaskManager instance
       TaskManager taskManager = new TaskManager();
       
       // Adding tasks to the manager
       taskManager.addTask(new Task(1, "Design Database", new Date(), 1, "John"));
       taskManager.addTask(new Task(2, "Develop API", new Date(), 2, "Sarah"));
       taskManager.addTask(new Task(3, "Create UI", new Date(), 3, "Michael"));
       taskManager.addTask(new Task(4, "Test Application", new Date(), 1, "John"));
       
       // Display all tasks
       System.out.println("All Tasks:");
       taskManager.displayTasks();
       
       // Remove a task
       taskManager.removeTaskById(2);
       
       // Display tasks after removal
       System.out.println("\nTasks After Removal:");
       taskManager.displayTasks();
       
       // Get tasks with high priority
       System.out.println("\nHigh Priority Tasks:");
       taskManager.getTasksByPriority(1).forEach(System.out::println);
       
       // Update task deadline
       taskManager.updateTaskDeadline(3, new Date(System.currentTimeMillis() + 86400000));  // 1 day later
       
       // Display updated task
       System.out.println("\nUpdated Task 3:");
       taskManager.displayTasks();
       
       // Get tasks for a specific assignee
       System.out.println("\nTasks assigned to John:");
       taskManager.getTasksForAssignee("John").forEach(System.out::println);
   }
}

 

The ArrayList<E> class in Java is an implementation of the List<E> interface, and it extends AbstractList<E>, implementing several other interfaces like RandomAccess, Cloneable, and Serializable. Let’s break down the purpose of each of these interfaces and how they benefit the ArrayList class.

List<E> Interface

  • The ArrayList implements the List<E> interface, which defines methods to manage a collection of elements in a specific order.
  • This includes basic operations like adding, removing, and accessing elements by index, ensuring that ArrayList follows the List contract.

2. RandomAccess Interface

  • Purpose: The RandomAccess interface is a marker interface that indicates that the list supports fast, constant-time access to elements by index.
  • Benefits to ArrayList:
    • ArrayList provides fast O(1) time complexity for element access via the get(int index) and set(int index, E element) methods. This is because it is backed by a dynamic array.
    • When a collection supports RandomAccess, algorithms or utilities can take advantage of this feature to optimize performance. For example, methods like Collections.sort() can optimize their sorting algorithms for RandomAccess collections.
    • If ArrayList didn’t implement RandomAccess, performance might degrade when iterating over large lists or when trying to access elements directly by index, compared to an implementation like LinkedList, which doesn't offer fast random access.

3. Cloneable Interface

  • Purpose: The Cloneable interface allows for creating a copy of the ArrayList using the clone() method.
  • Benefits to ArrayList:
    • By implementing Cloneable, the ArrayList can provide a shallow copy of the list (i.e., copying the array but not the individual objects themselves).
    • This can be useful when creating a new list with the same elements without modifying the original list.
    • Example: ArrayList<E> clone = (ArrayList<E>) originalList.clone();
  • Why it helps ArrayList: It makes it easier to duplicate the list if needed for parallel processing, backup purposes, or when working with independent collections that share the same elements initially.

4. Serializable Interface

  • Purpose: The Serializable interface marks the ArrayList as serializable, meaning instances of ArrayList can be converted into a byte stream, and this byte stream can be stored (e.g., in a file) or transmitted over a network.
  • Benefits to ArrayList:
    • Allows you to serialize an ArrayList and save its state to a file or send it over a network. When deserialized, the list can be restored to its original form.
    • It also helps in distributed applications, where collections need to be transferred across different Java Virtual Machines (JVMs).
    • Example:

FileOutputStream fos = new FileOutputStream("list.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(myArrayList);  // Serializing
 

Why it helps ArrayList: Serialization is key in systems where objects need to be saved or transmitted. By implementing Serializable, ArrayList can be part of these systems.

 

Why Did ArrayList Implement These Interfaces?

  • Efficiency and Versatility:

    • RandomAccess ensures that ArrayList can handle indexing efficiently, which is a key operation in many algorithms.
    • Cloneable makes it easier to duplicate the list when needed.
    • Serializable enables storing and transferring the list's state.

    These features are meant to make ArrayList a general-purpose, efficient, and flexible data structure, capable of performing a wide variety of tasks while being easy to use in complex systems where performance, duplication, or communication across JVMs is necessary.

  • By implementing these interfaces, ArrayList not only provides the core functionality expected from a list (such as element management) but also improves its utility in real-world applications, offering performance optimizations (via RandomAccess), making it easy to duplicate (via Cloneable), and ensuring compatibility with systems that need to store or transmit objects (via Serializable). These features align with common use cases like object persistence, network communication, and performance-critical applications.

Here’s a real-time code example demonstrating how Cloneable helps ArrayList by allowing you to create a shallow copy of an ArrayList. This can be useful when you want to create a new list that has the same elements as the original but without modifying the original list.

Real-Time Scenario:

Imagine you're building a shopping cart system for an e-commerce website. You want to clone the items in a cart before applying a discount or modifying some of the items, so you don't affect the original cart.

 

import java.util.ArrayList;

class Product {
   String name;
   double price;

   // Constructor
   public Product(String name, double price) {
       this.name = name;
       this.price = price;
   }

   // Method to display product info
   public void display() {
       System.out.println("Product: " + name + ", Price: " + price);
   }
}

public class ShoppingCart {

   public static void main(String[] args) {
       // Create an ArrayList to hold products in a shopping cart
       ArrayList<Product> cart = new ArrayList<>();
       cart.add(new Product("Laptop", 1000.00));
       cart.add(new Product("Phone", 500.00));
       cart.add(new Product("Headphones", 150.00));

       // Display the original cart
       System.out.println("Original Cart:");
       for (Product product : cart) {
           product.display();
       }

       // Clone the cart using the clone() method
       ArrayList<Product> clonedCart = (ArrayList<Product>) cart.clone();

       // Modify the cloned cart (apply a discount on the cloned cart)
       clonedCart.get(0).price = 900.00;  // Discount applied on the Laptop

       // Display the modified cloned cart
       System.out.println("\nCloned Cart (after applying discount):");
       for (Product product : clonedCart) {
           product.display();
       }

       // Display the original cart again to show that it was not affected
       System.out.println("\nOriginal Cart after modification:");
       for (Product product : cart) {
           product.display();
       }
   }
}
 

  • We have a Product class representing an item in the cart with a name and price.
  • In the ShoppingCart class, we create an ArrayList<Product> to represent the cart.
  • The cart.clone() method is used to clone the original shopping cart. This creates a new list with the same elements.
  • We apply a discount to the first product in the cloned cart (clonedCart.get(0).price = 900.00), but since it's a shallow copy, the original cart remains unaffected.

Key Points:

  1. Shallow Copy: clone() creates a shallow copy, meaning the Product objects themselves are not duplicated; the references to the same Product objects are copied. Therefore, changes to the properties of the objects inside the clonedCart will not affect the original cart object.
  2. Benefit: This method allows you to work with a modified version of the list (like applying discounts, temporary changes) without affecting the original list. In real-world applications like shopping carts, this is important for maintaining the integrity of the original data while applying changes temporarily.

When is this Useful in Real Time?

  • Undo/Redo Operations: You can clone a list before making a change, so if the user wants to undo, you can restore the original state from the clone.
  • Preview and Apply: You can clone an object (like a cart or form) and apply changes in the clone, allowing users to preview changes without affecting the original data.

 

1. Vector Overview

  • Definition: Vector is a legacy class in Java that implements the List interface, just like ArrayList. It is part of the java.util package and was introduced before ArrayList.
  • Key Difference: While ArrayList is not synchronized by default, Vector is synchronized, making it thread-safe (but at the cost of performance).
  • Resizing Behavior: Vector grows dynamically as needed. By default, it increases its size by doubling the capacity when it runs out of space.

2. Key Properties of Vector

  • Insertion Order: Maintains the order of elements as they were inserted, just like ArrayList.
  • Duplicates: Allows duplicate elements.
  • Thread-Safety: Operations on Vector are synchronized by default, which ensures thread safety but can cause performance overhead in single-threaded scenarios.

3. Time Complexities for Vector Operations

OperationTime Complexity
Add an element (at the end)O(1) (amortized), but O(n) when resizing occurs.
Add an element (at any index)O(n) (due to shifting elements)
Get an element by indexO(1)
Remove an element by indexO(n) (due to shifting elements)
Remove an element by objectO(n) (due to searching)
IteratingO(n)


 

9 ways to retrieve elements from a Vector:

package com.niteshsynergy.collection;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Vector;
import java.util.Enumeration;

public class Demo02Vector {
   public static void main(String[] args) {
       // Using generics for type safety
       Vector<String> vector = new Vector<>();

       // Adding elements to the Vector
       vector.add("A");
       vector.add("B");
       vector.add("C");
       vector.add("D");
       vector.add("E");
       vector.add("F");
       vector.add("G");
       vector.add("H");
       vector.add("I");

       // Printing the Vector
       System.out.println("Vector elements: " + vector);

       // 9 ways to retrieve elements from a Vector:

       // 1. Using the get() method:
       System.out.println("Using get method...");
       System.out.println(vector.get(1));  // Access element at index 1

       // 2. Using an iterator:
       System.out.println("Using Iterator...");
       Iterator<String> iterator = vector.iterator(); // Using generics
       while (iterator.hasNext()) {
           System.out.println(iterator.next());
       }
       // Time Complexity: O(n)

       // 3. Using enhanced for loop (foreach):
       System.out.println("Using enhanced for loop (foreach)...");
       for (String element : vector) { // No need to cast as it's a generic list
           System.out.println(element);
       }
       // Time Complexity: O(n)

       // 4. Using a ListIterator (for bi-directional iteration):
       System.out.println("Using ListIterator...");
       ListIterator<String> listIterator = vector.listIterator(); // Using generics
       while (listIterator.hasNext()) {
           System.out.println(listIterator.next());
       }
       // Time Complexity: O(n)

       // 5. Using the stream() method:
       System.out.println("Using stream() method...");
       vector.stream().forEach(System.out::println); 
       // Time Complexity: O(n)

       // 6. Using the forEach() method (Java 8+):
       System.out.println("Using forEach() method...");
       vector.forEach(System.out::println);
       // Time Complexity: O(n)

       // 7. Using the toArray() method (convert to array):
       System.out.println("Using toArray() method...");
       Object[] array = vector.toArray();
       for (Object element : array) {
           System.out.println(element);
       }
       // Time Complexity: O(n) for conversion and iteration

       // 8. Using the for loop with index:
       System.out.println("Using for loop with index...");
       for (int i = 0; i < vector.size(); i++) {
           System.out.println(vector.get(i));
       }
       // Time Complexity: O(n)

       // 9. Using Enumeration (specific to Vector):
       System.out.println("Using Enumeration...");
       Enumeration<String> enumeration = vector.elements(); // Vector's specific method to get Enumeration
       while (enumeration.hasMoreElements()) {
           System.out.println(enumeration.nextElement());
       }
       // Time Complexity: O(n)
   }
}
 

4. Vector vs. ArrayList

  • Synchronization: The most notable difference between ArrayList and Vector is that Vector is synchronized by default, which makes it thread-safe but slower than ArrayList for single-threaded applications.
  • Growth Behavior: Vector doubles its size when it reaches capacity, while ArrayList increases its size by 50% by default.

5. Example of Using Vector

Let’s create a real-world example similar to the task manager we did for ArrayList, but this time using Vector. Since Vector is thread-safe, it might be useful in scenarios where multiple threads need to interact with the same list of tasks concurrently.

Task Manager with Vector (Thread-Safe)

 

import java.util.*;

class Task {
   private int taskId;
   private String taskName;
   private String assignee;

   public Task(int taskId, String taskName, String assignee) {
       this.taskId = taskId;
       this.taskName = taskName;
       this.assignee = assignee;
   }

   public int getTaskId() { return taskId; }
   public String getTaskName() { return taskName; }
   public String getAssignee() { return assignee; }

   @Override
   public String toString() {
       return "Task{" + "taskId=" + taskId + ", taskName='" + taskName + '\'' + ", assignee='" + assignee + '\'' + '}';
   }
}

public class TaskManagerWithVector {
   private Vector<Task> taskList;

   public TaskManagerWithVector() {
       taskList = new Vector<>();
   }

   // Add a task
   public void addTask(Task task) {
       taskList.add(task);
   }

   // Remove a task by taskId
   public boolean removeTaskById(int taskId) {
       for (Task task : taskList) {
           if (task.getTaskId() == taskId) {
               taskList.remove(task);
               return true;
           }
       }
       return false;
   }

   // Search task by assignee
   public List<Task> getTasksForAssignee(String assignee) {
       List<Task> result = new ArrayList<>();
       for (Task task : taskList) {
           if (task.getAssignee().equals(assignee)) {
               result.add(task);
           }
       }
       return result;
   }

   // Display all tasks
   public void displayTasks() {
       for (Task task : taskList) {
           System.out.println(task);
       }
   }
}

class Main {
   public static void main(String[] args) {
       TaskManagerWithVector taskManager = new TaskManagerWithVector();

       // Adding tasks to the manager
       taskManager.addTask(new Task(1, "Design Database", "John"));
       taskManager.addTask(new Task(2, "Develop API", "Sarah"));
       taskManager.addTask(new Task(3, "Create UI", "Michael"));
       taskManager.addTask(new Task(4, "Test Application", "John"));

       // Display all tasks
       System.out.println("All Tasks:");
       taskManager.displayTasks();

       // Remove a task by ID
       taskManager.removeTaskById(2);
       System.out.println("\nAfter Removing Task with ID 2:");
       taskManager.displayTasks();

       // Get tasks for assignee "John"
       System.out.println("\nTasks assigned to John:");
       taskManager.getTasksForAssignee("John").forEach(System.out::println);
   }
}
 

  • Task Class: Represents a task with properties like taskId, taskName, and assignee.
  • TaskManagerWithVector Class:
    • Uses Vector<Task> to store tasks.
    • Operations like adding, removing tasks, and searching tasks are implemented.
  • Main Class: Demonstrates adding tasks, removing tasks, and retrieving tasks assigned to a specific person.

Real-World Application:

  • Multi-threaded Scenarios: This example is useful when multiple threads may be accessing and modifying the task list at the same time (e.g., multiple threads adding or removing tasks in a concurrent environment).
  • Thread-Safety: Since Vector is synchronized, it ensures that tasks are added and removed safely when accessed by different threads, but it may incur performance overhead in a single-threaded application due to synchronization.

6. When to Use Vector in Real-Time Projects:

  • When thread-safety is needed: If your application has multiple threads accessing or modifying the list at the same time and you don't want to manage synchronization yourself, Vector provides a quick solution.
  • Legacy Systems: Since Vector is a legacy class, it might be used in legacy systems where refactoring to newer collections like CopyOnWriteArrayList is impractical.

 

  • Vector is thread-safe and preserves the insertion order, making it a good choice in multi-threaded applications where synchronization is required.
  • For single-threaded applications, ArrayList might be a better choice because it offers better performance.
  • Like ArrayList, Vector allows duplicates and can dynamically resize, but at the cost of performance due to synchronization.

If you are building a high-performance application where thread-safety is not a concern, it’s often better to use ArrayList instead of Vector. If you need thread-safety and don’t want to manually manage synchronization, Vector can be a good choice, but modern alternatives like CopyOnWriteArrayList or Collections.synchronizedList() are often preferred.

 

 

 

1. Stack Overview

  • Definition: Stack is a legacy class in Java that represents a last-in-first-out (LIFO) stack of objects. It extends Vector and implements the List interface, so it has similar properties to Vector, but with additional stack-specific methods like push, pop, peek, etc.
  • Thread Safety: Like Vector, Stack is synchronized by default, making it thread-safe, but also potentially slower in single-threaded contexts due to the overhead of synchronization.
  • Use Case: A Stack is often used when you need to keep track of method calls, undo operations, or parsing expressions (like in calculators or compilers).

2. Key Properties of Stack

  • Insertion Order: Maintains the order of elements in a LIFO (Last-In-First-Out) manner.
  • Duplicates: Allows duplicate elements.
  • Thread-Safety: Synchronized operations, so thread-safe but with performance trade-offs.

3. Time Complexities for Stack Operations

OperationTime Complexity
Push (Add an element)O(1)
Pop (Remove an element)O(1)
Peek (View top element)O(1)
SearchO(n) (linear search for element)

 

9 ways to retrieve elements from a Stack:

package com.niteshsynergy.collection;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.Stack;
import java.util.Enumeration;

public class Demo02Stack {
   public static void main(String[] args) {
       // Using generics for type safety
       Stack<String> stack = new Stack<>();

       // Adding elements to the Stack
       stack.push("A");
       stack.push("B");
       stack.push("C");
       stack.push("D");
       stack.push("E");
       stack.push("F");
       stack.push("G");
       stack.push("H");
       stack.push("I");

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

       // 9 ways to retrieve elements from a Stack:

       // 1. Using the peek() method:
       System.out.println("Using peek method...");
       System.out.println(stack.peek());  // Access top element without removing it

       // 2. Using pop() method:
       System.out.println("Using pop method...");
       System.out.println(stack.pop());  // Access and remove top element

       // 3. Using an iterator:
       System.out.println("Using Iterator...");
       Iterator<String> iterator = stack.iterator(); // Using generics
       while (iterator.hasNext()) {
           System.out.println(iterator.next());
       }
       // Time Complexity: O(n)

       // 4. Using enhanced for loop (foreach):
       System.out.println("Using enhanced for loop (foreach)...");
       for (String element : stack) { // No need to cast as it's a generic list
           System.out.println(element);
       }
       // Time Complexity: O(n)

       // 5. Using a ListIterator (for bi-directional iteration):
       System.out.println("Using ListIterator...");
       ListIterator<String> listIterator = stack.listIterator(); // Using generics
       while (listIterator.hasNext()) {
           System.out.println(listIterator.next());
       }
       // Time Complexity: O(n)

       // 6. Using the stream() method:
       System.out.println("Using stream() method...");
       stack.stream().forEach(System.out::println); 
       // Time Complexity: O(n)

       // 7. Using the forEach() method (Java 8+):
       System.out.println("Using forEach() method...");
       stack.forEach(System.out::println);
       // Time Complexity: O(n)

       // 8. Using the toArray() method (convert to array):
       System.out.println("Using toArray() method...");
       Object[] array = stack.toArray();
       for (Object element : array) {
           System.out.println(element);
       }
       // Time Complexity: O(n) for conversion and iteration

       // 9. Using Enumeration (specific to Stack):
       System.out.println("Using Enumeration...");
       Enumeration<String> enumeration = stack.elements(); // Stack's specific method to get Enumeration
       while (enumeration.hasMoreElements()) {
           System.out.println(enumeration.nextElement());
       }
       // Time Complexity: O(n)
   }
}
 

4. Example of Using Stack

We can implement a real-world example of a Undo Mechanism using a Stack. This is a typical use case for a Stack, where each action performed (e.g., editing text) is pushed onto the stack, and you can pop the last action to undo it.

Undo/Redo Mechanism using Stack

 

import java.util.*;

class Action {
   private String actionName;
   private String data;

   public Action(String actionName, String data) {
       this.actionName = actionName;
       this.data = data;
   }

   @Override
   public String toString() {
       return "Action{" + "actionName='" + actionName + '\'' + ", data='" + data + '\'' + '}';
   }
}

public class UndoManager {
   private Stack<Action> undoStack;
   private Stack<Action> redoStack;

   public UndoManager() {
       undoStack = new Stack<>();
       redoStack = new Stack<>();
   }

   // Perform an action and push it onto the undo stack
   public void performAction(String actionName, String data) {
       Action action = new Action(actionName, data);
       undoStack.push(action);
       System.out.println("Performed: " + action);
       // Clear redo stack after a new action
       redoStack.clear();
   }

   // Undo the last action
   public void undo() {
       if (!undoStack.isEmpty()) {
           Action lastAction = undoStack.pop();
           redoStack.push(lastAction);
           System.out.println("Undone: " + lastAction);
       } else {
           System.out.println("No actions to undo.");
       }
   }

   // Redo the last undone action
   public void redo() {
       if (!redoStack.isEmpty()) {
           Action lastUndoneAction = redoStack.pop();
           undoStack.push(lastUndoneAction);
           System.out.println("Redone: " + lastUndoneAction);
       } else {
           System.out.println("No actions to redo.");
       }
   }

   // Display the current actions in the undo stack
   public void displayActions() {
       System.out.println("\nCurrent Actions (Undo Stack):");
       for (Action action : undoStack) {
           System.out.println(action);
       }
   }
}

class Main {
   public static void main(String[] args) {
       UndoManager undoManager = new UndoManager();

       // Perform some actions
       undoManager.performAction("Edit Text", "Added a paragraph.");
       undoManager.performAction("Delete Text", "Deleted a paragraph.");
       undoManager.performAction("Format Text", "Bolded a paragraph.");

       // Display actions in the undo stack
       undoManager.displayActions();

       // Undo an action
       undoManager.undo();

       // Display actions after undo
       undoManager.displayActions();

       // Redo an action
       undoManager.redo();

       // Display actions after redo
       undoManager.displayActions();
   }
}
 

  1. Action Class: Represents an action with properties like actionName and data to describe what was done (e.g., editing text).
  2. UndoManager Class:
    • Uses two stacks: undoStack for actions that can be undone and redoStack for actions that have been undone but can be redone.
    • The performAction method adds an action to the undoStack.
    • The undo method pops the most recent action from the undoStack and pushes it onto the redoStack.
    • The redo method pops the most recent undone action from the redoStack and pushes it back onto the undoStack.
  3. Main Class: Demonstrates performing actions, undoing and redoing actions, and displaying the actions in the stack.

5. When to Use Stack in Real-Time Projects

  • Undo/Redo Functionality: As shown in the above example, stacks are ideal for undo/redo functionality. Each action performed is pushed onto the stack, and the last action can be undone (popped) when necessary.
  • Expression Evaluation and Parsing: Stacks are commonly used in algorithms for evaluating expressions (such as infix, postfix expressions) and for parsing syntax trees.
  • Method Call Tracking: Stacks are used to track function calls in a recursion (or in a call stack in memory).
  • Backtracking Algorithms: Stacks are often used in backtracking algorithms, such as solving mazes or puzzles like Sudoku or N-Queens, where you need to explore possible solutions step by step and backtrack when necessary.

6. When to Avoid Stack

  • Single-Threaded Contexts: If thread-safety is not required, using a Stack (due to its synchronization) may introduce unnecessary performance overhead. For single-threaded environments, a simple ArrayList or LinkedList may be more efficient.
  • In modern applications: If you don’t need the thread-safety of Stack, and you are building a modern application, Deque (Double-ended Queue) or ArrayDeque is often recommended for stack-like operations because they provide better performance.

 

  • Stack is useful in scenarios where LIFO (Last In First Out) behavior is needed, such as undo/redo functionality, parsing, and recursive method calls.
  • However, due to its synchronization overhead, it might be less efficient than alternatives like ArrayDeque or LinkedList in non-concurrent contexts.
  • The undo/redo mechanism example demonstrates a real-world application where Stack can efficiently handle operations that need to be undone or redone, maintaining a history of actions and providing simple management of them.

 

 

1. LinkedList Overview

  • Definition: A LinkedList in Java is part of the java.util package and implements the List interface. Unlike an ArrayList, it stores elements in nodes where each node points to the next node, creating a linked structure. It supports operations such as insertion and deletion at both ends of the list.
  • Thread Safety: LinkedList is not synchronized, meaning it is not thread-safe by default. You would need to use external synchronization if multiple threads are involved.
  • Use Case: A LinkedList is useful when you need to frequently add or remove elements, particularly when these operations are expected to happen at the beginning or end of the list, as it performs better than ArrayList in these cases.

2. Key Properties of LinkedList

  • Insertion Order: Maintains the order of elements as they are inserted.
  • Duplicates: Allows duplicate elements.
  • Null Elements: Allows null elements in the list.

 

3. Time Complexities for LinkedList Operations

OperationTime Complexity
Add First (Insert at beginning)O(1)
Add Last (Insert at end)O(1)
Remove FirstO(1)
Remove LastO(1)
Get (Access element)O(n)
Remove (Access and remove an element)O(n)

 

8 ways to retrieve elements from a LinkedList:

package com.niteshsynergy.collection;

import java.util.LinkedList;
import java.util.Iterator;

public class Demo03LinkedList {
   public static void main(String[] args) {
       // Create a LinkedList of Strings
       LinkedList<String> list = new LinkedList<>();

       // Add elements to the LinkedList
       list.add("A");
       list.add("B");
       list.add("C");
       list.add("D");
       list.add("E");

       // Printing the LinkedList
       System.out.println("LinkedList elements: " + list);

       // 8 ways to retrieve elements from a LinkedList:

       // 1. Using the get() method:
       System.out.println("Using get() method...");
       System.out.println(list.get(2));  // Access the element at index 2

       // 2. Using iterator():
       System.out.println("Using Iterator...");
       Iterator<String> iterator = list.iterator();
       while (iterator.hasNext()) {
           System.out.println(iterator.next());
       }

       // 3. Using for-each loop:
       System.out.println("Using enhanced for loop...");
       for (String element : list) {
           System.out.println(element);
       }

       // 4. Using listIterator():
       System.out.println("Using ListIterator...");
       var listIterator = list.listIterator();
       while (listIterator.hasNext()) {
           System.out.println(listIterator.next());
       }

       // 5. Using stream() method:
       System.out.println("Using stream() method...");
       list.stream().forEach(System.out::println);

       // 6. Using forEach() method:
       System.out.println("Using forEach() method...");
       list.forEach(System.out::println);

       // 7. Using toArray():
       System.out.println("Using toArray() method...");
       Object[] array = list.toArray();
       for (Object element : array) {
           System.out.println(element);
       }

       // 8. Using descendingIterator() for reverse order:
       System.out.println("Using descendingIterator() for reverse order...");
       var descendingIterator = list.descendingIterator();
       while (descendingIterator.hasNext()) {
           System.out.println(descendingIterator.next());
       }
   }
}
 

5. Example Use Case: Implementing Undo Mechanism using LinkedList

In this case, a LinkedList can be used to track changes in an editor, where each change is added to the list, and actions can be undone by removing elements from the list.

import java.util.*;

class Action {
   private String actionName;
   private String data;

   public Action(String actionName, String data) {
       this.actionName = actionName;
       this.data = data;
   }

   @Override
   public String toString() {
       return "Action{" + "actionName='" + actionName + '\'' + ", data='" + data + '\'' + '}';
   }
}

public class UndoManager {
   private LinkedList<Action> actionHistory;

   public UndoManager() {
       actionHistory = new LinkedList<>();
   }

   // Perform an action
   public void performAction(String actionName, String data) {
       Action action = new Action(actionName, data);
       actionHistory.add(action);
       System.out.println("Performed: " + action);
   }

   // Undo the last action
   public void undo() {
       if (!actionHistory.isEmpty()) {
           Action lastAction = actionHistory.removeLast();
           System.out.println("Undone: " + lastAction);
       } else {
           System.out.println("No actions to undo.");
       }
   }

   // Display actions
   public void displayActions() {
       System.out.println("\nCurrent Actions:");
       for (Action action : actionHistory) {
           System.out.println(action);
       }
   }
}

class Main {
   public static void main(String[] args) {
       UndoManager undoManager = new UndoManager();

       // Perform some actions
       undoManager.performAction("Edit Text", "Added a paragraph.");
       undoManager.performAction("Delete Text", "Deleted a paragraph.");
       undoManager.performAction("Format Text", "Bolded a paragraph.");

       // Display actions
       undoManager.displayActions();

       // Undo an action
       undoManager.undo();

       // Display actions after undo
       undoManager.displayActions();
   }
}
 

In this example, the LinkedList allows us to perform actions (like editing text) and keep a history. The removeLast() method efficiently removes the most recent action for the undo functionality.

 

6. When to Use LinkedList in Real-Time Projects

  • Frequent Insertions and Deletions: LinkedLists are ideal when your application requires frequent insertions and deletions, especially at the beginning or end of the list.
  • Undo/Redo Functionality: Just like Stack, LinkedLists can be used for maintaining action histories in undo/redo functionality.
  • Queue-like Behavior: If you need queue-like behavior, LinkedLists support efficient operations on both ends, making them a good choice for scenarios requiring deque operations.

7. When to Avoid LinkedList

  • Random Access: If you need frequent access to elements at specific indices (e.g., list.get(index)), an ArrayList is often more efficient due to its random access capability (O(1) time complexity).
  • Overhead in Small Collections: For small collections with fewer insertions and deletions, the overhead of managing the linked structure might not provide significant advantages over ArrayList.

8. Key Takeaways

  • LinkedList is particularly useful when frequent insertions and deletions are required, especially at the beginning or end of the list.
  • Time Complexity for LinkedList operations such as adding/removing from the ends is O(1), but accessing elements by index takes O(n), which makes it less efficient for random access.
  • Use Case: It's often used in implementing undo/redo mechanisms and managing sequences of actions.

 

Java Program: Simulating Tab and History Navigation

import java.util.*;

public class BrowserNavigation {
   // Stack to store the history of opened tabs for back navigation
   private Stack<String> historyStack;
   
   // List to represent tabs open in the browser
   private List<String> tabs;
   
   // Current tab index
   private int currentTabIndex;
   
   public BrowserNavigation() {
       historyStack = new Stack<>();
       tabs = new ArrayList<>();
       currentTabIndex = -1;
   }
   
   // Add a new tab to the browser
   public void openTab(String tabName) {
       tabs.add(tabName);
       currentTabIndex = tabs.size() - 1; // Set current tab to the last one
       System.out.println("Opened tab: " + tabName);
   }

   // Switch to the previous tab
   public void previousTab() {
       if (currentTabIndex > 0) {
           currentTabIndex--;
           System.out.println("Switched to previous tab: " + tabs.get(currentTabIndex));
       } else {
           System.out.println("No previous tab available.");
       }
   }
   
   // Switch to the next tab
   public void nextTab() {
       if (currentTabIndex < tabs.size() - 1) {
           currentTabIndex++;
           System.out.println("Switched to next tab: " + tabs.get(currentTabIndex));
       } else {
           System.out.println("No next tab available.");
       }
   }

   // Navigate back to the last visited tab in history
   public void backHistory() {
       if (!historyStack.isEmpty()) {
           String lastVisitedTab = historyStack.pop();
           currentTabIndex = tabs.indexOf(lastVisitedTab);
           System.out.println("Navigated back to: " + tabs.get(currentTabIndex));
       } else {
           System.out.println("No history to go back to.");
       }
   }
   
   // Simulate closing the current tab
   public void closeTab() {
       if (currentTabIndex != -1) {
           String closedTab = tabs.remove(currentTabIndex);
           System.out.println("Closed tab: " + closedTab);
           // Adjust the current tab index
           if (currentTabIndex >= tabs.size()) {
               currentTabIndex = tabs.size() - 1;
           }
       } else {
           System.out.println("No tab to close.");
       }
   }
   
   // Show the current tab's name
   public void showCurrentTab() {
       if (currentTabIndex != -1) {
           System.out.println("Current tab: " + tabs.get(currentTabIndex));
       } else {
           System.out.println("No tabs opened.");
       }
   }
   
   // Add the current tab to history when switching tabs
   public void switchTab(int index) {
       if (index >= 0 && index < tabs.size()) {
           // Save the current tab to history stack before switching
           if (currentTabIndex != -1) {
               historyStack.push(tabs.get(currentTabIndex));
           }
           currentTabIndex = index;
           System.out.println("Switched to tab: " + tabs.get(currentTabIndex));
       } else {
           System.out.println("Invalid tab index.");
       }
   }

   public static void main(String[] args) {
       BrowserNavigation browser = new BrowserNavigation();
       
       // Simulate opening tabs
       browser.openTab("Home Page");
       browser.openTab("News");
       browser.openTab("Shopping Cart");
       browser.openTab("Settings");

       // Show current tab
       browser.showCurrentTab();
       
       // Simulate switching tabs
       browser.previousTab();
       browser.nextTab();

       // Add some history by switching tabs and navigating back
       browser.switchTab(2);
       browser.switchTab(0);
       browser.backHistory();
       
       // Close a tab
       browser.closeTab();
       
       // Show the final current tab
       browser.showCurrentTab();
   }
}
 

  • BrowserNavigation Class:
    • Tabs are stored in an ArrayList<String>, which allows dynamic tab creation and easy access.
    • History is managed using a Stack<String>, allowing for navigation of previously visited tabs (back functionality).
    • Current Tab Index is tracked by currentTabIndex, representing the active tab.
  • Methods:
    • openTab: Adds a new tab and sets it as the current tab.
    • previousTab: Navigates to the tab before the current one if available.
    • nextTab: Navigates to the next tab if available.
    • backHistory: Moves back to the last visited tab from the history stack.
    • closeTab: Removes the current tab and adjusts the current tab index accordingly.
    • showCurrentTab: Displays the current active tab.
    • switchTab: Switches to a tab by index and saves the previous tab to the history stack for back navigation.

 

HashSet Overview

  1. Definition:
    • A HashSet is part of the Java Collections Framework and implements the Set interface. It stores elements in a hash table, which means that it uses hashing to store elements.
    • It does not maintain the order of elements, so the elements are not stored in the order they were added.
    • HashSet ensures that no duplicate elements are stored. If you try to add a duplicate element, it won’t be added.
  2. Thread Safety:
    • HashSet is not synchronized, meaning it is not thread-safe by default. If multiple threads access the set concurrently, external synchronization is needed to ensure thread safety.
  3. Use Case:
    • HashSet is ideal when you need a collection of unique elements and do not need ordering. It is used for operations like checking membership, removing duplicates, or performing set operations (union, intersection, difference).

Key Properties of HashSet

  1. Insertion Order:
    • Does not maintain the order of elements. The order in which elements are iterated is unpredictable.
  2. Duplicates:
    • Does not allow duplicates. If you try to add a duplicate element, it will not be added.
  3. Null Elements:
    • Allows one null element. However, if more than one null is added, only the first one is retained.

Time Complexities for HashSet Operations

OperationTime Complexity
add(E e)O(1)
remove(Object o)O(1)
contains(Object o)O(1)
size()O(1)
clear()O(n)
iterator()O(n)

 

9 ways to retrieve elements from a HashSet:

package com.niteshsynergy.collection;

import java.util.HashSet;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.ArrayList;

public class Demo05HashSet {
   public static void main(String[] args) {
       // Creating a HashSet and adding elements
       HashSet<String> set = new HashSet<>();
       set.add("A");
       set.add("B");
       set.add("C");
       set.add("D");
       set.add("E");
       set.add("F");
       set.add("G");
       set.add("H");
       set.add("I");

       System.out.println("HashSet: " + set);

       // 1. Using the for-each loop (enhanced for loop):
       System.out.println("\nUsing for-each loop:");
       for (String element : set) {
           System.out.println(element);
       }
       // Time Complexity: O(n)

       // 2. Using an iterator:
       System.out.println("\nUsing Iterator:");
       Iterator<String> iterator = set.iterator();
       while (iterator.hasNext()) {
           System.out.println(iterator.next());
       }
       // Time Complexity: O(n)

       // 3. Using ListIterator (only possible with List, not directly with HashSet):
       // HashSet does not support ListIterator directly, but we can convert to a List first
       System.out.println("\nUsing ListIterator (converted to List):");
       ListIterator<String> listIterator = new ArrayList<>(set).listIterator();
       while (listIterator.hasNext()) {
           System.out.println(listIterator.next());
       }
       // Time Complexity: O(n)

       // 4. Using stream() method (Java 8+):
       System.out.println("\nUsing stream() method:");
       set.stream().forEach(System.out::println);
       // Time Complexity: O(n)

       // 5. Using forEach() method (Java 8+):
       System.out.println("\nUsing forEach() method:");
       set.forEach(System.out::println);
       // Time Complexity: O(n)

       // 6. Using toArray() method:
       System.out.println("\nUsing toArray() method:");
       Object[] array = set.toArray();
       for (Object element : array) {
           System.out.println(element);
       }
       // Time Complexity: O(n)

       // 7. Using a for loop with an index (after converting to ArrayList):
       System.out.println("\nUsing for loop with index:");
       ArrayList<String> list = new ArrayList<>(set);
       for (int i = 0; i < list.size(); i++) {
           System.out.println(list.get(i));
       }
       // Time Complexity: O(n)

       // 8. Using forEachRemaining() method of Iterator:
       System.out.println("\nUsing forEachRemaining() of Iterator:");
       iterator = set.iterator();  // Re-initialize iterator as it was already used
       iterator.forEachRemaining(System.out::println);
       // Time Complexity: O(n)

       // 9. Using Lambda expression with forEach() (Java 8+):
       System.out.println("\nUsing Lambda expression with forEach():");
       set.forEach(element -> System.out.println(element));
       // Time Complexity: O(n)
   }
}
 

  • add(E e): Adds elements to the set. Duplicates are ignored.
  • remove(Object o): Removes the specified element from the set.
  • contains(Object o): Checks if the set contains a specific element.
  • size(): Returns the number of elements in the set.
  • clear(): Removes all elements from the set.
  • iterator(): Allows iteration over the elements in the set.

 

Use Case Example: Removing Duplicates Using HashSet

In this example, a HashSet is used to remove duplicate elements from a collection.

 

import java.util.HashSet;
import java.util.ArrayList;

public class RemoveDuplicates {
   public static void main(String[] args) {
       // Create an ArrayList with duplicate elements
       ArrayList<String> list = new ArrayList<>();
       list.add("Java");
       list.add("Python");
       list.add("C++");
       list.add("Java"); // Duplicate

       // Remove duplicates by converting the list to a HashSet
       HashSet<String> set = new HashSet<>(list);

       // Display unique elements
       System.out.println("Unique elements: " + set);
   }
}
 

In this case, the HashSet automatically removes the duplicate "Java" when converting from the ArrayList.

 

When to Use HashSet in Real-Time Projects

  1. Uniqueness:
    • HashSet is ideal when you need to store only unique elements and do not care about the order of elements (e.g., removing duplicates from a list, checking membership).
  2. Efficient Lookup:
    • HashSet provides constant-time average complexity (O(1)) for operations like contains(), add(), and remove(), making it a great choice for checking if an element exists or removing an element.
  3. Set Operations:
    • Use HashSet for performing set operations like union, intersection, or difference.

 

When to Avoid HashSet

  1. Ordering Elements:
    • If you need to maintain the order of elements (i.e., the order they were inserted), use LinkedHashSet instead. A HashSet does not guarantee any specific order for the elements.
  2. Thread Safety:
    • HashSet is not synchronized, so if you need thread safety, consider using Collections.synchronizedSet(new HashSet<>()) or explore ConcurrentHashMap or CopyOnWriteArraySet.
  3. Memory Overhead:
    • Since HashSet uses a hash table, it may consume more memory compared to other sets like TreeSet or LinkedHashSet depending on the number of elements and hash collisions.
  • HashSet is used to store unique elements and is ideal when you need fast access to elements with constant time complexity for insertion, deletion, and membership check.
  • The order of elements is not guaranteed in a HashSet.
  • HashSet is typically used for removing duplicates, set operations (union, intersection, difference), and membership checks.
  • It does not allow duplicate elements and supports at most one null element.

 

Real-Time Complex Example: Using HashSet for Unique Visitor Tracking

In a web application, one of the common use cases for HashSet is to track unique visitors to a website. This can be done by capturing the IP addresses of visitors and ensuring that duplicate visits from the same IP are not counted.

Let's consider a scenario where we need to keep track of unique visitors over a period of time. The goal is to store the IP addresses of visitors, but only count each IP once, regardless of how many times that IP accesses the site.

 

 

Use Case Scenario: Tracking Unique Website Visitors

The web application logs IP addresses of visitors to a specific webpage. To track how many unique visitors visited the site, we can use a HashSet. As the HashSet automatically ensures no duplicates, this will allow us to store and count only unique IP addresses.

Problem:

We want to track the number of unique visitors (based on their IP address) to the website in real time.

Solution:

We'll use a HashSet to store unique IP addresses. Each time a user visits the webpage, their IP address will be added to the set. If the user has already visited, their IP address will not be added again.

 

 

import java.util.HashSet;
import java.util.Set;

public class UniqueVisitorTracker {
   
   // Set to store unique IP addresses
   private Set<String> uniqueVisitors;

   // Constructor to initialize the set
   public UniqueVisitorTracker() {
       uniqueVisitors = new HashSet<>();
   }

   // Method to log a visit from an IP address
   public void logVisit(String ipAddress) {
       // Add the IP address to the HashSet
       if (uniqueVisitors.add(ipAddress)) {
           System.out.println("New visitor logged: " + ipAddress);
       } else {
           System.out.println("Visitor from " + ipAddress + " has already visited.");
       }
   }

   // Method to get the count of unique visitors
   public int getUniqueVisitorCount() {
       return uniqueVisitors.size();
   }

   // Method to display all unique visitors (Optional)
   public void displayUniqueVisitors() {
       System.out.println("Unique Visitors: ");
       for (String ip : uniqueVisitors) {
           System.out.println(ip);
       }
   }

   public static void main(String[] args) {
       UniqueVisitorTracker tracker = new UniqueVisitorTracker();
       
       // Simulating visits from various IP addresses
       tracker.logVisit("192.168.1.1");
       tracker.logVisit("192.168.1.2");
       tracker.logVisit("192.168.1.3");
       tracker.logVisit("192.168.1.1");  // Duplicate visit
       tracker.logVisit("192.168.1.4");

       // Display the number of unique visitors
       System.out.println("Total unique visitors: " + tracker.getUniqueVisitorCount());

       // Optional: Display all unique visitors
       tracker.displayUniqueVisitors();
   }
}
 

 

  1. uniqueVisitors:
    • We use a HashSet<String> to store unique visitor IP addresses. The HashSet automatically handles duplicate entries. If an IP address is already in the set, it won’t be added again.
  2. logVisit(String ipAddress):
    • This method simulates logging a visitor's IP address when they access the site. The add() method of HashSet returns true if the IP address was not already in the set and was successfully added. If it returns false, it means the visitor has already been logged (i.e., it's a duplicate).
  3. getUniqueVisitorCount():
    • This method returns the total number of unique visitors by calling size() on the HashSet. Since the HashSet only contains unique IP addresses, its size will represent the number of unique visitors.
  4. displayUniqueVisitors():
    • This method simply prints out all the unique IP addresses stored in the HashSet.

 

Why HashSet is Ideal for This Use Case:

  1. No Duplicates:
    • A HashSet is perfect for ensuring uniqueness because it automatically prevents duplicate entries. If the same IP address tries to enter again, it’s simply ignored, making it efficient for tracking unique visitors.
  2. Efficient Lookups:
    • The add() method of HashSet performs O(1) on average, making it highly efficient for logging a visitor's IP address and checking if they’ve already been logged.
  3. Fast Counting:
    • The size() method of HashSet is O(1), allowing you to quickly get the total number of unique visitors.

Real-Time Application in a Web Server Context:

In a real-time application, such as a web server or analytics dashboard, the UniqueVisitorTracker could be used to track visitors over a day or week. This is especially useful for generating real-time metrics like:

  • Unique User Count for a given time period.
  • Tracking how many new visitors versus returning visitors access the website.
  • Analyzing traffic patterns and user engagement by keeping track of the number of unique visitors over different time intervals.

In a more advanced scenario, this logic could be extended to handle visitors based on IP ranges, geolocation, or even unique session IDs, and would be backed by databases and caching systems for persistence and speed.

 

 

LinkedHashSet Overview

1. LinkedHashSet Definition:

A LinkedHashSet in Java is part of the java.util package and implements the Set interface. It is an ordered version of a HashSet, meaning it maintains the order of insertion while still preventing duplicate elements. It provides constant time performance for basic operations like add, remove, and contains, but with the added feature of maintaining the order of insertion, which is not a guarantee in a standard HashSet.

  • Thread Safety: Like HashSet, LinkedHashSet is not synchronized by default, which means it is not thread-safe unless externally synchronized.
  • Use Case: A LinkedHashSet is useful when you need to ensure uniqueness of elements while also maintaining the order in which they were added. For example, maintaining a list of unique visitor sessions where the order of arrival matters.

2. Key Properties of LinkedHashSet:

  • Insertion Order: Maintains the order of elements as they are inserted into the set.
  • Duplicates: Does not allow duplicate elements. If you try to add a duplicate, it will be ignored.
  • Null Elements: Allows one null element, similar to HashSet.

3. Time Complexities for LinkedHashSet Operations:

OperationTime Complexity
AddO(1)
RemoveO(1)
ContainsO(1)
SizeO(1)
IterationO(n)

4. Common Methods:

  • add(E e): Adds the element to the set if it is not already present.
  • remove(Object o): Removes the specified element from the set.
  • contains(Object o): Checks if the set contains the specified element.
  • size(): Returns the number of elements in the set.
  • clear(): Removes all elements from the set.
  • iterator(): Returns an iterator over the elements in the set.

9 ways to retrieve elements from a LinkedHashSet:

package com.niteshsynergy.collection;

import java.util.LinkedHashSet;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.ArrayList;

public class Demo06LinkedHashSet {
   public static void main(String[] args) {
       // Creating a LinkedHashSet and adding elements
       LinkedHashSet<String> set = new LinkedHashSet<>();
       set.add("A");
       set.add("B");
       set.add("C");
       set.add("D");
       set.add("E");
       set.add("F");
       set.add("G");
       set.add("H");
       set.add("I");

       System.out.println("LinkedHashSet: " + set);

       // 1. Using the for-each loop (enhanced for loop):
       System.out.println("\nUsing for-each loop:");
       for (String element : set) {
           System.out.println(element);
       }
       // Time Complexity: O(n)

       // 2. Using an iterator:
       System.out.println("\nUsing Iterator:");
       Iterator<String> iterator = set.iterator();
       while (iterator.hasNext()) {
           System.out.println(iterator.next());
       }
       // Time Complexity: O(n)

       // 3. Using ListIterator (only possible with List, not directly with LinkedHashSet):
       // LinkedHashSet does not support ListIterator directly, but we can convert to a List first
       System.out.println("\nUsing ListIterator (converted to List):");
       ListIterator<String> listIterator = new ArrayList<>(set).listIterator();
       while (listIterator.hasNext()) {
           System.out.println(listIterator.next());
       }
       // Time Complexity: O(n)

       // 4. Using stream() method (Java 8+):
       System.out.println("\nUsing stream() method:");
       set.stream().forEach(System.out::println);
       // Time Complexity: O(n)

       // 5. Using forEach() method (Java 8+):
       System.out.println("\nUsing forEach() method:");
       set.forEach(System.out::println);
       // Time Complexity: O(n)

       // 6. Using toArray() method:
       System.out.println("\nUsing toArray() method:");
       Object[] array = set.toArray();
       for (Object element : array) {
           System.out.println(element);
       }
       // Time Complexity: O(n)

       // 7. Using a for loop with an index (after converting to ArrayList):
       System.out.println("\nUsing for loop with index:");
       ArrayList<String> list = new ArrayList<>(set);
       for (int i = 0; i < list.size(); i++) {
           System.out.println(list.get(i));
       }
       // Time Complexity: O(n)

       // 8. Using forEachRemaining() method of Iterator:
       System.out.println("\nUsing forEachRemaining() of Iterator:");
       iterator = set.iterator();  // Re-initialize iterator as it was already used
       iterator.forEachRemaining(System.out::println);
       // Time Complexity: O(n)

       // 9. Using Lambda expression with forEach() (Java 8+):
       System.out.println("\nUsing Lambda expression with forEach():");
       set.forEach(element -> System.out.println(element));
       // Time Complexity: O(n)
   }
}
 

5. Example Use Case: Implementing Session Tracking in a Web Application

In this scenario, we can use LinkedHashSet to track the unique session IDs of users visiting a website, ensuring that we can not only track which sessions are active but also preserve the order in which the sessions were created.

 

import java.util.LinkedHashSet;
import java.util.Iterator;

public class SessionTracker {
   
   // LinkedHashSet to store unique session IDs
   private LinkedHashSet<String> activeSessions;

   // Constructor to initialize the LinkedHashSet
   public SessionTracker() {
       activeSessions = new LinkedHashSet<>();
   }

   // Method to log a new session
   public void logSession(String sessionId) {
       if (activeSessions.add(sessionId)) {
           System.out.println("New session started: " + sessionId);
       } else {
           System.out.println("Session already active: " + sessionId);
       }
   }

   // Method to remove a session (e.g., user logs out)
   public void removeSession(String sessionId) {
       if (activeSessions.remove(sessionId)) {
           System.out.println("Session ended: " + sessionId);
       } else {
           System.out.println("Session not found: " + sessionId);
       }
   }

   // Method to get the number of active sessions
   public int getActiveSessionCount() {
       return activeSessions.size();
   }

   // Method to display all active sessions
   public void displayActiveSessions() {
       System.out.println("Active Sessions: ");
       Iterator<String> iterator = activeSessions.iterator();
       while (iterator.hasNext()) {
           System.out.println(iterator.next());
       }
   }

   public static void main(String[] args) {
       SessionTracker tracker = new SessionTracker();
       
       // Simulating session starts and ends
       tracker.logSession("session1");
       tracker.logSession("session2");
       tracker.logSession("session3");
       tracker.logSession("session2"); // Duplicate session
       tracker.removeSession("session1"); // Ending session1
       tracker.logSession("session4");

       // Display the count of active sessions
       System.out.println("Active session count: " + tracker.getActiveSessionCount());

       // Display all active sessions
       tracker.displayActiveSessions();
   }
}
 

  1. activeSessions:
    • We use a LinkedHashSet<String> to store active session IDs. The LinkedHashSet ensures that each session is unique and also preserves the order of insertion, making it easier to track which sessions started first.
  2. logSession(String sessionId):
    • This method simulates logging a new session. The add() method of LinkedHashSet ensures that duplicate sessions are not added. If a session ID is already in the set, the method simply ignores the new addition.
  3. removeSession(String sessionId):
    • This method simulates a user logging out and their session ending. The remove() method removes the session ID from the set.
  4. getActiveSessionCount():
    • This method returns the number of active sessions, which is simply the size of the LinkedHashSet.
  5. displayActiveSessions():
    • This method displays all the active sessions in the order in which they were logged.

 

Real-Time Complex Example: Using LinkedHashSet for Session Tracking in a Web Application

In a real-time web application (such as an e-commerce site or a banking application), session tracking is crucial to monitor the active sessions of users on the website. A session ID is generated when a user logs into the system, and the LinkedHashSet is used to track these session IDs. By using LinkedHashSet, we ensure that:

  • Only unique session IDs are counted, preventing session duplication.
  • The order of session creation is preserved, which can be useful for debugging or analysis.
  • The performance of session tracking remains optimal with constant time complexity for operations like adding, removing, and checking session presence.

Advantages of Using LinkedHashSet:

  1. Ordered Tracking: The sessions are tracked in the order they were created. This can be useful for features like session expiration where older sessions might need to be cleared first.
  2. Efficient and Simple: The add(), remove(), and contains() operations all have O(1) time complexity on average, making it efficient for large-scale applications.
  3. Real-Time Session Management: This can be part of a larger session management system where you need to:
    • Track the number of users currently logged in.
    • Handle session expiration.
    • Provide audit logs that show the order of user actions.

This approach ensures that the system remains scalable and efficient, especially when handling millions of users in real-time applications like social media platforms, e-commerce websites, or online banking.

 

 

TreeSet Overview

1. TreeSet Definition:

A TreeSet in Java is part of the java.util package and implements the Set interface. It is a NavigableSet, meaning it provides methods to traverse and navigate the set. The elements in a TreeSet are ordered, either according to their natural ordering (for elements that implement Comparable) or by a Comparator provided at set creation time.

  • Thread Safety: TreeSet is not synchronized and hence not thread-safe unless externally synchronized.
  • Use Case: TreeSet is used when you need a sorted collection of unique elements. It automatically sorts the elements based on their natural order or a custom comparator, making it suitable for scenarios like maintaining a sorted list of items, performing range queries, and more.

2. Key Properties of TreeSet:

  • Insertion Order: Maintains a sorted order, not the insertion order.
  • Duplicates: Does not allow duplicate elements. If a duplicate element is added, it is ignored.
  • Null Elements: Does not allow null elements. If you attempt to add a null element, a NullPointerException will be thrown.

3. Time Complexities for TreeSet Operations:

OperationTime Complexity
AddO(log n)
RemoveO(log n)
ContainsO(log n)
SizeO(1)
IterationO(n)

 

4. Common Methods:

  • add(E e): Adds the element to the set. If the element is already present, it is ignored.
  • remove(Object o): Removes the specified element from the set.
  • contains(Object o): Checks if the set contains the specified element.
  • size(): Returns the number of elements in the set.
  • clear(): Removes all elements from the set.
  • first(): Returns the first (lowest) element in the set.
  • last(): Returns the last (highest) element in the set.
  • headSet(E toElement): Returns a view of the portion of the set whose elements are strictly less than toElement.
  • tailSet(E fromElement): Returns a view of the portion of the set whose elements are greater than or equal to fromElement.

 

9 Ways to Access Elements from a TreeSet:

package com.niteshsynergy.collection;

import java.util.TreeSet;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.ArrayList;

public class Demo07TreeSet {
   public static void main(String[] args) {
       // Creating a TreeSet and adding elements
       TreeSet<String> set = new TreeSet<>();
       set.add("A");
       set.add("B");
       set.add("C");
       set.add("D");
       set.add("E");
       set.add("F");
       set.add("G");
       set.add("H");
       set.add("I");

       System.out.println("TreeSet: " + set);

       // 1. Using the for-each loop (enhanced for loop):
       System.out.println("\nUsing for-each loop:");
       for (String element : set) {
           System.out.println(element);
       }
       // Time Complexity: O(n)

       // 2. Using an iterator:
       System.out.println("\nUsing Iterator:");
       Iterator<String> iterator = set.iterator();
       while (iterator.hasNext()) {
           System.out.println(iterator.next());
       }
       // Time Complexity: O(n)

       // 3. Using ListIterator (only possible with List, not directly with TreeSet):
       // TreeSet does not support ListIterator directly, but we can convert to a List first
       System.out.println("\nUsing ListIterator (converted to List):");
       ListIterator<String> listIterator = new ArrayList<>(set).listIterator();
       while (listIterator.hasNext()) {
           System.out.println(listIterator.next());
       }
       // Time Complexity: O(n)

       // 4. Using stream() method (Java 8+):
       System.out.println("\nUsing stream() method:");
       set.stream().forEach(System.out::println);
       // Time Complexity: O(n)

       // 5. Using forEach() method (Java 8+):
       System.out.println("\nUsing forEach() method:");
       set.forEach(System.out::println);
       // Time Complexity: O(n)

       // 6. Using toArray() method:
       System.out.println("\nUsing toArray() method:");
       Object[] array = set.toArray();
       for (Object element : array) {
           System.out.println(element);
       }
       // Time Complexity: O(n)

       // 7. Using a for loop with an index (after converting to ArrayList):
       System.out.println("\nUsing for loop with index:");
       ArrayList<String> list = new ArrayList<>(set);
       for (int i = 0; i < list.size(); i++) {
           System.out.println(list.get(i));
       }
       // Time Complexity: O(n)

       // 8. Using forEachRemaining() method of Iterator:
       System.out.println("\nUsing forEachRemaining() of Iterator:");
       iterator = set.iterator();  // Re-initialize iterator as it was already used
       iterator.forEachRemaining(System.out::println);
       // Time Complexity: O(n)

       // 9. Using Lambda expression with forEach() (Java 8+):
       System.out.println("\nUsing Lambda expression with forEach():");
       set.forEach(element -> System.out.println(element));
       // Time Complexity: O(n)
   }
}
 

6. Internal Working of TreeSet:

  • Underlying Data Structure: TreeSet is backed by a Red-Black Tree (a balanced binary search tree).
  • Red-Black Tree: This tree structure maintains the elements in a balanced way. The balancing is done by enforcing specific properties, ensuring that the longest path from the root to a leaf is no more than twice the length of the shortest path.
  • Sorting: The tree maintains the elements in a sorted order, either using their natural ordering (for elements that implement Comparable) or using a custom Comparator provided at the time of set creation.
  • Time Complexity: All basic operations (add, remove, contains) have O(log n) time complexity due to the balanced nature of the Red-Black Tree.

 

Natural Sorting (Using Comparable)

In this example, the TreeSet uses the natural ordering of the Integer elements (which implement the Comparable interface) to sort the elements in ascending order.

 

import java.util.TreeSet;

public class TreeSetSortingExample {

   public static void main(String[] args) {
       // Creating a TreeSet that uses the natural order for sorting (ascending order)
       TreeSet<Integer> treeSet = new TreeSet<>();
       
       // Adding elements
       treeSet.add(30);
       treeSet.add(10);
       treeSet.add(20);
       
       // Output will be sorted in ascending order
       System.out.println("TreeSet sorted by natural order (ascending):");
       treeSet.forEach(System.out::println);
   }
}
 

TreeSet sorted by natural order (ascending):
10
20
30
 

  • In this case, the TreeSet sorts the elements automatically based on their natural order (Integer class implements Comparable, so it sorts numbers in ascending order by default).

 

2. Custom Sorting (Using Comparator)

Here, we provide a custom comparator to sort the elements in descending order.

 

import java.util.Comparator;
import java.util.TreeSet;

public class TreeSetSortingExample {

   public static void main(String[] args) {
       // Custom comparator to sort elements in descending order
       Comparator<Integer> reverseComparator = (a, b) -> b - a;
       
       // Creating a TreeSet with the custom comparator
       TreeSet<Integer> treeSet = new TreeSet<>(reverseComparator);
       
       // Adding elements
       treeSet.add(30);
       treeSet.add(10);
       treeSet.add(20);
       
       // Output will be sorted in descending order
       System.out.println("TreeSet sorted by custom comparator (descending):");
       treeSet.forEach(System.out::println);
   }
}
 

TreeSet sorted by custom comparator (descending):
30
20
10
 

  • In this case, the custom comparator ((a, b) -> b - a) sorts the elements in descending order. The comparator compares the elements in reverse, ensuring that the larger numbers come first.
  • Natural Sorting (Comparable):
    • TreeSet uses the compareTo method defined in the Comparable interface (which Integer implements) to order the elements in their natural ascending order.
  • Custom Sorting (Comparator):
    • You can provide a custom sorting order by using a Comparator. In this example, we define a comparator for descending order (b - a) and pass it to the TreeSet constructor. This allows sorting elements in descending order.

 

Example Use Case for Default Natural Sorting:

Consider a case where we need to store the top scores in a game and maintain them in sorted order. We can use a TreeSet to ensure that the scores are sorted in ascending order by default.

 

import java.util.*;

public class GameScores {
   public static void main(String[] args) {
       // Using TreeSet to store unique scores in ascending order
       TreeSet<Integer> topScores = new TreeSet<>();
       
       // Adding scores to the TreeSet
       topScores.add(50);
       topScores.add(30);
       topScores.add(40);
       topScores.add(70);
       
       // Printing the sorted scores
       System.out.println("Top Scores: ");
       topScores.forEach(System.out::println); // Output will be 30, 40, 50, 70
   }
}
 

9. Example Use Case for Custom Sorting with Comparator:

Imagine you are building a system where you need to store employees' records based on their salary, but in descending order. This can be done using a custom comparator.

 

import java.util.*;

class Employee {
   String name;
   int salary;

   Employee(String name, int salary) {
       this.name = name;
       this.salary = salary;
   }

   @Override
   public String toString() {
       return name + ": " + salary;
   }
}

public class EmployeeSortExample {
   public static void main(String[] args) {
       // Custom comparator to sort by salary in descending order
       Comparator<Employee> salaryComparator = (e1, e2) -> Integer.compare(e2.salary, e1.salary);

       TreeSet<Employee> employees = new TreeSet<>(salaryComparator);
       
       employees.add(new Employee("Alice", 50000));
       employees.add(new Employee("Bob", 60000));
       employees.add(new Employee("Charlie", 55000));
       
       // Printing employees sorted by salary in descending order
       employees.forEach(System.out::println); // Output will be: Bob: 60000, Charlie: 55000, Alice: 50000
   }
}
 

 

10. ClassCastException and How to Resolve It:

A ClassCastException occurs when elements in a TreeSet cannot be compared using the specified comparator or their natural ordering. This can happen when:

  1. Incompatible Elements: The elements in the TreeSet are not of a type that can be compared (e.g., mixing String and Integer).
  2. Elements Not Implementing Comparable: If a custom class doesn't implement Comparable and no comparator is provided.

How to Resolve It:

  • Ensure that all elements in the TreeSet are of a type that can be compared using their natural ordering or the provided comparator.
  • If you're using custom objects, make sure they implement Comparable or provide a Comparator when creating the TreeSet.

 

class Person implements Comparable<Person> {
   String name;
   int age;

   Person(String name, int age) {
       this.name = name;
       this.age = age;
   }

   @Override
   public int compareTo(Person other) {
       return this.age - other.age; // Sorting by age
   }

   @Override
   public String toString() {
       return name + ": " + age;
   }
}

public class PersonSort {
   public static void main(String[] args) {
       TreeSet<Person> people = new TreeSet<>();
       people.add(new Person("Alice", 30));
       people.add(new Person("Bob", 25));
       people.add(new Person("Charlie", 35));
       
       people.forEach(System.out::println); // Sorted by age: Bob: 25, Alice: 30, Charlie: 35
   }
}
 

Collision Issues in TreeSet

A collision in a TreeSet occurs when two elements are considered equal according to the sorting mechanism (either Comparable or Comparator). However, TreeSet does not use hashCode() and equals() methods, which are typically associated with hash-based collections like HashSet. Instead, it uses the compareTo() method from Comparable or the compare() method from Comparator for element comparisons.

So, in the context of a TreeSet, there is no direct collision issue like in hash-based collections (such as HashSet) because:

  1. TreeSet does not use hash values: The TreeSet is based on a balanced tree structure (typically a Red-Black Tree), and comparisons between elements are done using their natural order (if they implement Comparable) or via a custom Comparator.
  2. Duplicate Handling: If two elements are considered equal by the sorting mechanism, the second element will simply not be added to the TreeSet, because it maintains a set of unique elements.

Role of equals() and hashCode() in TreeSet

In the context of a TreeSet, the equals() and hashCode() methods are not directly used for the element comparison or ordering. These methods are typically used in hash-based collections like HashSet, HashMap, or Hashtable.

However, if you have custom objects in the set that implement Comparable or are compared using a Comparator, the equals() and hashCode() methods may still play a role in certain edge cases, like when checking object equality in specific scenarios or performing collections operations outside of the TreeSet.

Need for equals() and hashCode() in TreeSet

  1. compareTo() vs equals():
    • For a TreeSet, the compareTo() method is the most important method, as it is used to determine the order of elements in the set.
    • equals() is generally not required for TreeSet to function correctly because it's used for object equality checks, not ordering.
    • However, if two objects are "equal" according to the compareTo() method (i.e., their compareTo() returns 0), they should logically be considered equal and thus the second instance should not be added.
  2. When equals() and hashCode() are important:
    • If you are using both HashSet and TreeSet for some operations (e.g., removing elements from a TreeSet by checking with a HashSet), equals() and hashCode() would then play a role.
    • For example, in collections like HashSet or HashMap, both equals() and hashCode() are used to determine if two elements are equivalent.

Example of TreeSet Behavior with Duplicate Elements

Let's consider a simple Person class:

 

import java.util.*;

class Person implements Comparable<Person> {
   String name;
   int age;

   Person(String name, int age) {
       this.name = name;
       this.age = age;
   }

   @Override
   public int compareTo(Person other) {
       return Integer.compare(this.age, other.age);  // Sorting by age
   }

   @Override
   public String toString() {
       return name + ": " + age;
   }
}

public class TreeSetExample {
   public static void main(String[] args) {
       TreeSet<Person> treeSet = new TreeSet<>();
       treeSet.add(new Person("Alice", 30));
       treeSet.add(new Person("Bob", 25));
       treeSet.add(new Person("Charlie", 30)); // Same age as Alice

       treeSet.forEach(System.out::println);
   }
}
 

 

Here, the TreeSet keeps only one instance of Alice and Charlie because their age is the same, and the compareTo() method is based on the age. Even though they are different objects, they are considered equal by the sorting criteria, and only one will be retained in the TreeSet

 

Conclusion:

  1. Collision issues are not a concern for TreeSet because it does not use hashCode() and equals() for ordering. Instead, it uses compareTo() (for natural ordering) or a custom Comparator.
  2. equals() and hashCode() are not necessary for TreeSet to function properly, but they are essential in certain situations (like in other hash-based collections or for checking equality in a broader context).
  3. When implementing your own compareTo() (or Comparator), ensure that it is consistent with equals() (i.e., if two elements are considered equal by compareTo(), they should also be considered equal by equals() to avoid logical inconsistencies).

 

Real-Time Complex Project Use Case: Using TreeSet for Employee Management System

Scenario:

Imagine a company that manages employee records, and we need to implement a system where employees are sorted by their salary in ascending order. Employees should also be unique based on their employee ID. Additionally, employees should be able to receive bonuses based on certain criteria, and the system should automatically maintain an up-to-date sorted list.

For this system, we will use a TreeSet to store employees, and we will ensure the sorting of employees by salary while enforcing uniqueness based on employee ID.

 

1. Project Overview:

We are implementing an Employee Management System that stores the details of employees and automatically sorts them by their salary. Employees are considered equal if they have the same Employee ID (using compareTo and ensuring consistency with equals). We'll use TreeSet to maintain a unique and sorted collection of employees.

 

2. Classes and Implementation

We will implement the following components:

  • Employee Class: Contains attributes like employeeId, name, and salary.
  • EmployeeComparator: A custom comparator to sort employees based on their salary.
  • EmployeeManagementSystem: Main class to manage employees and use TreeSet to keep the data sorted.

 

import java.util.Objects;

public class Employee implements Comparable<Employee> {
   private int employeeId;
   private String name;
   private double salary;

   public Employee(int employeeId, String name, double salary) {
       this.employeeId = employeeId;
       this.name = name;
       this.salary = salary;
   }

   public int getEmployeeId() {
       return employeeId;
   }

   public String getName() {
       return name;
   }

   public double getSalary() {
       return salary;
   }

   @Override
   public int compareTo(Employee other) {
       // Sorting by salary in ascending order
       return Double.compare(this.salary, other.salary);
   }

   @Override
   public boolean equals(Object obj) {
       if (this == obj) return true;
       if (obj == null || getClass() != obj.getClass()) return false;
       Employee employee = (Employee) obj;
       return employeeId == employee.employeeId;
   }

   @Override
   public int hashCode() {
       return Objects.hash(employeeId);
   }

   @Override
   public String toString() {
       return "Employee{id=" + employeeId + ", name='" + name + "', salary=" + salary + "}";
   }
}
 

 

Employee Comparator for Custom Sorting (Optional)

If you want to sort the employees differently (e.g., by name or other attributes), you can implement a custom comparator.

 

import java.util.Comparator;

public class EmployeeComparator implements Comparator<Employee> {
   @Override
   public int compare(Employee e1, Employee e2) {
       // Compare by name if salaries are the same
       if (Double.compare(e1.getSalary(), e2.getSalary()) == 0) {
           return e1.getName().compareTo(e2.getName());
       }
       return Double.compare(e1.getSalary(), e2.getSalary());
   }
}
 

 

Employee Management System:

The main system will add employees to a TreeSet (which will automatically sort and maintain uniqueness) and allow operations such as adding employees, removing them, and updating salaries.

 

import java.util.*;

public class EmployeeManagementSystem {
   private Set<Employee> employeeSet;

   public EmployeeManagementSystem() {
       // Using TreeSet to automatically sort by salary (using compareTo)
       this.employeeSet = new TreeSet<>();
   }

   // Add a new employee
   public void addEmployee(Employee employee) {
       if (!employeeSet.add(employee)) {
           System.out.println("Employee with ID " + employee.getEmployeeId() + " already exists.");
       } else {
           System.out.println("Employee added: " + employee);
       }
   }

   // Update an employee's salary (by removing and adding again)
   public void updateSalary(int employeeId, double newSalary) {
       // Find and remove the employee
       employeeSet.removeIf(employee -> employee.getEmployeeId() == employeeId);
       // Add the updated employee with new salary
       Employee updatedEmployee = findEmployeeById(employeeId);
       if (updatedEmployee != null) {
           updatedEmployee = new Employee(employeeId, updatedEmployee.getName(), newSalary);
           addEmployee(updatedEmployee);
       } else {
           System.out.println("Employee not found with ID: " + employeeId);
       }
   }

   // Find an employee by ID
   public Employee findEmployeeById(int employeeId) {
       for (Employee employee : employeeSet) {
           if (employee.getEmployeeId() == employeeId) {
               return employee;
           }
       }
       return null;
   }

   // Display all employees in the set
   public void displayEmployees() {
       System.out.println("Employee List (Sorted by Salary):");
       for (Employee employee : employeeSet) {
           System.out.println(employee);
       }
   }

   // Remove an employee by ID
   public void removeEmployee(int employeeId) {
       employeeSet.removeIf(employee -> employee.getEmployeeId() == employeeId);
   }

   public static void main(String[] args) {
       EmployeeManagementSystem ems = new EmployeeManagementSystem();

       // Adding employees
       ems.addEmployee(new Employee(101, "Alice", 50000));
       ems.addEmployee(new Employee(102, "Bob", 60000));
       ems.addEmployee(new Employee(103, "Charlie", 45000));
       ems.addEmployee(new Employee(104, "David", 60000));

       // Display all employees (sorted by salary)
       ems.displayEmployees();

       // Updating an employee's salary
       ems.updateSalary(103, 70000);

       // Display all employees after update
       ems.displayEmployees();

       // Removing an employee
       ems.removeEmployee(102);
       ems.displayEmployees();
   }
}
 

 

Why Use TreeSet for Employee Management System?

  1. Automatic Sorting: The TreeSet automatically sorts employees based on their salary when they are added, so there is no need for explicit sorting logic.
  2. Unique Elements: Since the TreeSet uses the compareTo() method to check for equality, it ensures that employees are unique based on their employeeId (you can modify this logic to check for any uniqueness).
  3. Efficiency: The TreeSet provides O(log n) time complexity for insertion, removal, and searching, making it a suitable choice when dealing with a large set of employee data that needs to be kept sorted.

 

 

Overview of PriorityQueue

A PriorityQueue is often used in applications such as job scheduling, Dijkstra’s shortest path algorithm, and Huffman encoding. The primary feature of the queue is that the highest priority element is always processed first.

The elements of the PriorityQueue are ordered either by their natural ordering (if the elements implement Comparable) or by a custom comparator provided at the time of construction.

Key Points:

  • A binary heap is typically used to implement a PriorityQueue internally.
  • The front element is always the one with the highest priority.
  • Duplicates are allowed.
  • The PriorityQueue does not guarantee a first-in-first-out (FIFO) ordering.

2. Common Methods of PriorityQueue

Here are some important methods provided by the PriorityQueue class:

  • add(E e): Adds the specified element to the queue.
  • offer(E e): Similar to add(), but returns false if the element cannot be added.
  • poll(): Retrieves and removes the head of the queue (element with the highest priority).
  • peek(): Retrieves, but does not remove, the head of the queue.
  • remove(Object o): Removes a specific element from the queue.
  • size(): Returns the number of elements in the queue.
  • isEmpty(): Returns true if the queue is empty.
  • clear(): Removes all elements from the queue.

3. Internal Working of PriorityQueue

Internally, a PriorityQueue is typically implemented as a binary heap (a complete binary tree), which allows for efficient insertion and removal operations:

  • Insertion (add/offer): Takes O(log n) time, where n is the number of elements in the queue.
  • Accessing the head (peek): Takes O(1) time since the root element (head) is always the highest priority.
  • Removing the head (poll): Takes O(log n) time, as it involves removing the root and re-adjusting the heap.

4. Sorting Mechanism in PriorityQueue

Default Sorting (Natural Sorting)

If the elements in the queue implement the Comparable interface, the queue will use the natural ordering of the elements to sort them.

For example, if you are storing Integer objects in a PriorityQueue, the queue will order the elements in ascending order by default because Integer implements Comparable.

Custom Sorting (Using Comparator)

If you want to change the way elements are sorted, you can provide a custom comparator. This is useful if the natural ordering does not meet your needs.

For example, you can use a comparator to create a max heap (elements with the highest value come first), or sort objects in a custom order like reverse order or by any other property.

 

import java.util.Comparator;
import java.util.PriorityQueue;

public class MaxHeapExample {

   public static void main(String[] args) {
       // Custom comparator to create a max heap
       Comparator<Integer> comparator = (a, b) -> b - a;  // Reverse order for max heap

       // Creating a PriorityQueue with the custom comparator
       PriorityQueue<Integer> maxHeap = new PriorityQueue<>(comparator);

       // Adding elements to the max heap
       maxHeap.add(10);
       maxHeap.add(20);
       maxHeap.add(30);
       maxHeap.add(5);
       maxHeap.add(15);

       // Output: The largest element will be retrieved first
       System.out.println("Max Heap elements in order of extraction:");
       while (!maxHeap.isEmpty()) {
           System.out.println(maxHeap.poll());  // poll() removes and returns the root (max element)
       }
   }
}
 

  • Custom Comparator:
    • The comparator (a, b) -> b - a is used to reverse the natural ordering, effectively creating a max heap. In this case, the larger element b is placed before a to ensure that the largest element is always at the root of the heap.
  • PriorityQueue:
    • PriorityQueue<Integer> is used to create a heap. By default, it is a min-heap (smallest element comes first), but by passing the custom comparator (b - a), we convert it into a max heap.
  • Adding Elements:
    • We add several integers to the PriorityQueue, and the custom comparator ensures that the heap organizes itself such that the largest element is always at the top.
  • Polling the Max Heap:
    • Using poll(), we can remove and retrieve the largest element from the heap. As the largest element is always at the root, calling poll() repeatedly gives the elements in descending order.

7 Ways to access 

import java.util.PriorityQueue;
import java.util.Iterator;

public class Demo08PriorityQueue {
   public static void main(String[] args) {
       // Creating a PriorityQueue
       PriorityQueue<String> queue = new PriorityQueue<>();
       queue.add("D");
       queue.add("A");
       queue.add("B");
       queue.add("C");
       queue.add("E");

       System.out.println("PriorityQueue: " + queue);

       // 1. Using peek() method (to get the top element)
       System.out.println("\nUsing peek() method:");
       System.out.println(queue.peek());  // Returns the top element without removing it
       // Time Complexity: O(1)

       // 2. Using poll() method (to get and remove the top element)
       System.out.println("\nUsing poll() method:");
       System.out.println(queue.poll());  // Returns and removes the top element
       System.out.println("PriorityQueue after poll: " + queue);
       // Time Complexity: O(log n)

       // 3. Using iterator() method (to iterate over elements)
       System.out.println("\nUsing Iterator:");
       Iterator<String> iterator = queue.iterator();
       while (iterator.hasNext()) {
           System.out.println(iterator.next());
       }
       // Time Complexity: O(n)

       // 4. Using for-each loop
       System.out.println("\nUsing for-each loop:");
       for (String element : queue) {
           System.out.println(element);
       }
       // Time Complexity: O(n)

       // 5. Using toArray() method (to convert the PriorityQueue to an array)
       System.out.println("\nUsing toArray() method:");
       Object[] array = queue.toArray();
       for (Object element : array) {
           System.out.println(element);
       }
       // Time Complexity: O(n)

       // 6. Using forEach() method (Java 8+)
       System.out.println("\nUsing forEach() method:");
       queue.forEach(System.out::println);
       // Time Complexity: O(n)

       // 7. Using stream() method (Java 8+)
       System.out.println("\nUsing stream() method:");
       queue.stream().forEach(System.out::println);
       // Time Complexity: O(n)
   }
}
 

 

5. Real-Time Use Case: Priority Queue for Query Processing in a Search Engine

In this example, we will build a PriorityQueue to manage queries based on their priority, where each query has a priority level and a time stamp. The queue will allow us to always process the highest priority query first.

Project Overview

Let’s consider a search engine system where users submit search queries, and each query has a priority. The system needs to handle these queries based on priority and the timestamp, ensuring that high-priority queries are processed before low-priority ones, and queries with the same priority are processed in the order of their arrival.

Classes to Implement

  1. Query Class: This represents a search query with a priority and timestamp.
  2. QueryComparator: A comparator class to sort queries by priority and timestamp.
  3. QueryProcessor: This class uses a PriorityQueue to manage the queries.

 

public class Query {
   private String queryText;
   private int priority;
   private long timestamp;

   public Query(String queryText, int priority, long timestamp) {
       this.queryText = queryText;
       this.priority = priority;
       this.timestamp = timestamp;
   }

   public String getQueryText() {
       return queryText;
   }

   public int getPriority() {
       return priority;
   }

   public long getTimestamp() {
       return timestamp;
   }

   @Override
   public String toString() {
       return "Query{" +
               "queryText='" + queryText + '\'' +
               ", priority=" + priority +
               ", timestamp=" + timestamp +
               '}';
   }
}
 

 

2. Query Comparator (Custom Sorting)

import java.util.Comparator;

public class QueryComparator implements Comparator<Query> {
   @Override
   public int compare(Query q1, Query q2) {
       // First compare by priority
       if (q1.getPriority() != q2.getPriority()) {
           return Integer.compare(q2.getPriority(), q1.getPriority()); // Max priority first
       }

       // If priority is same, compare by timestamp (older queries processed first)
       return Long.compare(q1.getTimestamp(), q2.getTimestamp());
   }
}
 

3. QueryProcessor (Priority Queue Implementation)

import java.util.PriorityQueue;

public class QueryProcessor {
   private PriorityQueue<Query> queryQueue;

   public QueryProcessor() {
       // Using the custom comparator to sort queries by priority and timestamp
       this.queryQueue = new PriorityQueue<>(new QueryComparator());
   }

   // Method to add a query to the queue
   public void addQuery(Query query) {
       queryQueue.offer(query);
       System.out.println("Query added: " + query);
   }

   // Method to process the highest priority query
   public void processQuery() {
       Query query = queryQueue.poll();
       if (query != null) {
           System.out.println("Processing query: " + query);
       } else {
           System.out.println("No queries to process.");
       }
   }

   // Display all the queries in the queue
   public void displayQueries() {
       System.out.println("Queries in the queue:");
       for (Query query : queryQueue) {
           System.out.println(query);
       }
   }

   public static void main(String[] args) {
       QueryProcessor processor = new QueryProcessor();

       // Adding queries
       processor.addQuery(new Query("Search for Java tutorials", 2, System.currentTimeMillis()));
       processor.addQuery(new Query("Search for Kotlin tutorials", 1, System.currentTimeMillis()));
       processor.addQuery(new Query("Search for JavaScript tutorials", 3, System.currentTimeMillis()));

       // Display the queries (sorted by priority and timestamp)
       processor.displayQueries();

       // Processing the highest priority query
       processor.processQuery();

       // Display remaining queries
       processor.displayQueries();
   }
}
 

Real-Time Complex Example: Using PriorityQueue for Search Engine Query Handling

In a search engine scenario, multiple queries arrive at the same time, and the system must process them based on their priority (e.g., paid advertisements might have higher priority) and the timestamp (earlier queries should be processed first).

Use Case Scenario:

  1. High-priority queries (e.g., paid search ads) need to be processed before low-priority queries (e.g., organic search).
  2. Queries with the same priority need to be processed in the order in which they arrive (using their timestamp).

 

import java.util.PriorityQueue;
import java.util.Comparator;

public class QueryProcessor {
   private PriorityQueue<Query> queryQueue;

   // Constructor to initialize the PriorityQueue with the custom comparator
   public QueryProcessor() {
       this.queryQueue = new PriorityQueue<>(new QueryComparator());
   }

   // Method to add a query to the queue
   public void addQuery(Query query) {
       queryQueue.offer(query);
       System.out.println("Query added: " + query);
   }

   // Method to process the highest priority query
   public void processQuery() {
       Query query = queryQueue.poll();
       if (query != null) {
           System.out.println("Processing query: " + query);
       } else {
           System.out.println("No queries to process.");
       }
   }

   // Method to handle queries with different priorities
   public static void main(String[] args) {
       QueryProcessor processor = new QueryProcessor();

       // Adding queries with different priorities
       processor.addQuery(new Query("Buy iPhone 15", 5, System.currentTimeMillis()));
       processor.addQuery(new Query("Search for Java tutorials", 2, System.currentTimeMillis()));
       processor.addQuery(new Query("Search for Kotlin tutorials", 3, System.currentTimeMillis()));
       processor.addQuery(new Query("Search for JavaScript tutorials", 1, System.currentTimeMillis()));

       // Displaying queries (they will be sorted by priority and timestamp)
       System.out.println("Displaying queries after insertion:");
       processor.queryQueue.forEach(query -> System.out.println(query));

       // Process the highest priority query
       processor.processQuery();

       // Display remaining queries after processing one
       System.out.println("Remaining queries after processing:");
       processor.queryQueue.forEach(query -> System.out.println(query));
   }
}


 

Rest Of Topic You Can Do Yourself ! 

Support My Work - Donate if You Find My Content Valuable

If you find my content valuable, please consider donating via Razorpay . You can add your name details in the note section on the payment page.

We are committed to helping individuals like you learn and navigate the tech stack in a better way. Your support enables us to continue providing high-quality learning resources.

If you have any additional topics—technical or non-technical—that you would like to explore, feel free to reach out to me at contact@niteshsynergy.com .

109 min read
Nov 19, 2024
By Nitesh Synergy
Share