Java

Circular Linked List

Unlocking the Power of Circular Linked Lists in Java: A Comprehensive Guide

Linked lists are a fundamental data structure in computer science, offering flexibility and efficiency in managing collections of data. Among the variations of linked lists, the circular linked list stands out for its unique structure and applications. In this blog post, we’ll delve into the concept of circular linked lists, explore their implementation in Java, and discuss their advantages and use cases.

Understanding Circular Linked Lists

A circular linked list is similar to a regular linked list, with the key distinction being that the last node points back to the first node, forming a circle. Unlike linear linked lists, which have a NULL pointer at the end, circular linked lists have no NULL pointers within the list itself. This circular structure allows for traversal from any node to any other node within the list.

The basic components of a circular linked list include:

  • Node: A unit of data that contains both the data value and a reference (pointer) to the next node in the sequence.
  • Head: A reference to the first node in the list. In a circular linked list, this node is connected to the last node, forming a circle.
  • Tail: Although not always explicitly maintained, it refers to the last node in the list, which points back to the head.

How to represent a circular singly linked list in java?

A Circular Singly Linked List is similar to a singly linked list, with the difference that in a circular linked list, the last node points to the first node and not null. Instead of keeping track of the head, we keep track of the last node in the circular singly linked list.

For example:

In a Singly Linked List:

head –> 1 –> 8 –> 10 –> 16 –> null

In a Circular Singly Linked List:

Java
last --> 16 --> 1 --> 8 --> 10 --> 
    ^____________________________|

last –> 16 –> 1 –> 8 –> 10 –> 16 // Please refer to the actual diagram for clarification.

We can insert nodes at both the end and beginning with constant time complexity.

How to implement a Circular Singly Linked List in a java ?

Java
public class CircularSinglyLinkedList {

  private ListNode last;        // Keep track of the last node of the circular linked list   
  private int length;           // Hold the size of the circular singly list

  private class ListNode {
    private int data;
    private ListNode next;

    public ListNode(int data) {
       this.data = data;
    }
  }

  public CircularSinglyLinkedList() {
    last = null;       // When we initialize the circular singly linked list, we know the last points to null and that time the list is empty 
    length = 0;        // So the length is also 0;
  }

  // Gives the size of the circular singly linked list 
  public int length() {
    return length;
  }

  // Check whether the circular list is empty or not 
  public boolean isEmpty() {
    return length == 0;
  }


  public void createCircularLinkedList() {
    ListNode first = new ListNode(1);
    ListNode second = new ListNode(5);
    ListNode third = new ListNode(10);
    ListNode fourth = new ListNode(15);
 
    first.next = second;
    second.next = third;
    third.next = fourth;
    fourth.next = first;      // Here we make the list in circular nature by assigning the first node 

    last = fourth;         // Last node points to the fourth node
  }


  public static void main(String[] args) {
     CircularSinglyLinkedList csll = new CircularSinglyLinkedList();
     csll.createCircularLinkedList();
  }
}

This Java class CircularSinglyLinkedList defines a circular singly linked list. It includes inner class ListNode for representing each node in the list. The class provides methods to create a circular linked list, check its length, and check whether it is empty. The main method demonstrates creating a circular linked list instance and initializing it.

How to traverse and print a circular linked list in java ?

Java
last --> 16 --> 1 --> 8 --> 10 --> 
    ^____________________________|

Algorithm & Execution

Java
if (last == null) {
    return;
}
ListNode first = last.next;
while (first != last) {
    System.out.println(first.data + "");
    first = first.next;
}
System.out.println(first.data + "");

The basic idea here is to find the first node using the last node, and then traverse the list from the first node to the last node. When the first node equals the last node, at that point, we are at the last node, but our while loop terminates, so we couldn’t print the last node’s data. That’s why we print it separately after the loop.

Code

Java
public void display() {
   if (last == null) {
      return;
   }
   ListNode first = last.next;
   while (first != last) {
     System.out.println(first.data + "");
     first = first.next;
   }
   System.out.println(first.data + "");
}

This method display is designed to print the elements of the circular singly linked list. It starts from the first node, which is the node after the last node. Then, it traverses the list until it reaches the last node, printing the data of each node. Finally, it prints the data of the last node itself. If the list is empty (i.e., last is null), the method simply returns without performing any operation.

How to insert node at the beginning of a circular Singly Linked List in java ?

Java
ListNode temp = new ListNode(data);
if (last == null) {
    last = temp;
} else {
    temp.next = last.next;
}
last.next = temp;
length++;
  1. We create a temporary node (temp) which we will insert at the beginning of the circular list.
  2. If the last node is null, it means our list is empty. In this case, we assign the temp node to be the last node. Both last and temp now point to the same new node we inserted. We update the last.next pointer to point to temp, forming the circular nature for the first node.
  3. If the list is not empty (last is not null), we create a new temp node with the given data, and it points to null initially. Then, we update temp.next to point to last.next, so it will be added at the beginning of the last node. Even now, last and temp.next will point to the last node. We need to update the last node’s next pointer to temp so that the circular list nature remains intact.
  4. Finally, we increment the length by 1 because every time we add a new node at the beginning of the last node.

Code

Java
public void insertFirst(int data) {
  ListNode temp = new ListNode(data);
  if (last == null) {
      last = temp;
  } else {
      temp.next = last.next;
  }
  last.next = temp;
  length++;
}

This method insertFirst is designed to insert a new node with the given data at the beginning of the circular singly linked list. If the list is empty (i.e., last is null), the new node becomes both the first and last node. Otherwise, the new node is inserted after the last node, and its next reference is updated to point to the node originally after the last node. Finally, the length of the list is incremented.

How to insert node at the end of a circular singly linked list in Java?

Java
ListNode temp = new ListNode(data);
if (last == null) {   // list is empty
   last = temp;
   last.next = last;
} else {              // if list is non-empty
   temp.next = last.next;
   last.next = temp;
   last = temp;
}
length++;
  1. First, we create a new temporary node (temp) with the given data value.
  2. Initially, the circular list is empty, so last will point to null, i.e., last --> null.
  3. When we insert a new node, we check whether we are inserting into an empty list or a non-empty list.
  4. If the list is empty, we point last to our temporary node, i.e., last = temp.
  5. To create a circular structure, we need to make last.next point to last itself, i.e., last.next = last.
  6. After that, we increment the length by 1 as we successfully inserted a new node into the list.
  7. Now, let’s consider the scenario of a non-empty circular list. In this case, first, we assign last.next to the last node itself when there is only one node. If there is more than one node, then last.next will point to the first node, and temp.next will point to null.
  8. Then, we attach the new temporary node to the end by assigning it as last.next. Previously, it pointed to itself because there was only one node. If there are more than one node, it will point to the temp node because the last node becomes the first, and temp will always become the last node to maintain the circular chain nature.
  9. Finally, temp becomes our new last because it is added at the end.

So, the basic logic is to add the temporary node at the end and always make that newly inserted node the last one.

How to remove first node from a circular singly linked list in java ?

Java
if (isEmpty()) {
   throw new NoSuchElementException();
}
ListNode temp = last.next;
if (last.next == last) {
   last = null;
} else {
   last.next = temp.next;
}
temp.next = null;
length--;
return temp;
  1. If the list is empty, meaning there are no elements to remove, we throw a NoSuchElementException.
  2. We create a temporary node temp which will store the node to be removed, i.e., the first node (last.next).
  3. If last.next points to last itself, it means there is only one node in the list. In this case, we remove the last node by assigning null to last.
  4. If last.next doesn’t point to last, it means there are multiple nodes in the list. We remove the first node by updating last.next to point to the second node (temp.next).
  5. We then set temp.next to null to detach temp from the list.
  6. We decrement the length of the list by 1 since we have successfully removed a node.
  7. Finally, we return the removed node temp.

So, the main logic is to remove the first node by adjusting pointers and then returning the removed node.

Advantages of Circular Linked Lists

Circular linked lists offer several advantages over their linear counterparts:

  1. Efficient Insertion and Deletion: Insertion and deletion operations can be performed quickly, especially at the beginning or end of the list, as there’s no need to traverse the entire list.
  2. Circular Traversal: With a circular structure, traversal from any point in the list to any other point becomes straightforward.
  3. Memory Efficiency: Circular linked lists save memory by eliminating the need for a NULL pointer at the end of the list.

Use Cases of Circular Linked Lists

Circular linked lists find applications in various scenarios, including:

  • Round-Robin Scheduling: In operating systems, circular linked lists are used to implement round-robin scheduling algorithms, where tasks are executed in a circular order.
  • Music and Video Playlists: Circular linked lists can be used to implement circular playlists, allowing seamless looping from the last item to the first.
  • Resource Management: In resource allocation systems, circular linked lists can represent a pool of resources where allocation and deallocation operations are frequent.

Conclusion

Circular linked lists provide an elegant solution to certain problems by leveraging their circular structure and efficient operations. In Java, implementing a circular linked list involves managing node connections carefully to maintain the circular property. Understanding the strengths and applications of circular linked lists can aid in designing efficient algorithms and data structures for various computational tasks.

Singly Linked List in Java

Unlocking Singly Linked List Logical Operations in Java: Master Your Skills

Singly linked lists are versatile data structures that offer efficient insertion, deletion, and traversal operations. However, beyond the basic CRUD (Create, Read, Update, Delete) operations, there lies a realm of logical operations that can be performed on singly linked lists. In this detailed blog, we’ll explore some of the most important logical operations that can be applied to singly linked lists in Java, providing insights into their implementation and usage.

How to search an element in a Singly Linked List in Java?

head –> 10 –> 8 –> 1 –> 11 –> null

Java
ListNode current = head;
while(current != null)
{
  if(current.data == searchKey)
  {
    return true;
  }
  current = current.next;
}
return false;

As the main logic, we need to traverse the list node by node. While traversing, we check each node’s data. If it matches the search key, then we’ve found the key; otherwise, we haven’t found it.

  1. We create a temporary node current to traverse the list until the end. Initially, it’s set to the head node, i.e., ListNode current = head.
  2. Using a while loop, we traverse until the end of the list. If current becomes null, it means we’ve reached the end, and we terminate the loop. During traversal, we check each node’s data with the search key. If we find a match, we return true immediately and exit the loop.
Java
while( current  != null )
{
  if(current.data == searchKey)
  {
   return true;
  }
  current = current.next;      // Move to the next node in each iteration 
}
  1. Finally, if we haven’t found the exact search key after traversing the entire list, we return false.

Output:

  • If the search key is 1, then it is found.
  • If the search key is 12, then it is not found.

Code

Java
public boolean find(ListNode head, int searchKey) {
  if (head == null) {
    return false;
  }

  ListNode current = head;
  while (current != null) {
    if (current.data == searchKey) {
      return true;
    }
    current = current.next; // Move to the next node
  }
  return false;
}

This method find is designed to search for a specific key value (searchKey) within the linked list. It returns true if the key is found, and false otherwise. If the list is empty (i.e., head is null), it immediately returns false. Otherwise, it traverses the list, comparing the data value of each node (current.data) with the search key. If a match is found, it returns true. If the end of the list is reached without finding the key, it returns false.

How to reverse a singly linked list in java

Input:

head –> 10 –> 8 –> 1 — > 11 –> null

Output:

head –> 11 –> 1 –> 8 –> 10 –> null

Java
ListNode current = head;
ListNode previous = null;
ListNode next = null;
while(current != null)
{
  next = current.next;
  current.next = previous;
  previous = current;
  current = next;
}
return previous;

The main logic is to traverse the list until the end and apply a logic that reverses the pointing of each node. Ultimately, we obtain the reversed list.

  1. We create three temporary nodes:
    • current points to head.
    • previous initially points to null.
    • next also initially points to null.
  2. We traverse the list node by node using a while loop. The loop iterates until current becomes null.
  3. In each iteration of the while loop, we perform the following operations:
Java
while(current != null)
{
   next = current.next;        // Store the reference to the next node
   current.next = previous;    // Reverse the pointing direction of the current node
   previous = current;         // Move forward: previous becomes current
   current = next;             // Move forward: current becomes next
}
    • First, we move to the next node by assigning next to current.next.
    • Second, we reverse the reference of the current node to point to the previous node.
    • Third, we update previous to be the current node, preparing for the next iteration.
    • Finally, we move forward by assigning next to current.
    This process effectively reverses the pointing direction of each node in the list.
  1. When the while loop terminates, we have reversed the entire list, and the last previous node becomes the new head of the list. So, we return previous.

Output: head --> 11 --> 1 --> 8 --> 10 --> null.

After the reversal, the list becomes reversed.

Code

Java
public ListNode reverse(ListNode head)
{
   if(head == null)
   {
      return head;
   }
   
   ListNode current = head;
   ListNode previous = null;
   ListNode next = null;

   while(current != null)
   {
      next = current.next;
      current.next = previous;
      previous = current;
      current = next;
   }
   return previous;
}

This method reverse is designed to reverse the linked list. If the list is empty (i.e., head is null), it immediately returns null. Otherwise, it iterates through the list, changing the next pointer of each node to point to the previous node. At the end of the iteration, it returns the last node encountered, which becomes the new head of the reversed list.

How to find middle node in singly linked list in java ?

To find the middle node in a singly linked list, we employ the same logic for two different cases.

Case 1: List having an even number of nodes:

For example, head –> 10 –> 8 –> 1 –> 11 –> null

In this case, the middle node is 1.

Case 2: List having an odd number of nodes:

For example, head –> 10 –> 8 –> 1 –> 11 –> 15 –> null

Here again, the middle node is 1.

Java
ListNode slowPtr = head;
ListNode fastPtr = head;
while(fastPtr != null && fastPtr.next != null) {
    slowPtr = slowPtr.next;      // Move slow pointer to the next node
    fastPtr = fastPtr.next.next; // Move fast pointer to two nodes ahead
}
return slowPtr; // Return the slow pointer, which points to the middle node

The main logic involves using two different pointers: a slow pointer and a fast pointer. The slow pointer moves to the next node one by one, while the fast pointer moves two nodes ahead at a time. When the fast pointer reaches the end (either pointing to null or its next points to null), the while loop terminates, and we return the slow pointer, which represents the middle node in both cases.

Code

Java
public ListNode getMiddleNode() {
    if (head == null) {
      return null;
    }
    
    ListNode slowPtr = head;
    ListNode fastPtr = head;
 
    while (fastPtr != null && fastPtr.next != null) {
       slowPtr = slowPtr.next;
       fastPtr = fastPtr.next.next;
    }
    return slowPtr;
}

This method getMiddleNode is designed to find and return the middle node of the linked list. It initializes two pointers, slowPtr and fastPtr, both starting at the head of the list. The slowPtr moves one node at a time while the fastPtr moves two nodes at a time. When the fastPtr reaches the end of the list (or null), the slowPtr will be at the middle node. If the list is empty (i.e., head is null), it returns null.

How to detect a loop in Singly Linked List in java ?

In a given singly linked list, if there exists a loop, it can be identified by employing the following logic.

Consider the linked list: head –> 1 –> 2 –> 3 –> 4 –> 5 –> 6 –> 3

As it can be seen, the list loops back to the node with value 3.

The main logic remains the same as before: we use two different pointers, a slow pointer and a fast pointer. However, in this case, we move the fast pointer first, followed by the slow pointer. Due to the loop, these pointers will eventually meet at the same node. Once the slow and fast pointers are equal, pointing to the same node, we can conclude that there exists a loop in the linked list. If the pointers never meet, then the list does not contain any loop.

Java
ListNode fastPtr = head;
ListNode slowPtr = head;
while(fastPtr != null && fastPtr.next != null) {
    fastPtr = fastPtr.next.next; // Move fast pointer two nodes ahead
    slowPtr = slowPtr.next;      // Move slow pointer one node ahead
    if(slowPtr == fastPtr) {     // If slow pointer meets fast pointer, it indicates a loop
        return true;
    }
}
return false; // If loop termination condition is met without meeting points, return false

The main logic is the same as before: we use two pointers, a slow pointer and a fast pointer, to traverse the list. However, in this case, we move the fast pointer two nodes ahead and the slow pointer one node ahead in each iteration. If the pointers meet at any point during traversal, it indicates the presence of a loop in the list, and we return true. If the loop termination condition is met without the pointers meeting, it means there is no loop in the list, and we return false.

Code

Java
public boolean containsLoop() {
  ListNode fastPtr = head;
  ListNode slowPtr = head;

  while (fastPtr != null && fastPtr.next != null) {
    fastPtr = fastPtr.next.next;        // We need to move fast pointer fast so that it will catch the slow pointer if a loop is present   
    slowPtr = slowPtr.next;
   
    if (slowPtr == fastPtr) {
      return true;
    }
  }
  return false;
}

public void createALoopInLinkedList() {
  ListNode first = new ListNode(1);  
  ListNode second = new ListNode(2);
  ListNode third = new ListNode(3);
  ListNode fourth = new ListNode(4);
  ListNode fifth = new ListNode(5);
  ListNode sixth = new ListNode(6);

  head = first;
  first.next = second;
  second.next = third;
  third.next = fourth;
  fourth.next = fifth;
  fifth.next = sixth;
  sixth.next = third;
}

The method containsLoop checks whether a loop exists in the linked list using Floyd’s Cycle Detection Algorithm. It initializes two pointers, fastPtr and slowPtr, both starting at the head of the list. The fastPtr moves twice as fast as the slowPtr. If there is a loop in the list, eventually, the fastPtr will catch up with the slowPtr. If no loop is found, the method returns false.

The method createALoopInLinkedList is a helper method to create a loop in the linked list for testing purposes. It creates a linked list with six nodes and then creates a loop by making the next reference of the last node point to the third node.

How to find nth node from the end of a Singly Linked List in java?

Consider the singly linked list:

head –> 10 –> 8 –> 1 –> 11 –> 15 –> null

If we want to find the node that is “n” positions from the end of the list, where “n” is given as 2, then the node containing 11 would be that node.

Java
ListNode mainPtr = head;  // It will move forward when the reference pointer covers the nth position forward from the head 
ListNode referencePtr = head;  // It will move twice: first, it covers the nth distance from the head, then it goes till the end with mainPtr, so that mainPtr will reach the exact position
int count = 0;   // It is to track the number of nodes the reference pointer moved forward
while(count < n) {
    refPtr = refPtr.next;
    count++;
}
while(refPtr != null) {
    refPtr = refPtr.next;
    mainPtr = mainPtr.next;
}

return mainPtr;

The main logic involves using two pointers: a main pointer and a reference pointer. The reference pointer moves forward until it reaches the nth position from the head, while the main pointer remains stationary. After reaching the nth position, the reference pointer continues moving until it reaches the end of the list, while the main pointer moves along with it. When the reference pointer reaches the end of the list, the main pointer will be pointing to the nth node from the end of the list.

Code

Java
public ListNode getNthNodeFromEnd(int n) {

 if (head == null) {
    return null;
 }
 
 if (n <= 0) {
     throw new IllegalArgumentException("Invalid value: n = " + n);
 }

 ListNode mainPtr = head;  // It will move forward when the reference pointer covers the nth position forward from the head  
 ListNode refPtr = head;   // It will move twice: first, it covers nth distance from head, then it goes till the end with mainPtr, so that mainPtr will reach the exact position.

 int count = 0;   // It is to track the number of nodes refPtr moved forward

 while (count < n) {
  if (refPtr == null) {
      throw new IllegalArgumentException(n + " is greater than the number of nodes in the list");
  }

  refPtr = refPtr.next;
  count++;
 }

 while (refPtr != null) {
  refPtr = refPtr.next;
  mainPtr = mainPtr.next;
 }

 return mainPtr;    // The returned mainPtr will be at the nth position from the end of the list
}

This method getNthNodeFromEnd is designed to find and return the nth node from the end of the linked list. It initializes two pointers, mainPtr and refPtr, both starting at the head of the list. The refPtr moves forward n positions from the head. Then, both pointers move forward simultaneously until the refPtr reaches the end of the list. At this point, the mainPtr will be at the nth node from the end. If the list is empty or if the value of n is less than or equal to 0, the method throws an IllegalArgumentException.

How to remove duplicates from sorted Singly Linked List in java?

For the given input of a sorted linked list:

head –> 1 –> 1 –> 2 –> 3 –> 3 –> null

The desired output is a sorted linked list with duplicates removed:

head –> 1 –> 2 –> 3 –> null

Java
ListNode current = head;
while(current != null && current.next != null) {
    if(current.data == current.next.data) {
        current.next = current.next.next; // Connect current to the next next node to remove the duplicate node
    } else {
        current = current.next; // Move to the next node if no duplicate is found
    }
}

The main logic involves traversing the list node by node using a current pointer. While traversing, we check whether the data of the current node is equal to the data of the next node. If they are equal, it means a duplicate node is found, so we connect the current node to the next next node, effectively removing the duplicate node between them. If no duplicate is found, we simply move to the next node. This process continues until we reach the end of the list or the current node becomes null.

Code

Java
public void removeDuplicates() {
  
  if (head == null) {
    return;
  }

  ListNode current = head;

  while (current != null && current.next != null) {
     if (current.data == current.next.data) {
       current.next = current.next.next;
     } else {
       current = current.next;
     }
  }
}

This method removeDuplicates is designed to remove duplicates from a sorted linked list. It iterates through the list using the current pointer. If the current node’s data is equal to the data of the next node, it skips the next node by updating the next reference of the current node to skip the duplicate node. Otherwise, it moves the current pointer to the next node in the list. If the list is empty (i.e., head is null), the method returns without performing any operation.

Now, How to insert a node in a sorted Singly Linked List in java?

Given the sorted linked list:

head –> 1 –> 8 –> 10 –> 16 –> null

And a new node:

newNode –> 11 –> null

We want to insert the new node (11) into the sorted list such that the sorting order remains the same.

After insertion, the updated list would be:

head –> 1 –> 8 –> 10 –> 11 –> 16 –> null

Java
ListNode current = head;
ListNode previous = null;
while(current != null && current.data < newNode.data) {
    previous = current;
    current = current.next;
}
// When we reach the insertion point, we have references to current, previous, and newNode.
// Now, we rearrange the pointers so that previous points to newNode and newNode points to current.
newNode.next = current;
if (previous != null) {
    previous.next = newNode;
} else {
    // If previous is null, it means the newNode should become the new head.
    head = newNode;
}
return head;

The main logic involves traversing the sorted linked list until we find the appropriate position to insert the new node while maintaining the sorting order. We traverse the list node by node, comparing the data of each node with the data of the new node. We continue this process until we find a node whose data is greater than or equal to the data of the new node, or until we reach the end of the list.

When we reach the insertion point, we have references to three nodes: the current node, the previous node (the node before the insertion point), and the new node. To insert the new node into the list, we rearrange the pointers so that the previous node points to the new node, and the new node points to the current node.

If the previous node is null, it means that the new node should become the new head of the list. In this case, we update the head pointer to point to the new node.

Code

Java
public ListNode insertInSortedList(int value) {
  ListNode newNode = new ListNode(value);
 
  if (head == null) {
    return newNode;
  }

  ListNode current = head;
  ListNode previous = null;

  while (current != null && current.data < newNode.data) {   // Will go till the end while checking the sorting order between the current node and the new node data 
     previous = current;
     current = current.next;
  }
  
  // When we reach the insertion point, we have our current, previous, and newNode references so we only need to arrange pointers.
  // So that previous will point to newNode and newNode will point to current.
 
  newNode.next = current;
  if (previous == null) { // If the new node is to be inserted at the beginning
    head = newNode;
  } else {
    previous.next = newNode;
  }
  
  return head;    
}

This method insertInSortedList is designed to insert a new node with the provided value into a sorted linked list. If the list is empty (i.e., head is null), the new node becomes the head of the list. Otherwise, it traverses the list to find the correct position to insert the new node while maintaining the sorted order. Once the insertion point is found, it updates the pointers to insert the new node. Finally, it returns the head of the list.

How to remove a given key from singly linked list in java?

Given the linked list:

head –> 1 –> 8 –> 10 –> 11 –> 16 –> null

Suppose our key is 11, and we want to remove it from the list.

After removal, the updated list would be:

head –> 1 –> 8 –> 10 –> 16 –> null

Java
ListNode current = head;
ListNode previous = null;

// Traverse the list to find the node with the key value
while(current != null && current.data != key) {
   previous = current;
   current = current.next;
}

// If we reached the end of the list without finding the key, return
if(current == null) {
  return;
}

// If we found the key, remove the node by adjusting the previous node's next reference
if(previous != null) {
  previous.next = current.next;
} else {
  // If the key is found at the head, update the head pointer to skip the current node
  head = current.next;
}

The main logic involves traversing the linked list until we find the node with the specified key value. While traversing, we keep track of the previous node as well.

If we reach the end of the list without finding the key, it means the key doesn’t exist in the list, so we return without performing any removal.

If we find the node with the key value, we remove it from the list by adjusting the next reference of the previous node to skip over the current node. However, if the key is found at the head of the list, we update the head pointer to skip over the current node.

Code

Java
public void deleteNode(int key) {
 
  ListNode current = head;
  ListNode previous = null;

  // If we find our key at the first node that is head, just update head to point to the next node.
  if (current != null && current.data == key) {
     head = current.next;
     return;
  }

  while (current != null && current.data != key) {
    previous = current;
    current = current.next;
  }
 
  // If we reached the end of the list (current == null), the key was not found, so return without performing any operation.
  if (current == null) {
    return;
  }

  // If we found the key, update the next reference of the previous node to point to the next node of the current node, effectively removing the current node.
  previous.next = current.next;
}

This method deleteNode is designed to delete a node with the given key value from the linked list. It iterates through the list using the current pointer to find the node with the specified key value while keeping track of the previous node using the previous pointer. If the key is found at the first node (head), it updates the head to point to the next node. If the key is found in the middle of the list, it updates the next reference of the previous node to skip the current node. If the key is not found in the list, the method simply returns without performing any operation.

Bonus: Two Sum Problem in java

Problem: Given an array of integers, return the indices of the two numbers such that they add up to a specific target.

Example: Given array of integers: {2, 11, 5, 10, 7, 8}, and target = 9.

Solution: Since arr[0] + arr[4] = 2 + 7 = 9, we return {0, 4} as the indices.

The main logic involves using a map for fast lookup of stored values to find the exact sum of the target. We require one map for lookup purposes and one result array to store the indices of the two numbers that add up to the target sum from the given array. Here’s how it works:

  1. We iterate through the array, examining each element.
  2. At each element, we calculate the difference between the target and the current element.
  3. We check if this difference exists in the map. If it does, it means we have found the two numbers that add up to the target.
  4. We return the indices of the current element and the element with the required difference.
  5. If the difference is not found in the map, we store the current element’s value along with its index in the map for future lookups.

Algorithm & Executions

Java
int[] result = new int[2];
Map<Integer, Integer> map = new HashMap<>();

for(int i = 0; i < numbers.length; i++) {
    int complement = target - numbers[i];
    if(map.containsKey(complement)) {
        result[0] = map.get(complement);
        result[1] = i;
        return result;
    }
    map.put(numbers[i], i);
}

return result;

The main logic involves using a hash map to store the indices of elements in the array. We traverse the array and, for each element, check if its complement (target – current number) exists in the hash map. If it does, it means we have found two numbers that add up to the target, so we return their indices. If not, we add the current number and its index to the hash map for future reference.

Code

Java
public static int[] twoSum(int[] numbers, int target) {
  int[] result = new int[2];
  Map<Integer, Integer> map = new HashMap<>();
 
  for (int i = 0; i < numbers.length; i++) {
    if (!map.containsKey(target - numbers[i])) {
      map.put(numbers[i], i);
    } else {
      result[1] = i;
      result[0] = map.get(target - numbers[i]);
      return result;
    }     
  }

  throw new IllegalArgumentException("Two numbers not found");
}

This method twoSum is designed to find and return the indices of the two numbers in the numbers array that add up to the target value. It utilizes a hashmap to store the difference between the target and each element of the array along with its index. It iterates through the array, checking if the hashmap contains the difference between the target and the current element. If not, it adds the current element and its index to the hashmap. If it finds the difference in the hashmap, it retrieves the index of the other number and returns the indices as the result. If no such pair of numbers is found, it throws an IllegalArgumentException.

Note: Why is this Array Manipulation problem being discussed here?

While the problem of finding two numbers in an array that add up to a specific target is not directly related to singly linked list operations, the underlying logic and problem-solving techniques used in solving array manipulation problems can indeed be helpful in solving problems related to singly linked lists.

Many problem-solving techniques and algorithms used in array manipulation, such as iterating over elements, using hash maps for fast lookups, or employing two-pointer approaches, can also be applied to singly linked list problems. Additionally, understanding how to efficiently manipulate data structures and analyze patterns in data is a fundamental skill that can be transferred across various problem domains.

In the context of computer science and algorithmic problem-solving, building a strong foundation in problem-solving techniques through various types of problems, including those involving arrays, linked lists, trees, graphs, and more, can enhance your ability to tackle a wide range of problems effectively.

So, while the specific problem discussed may not directly relate to singly linked lists, the problem-solving skills and techniques learned from array manipulation problems can certainly be beneficial in solving problems related to singly linked lists and other data structures.

Conclusion

By mastering the logical operations of singly linked list in Java, programmers can unlock the full potential of this versatile data structure. Whether it’s searching, reversing, merging, or detecting loops, understanding these operations equips developers with powerful tools for solving complex problems and building efficient algorithms. With the insights provided in this blog, programmers can elevate their skills and become more proficient in leveraging singly linked lists for various applications.

SinglyLinkedList and It's Logical Operations

Mastering Singly Linked Lists: Essential Basic Insertion and Deletion Operations Explained

Singly linked lists are a fundamental data structure in computer science, offering dynamic flexibility and memory efficiency. In Java, understanding and mastering them is key to unlocking efficient algorithms and problem-solving skills. This blog aims to guide you from basic concepts to advanced techniques, transforming you from a linked list novice to a ninja!

Anatomy of a Singly Linked List

Each node in a singly linked list comprises two parts: data and a reference to the next node. This pointer-based structure enables dynamic growth and memory management. The first node is called the head, and the last node has a null reference as its next node.

Anatomy of a Singly Linked List

In a singly linked list, each node points to the next node in the sequence, forming a linear chain. The last node typically points to a null reference, indicating the end of the list.

How to represent a Linked List in Java ?

A linked list is a fundamental data structure employed for the storage of a series of elements, objects, or nodes, characterized by the following attributes:

  1. Sequential arrangement of nodes.
  2. Each node comprises data along with a pointer to the subsequent node.
  3. The initial node serves as the head node.
  4. The final node contains data and points to null.
Java
| data | next | ===> | data | next | ===> | data | next | ===> null

Visualizing Nodes: Unraveling the Building Blocks

In the intricate world of linked lists, nodes take center stage, acting as the fundamental units that store data and guide traversals. Let’s dissect the visual representation you provided:

Node Structure:

Java
| data | next |===>
  • data: This field holds the actual value, serving as the heart of the information a node carries. Its contents can vary depending on the application, from simple integers to complex objects.
  • next: This crucial pointer acts as the bridge to the next node in the sequence. By following these references, we can navigate the entire linked list, one node at a time. The arrow -----> represents this connection, visually depicting the flow from one node to the next.

Linked List Illustration

Java
head ====> |10|===>|8|===>|9|===>|11|===> null
  • head: The special node marking the beginning of the linked list. Think of it as the entrance gate, providing the entry point for accessing the list’s elements.
  • Nodes: Each rectangular box, labeled with |data|====>|next|, represents a node in the chain. The values inside the data field indicate the stored values (10, 8, 9, 11).
  • null: This unique value, often denoted by null, signifies the end of the list. Just like reaching the end of a road, encountering null indicates there are no more nodes to follow.
Java
public class SinglyLinkedList {

  private ListNode head; // head node to hold the list
  
  // It contains a static inner class ListNode
  private static class ListNode {
     private int data;
     private ListNode next;

     public ListNode(int data) {
          this.data = data;
          this.next = null;
     }
  }

  public static void main(String[] args) {
   
  }
}

This Java code defines a class named SinglyLinkedList with a private member variable head, which represents the starting node of the linked list. Inside this class, there is a static inner class named ListNode, which encapsulates the data and reference to the next node. The constructor of ListNode initializes the data and sets the next node reference to null.

The main method serves as the entry point, although it’s currently devoid of content. We will proceed to furnish it with creation code shortly.

How to create a singly linked list in java ?

Java
head ---> 10 ---> 8 ---> 1 ---> 11 ---> null

Now, let’s see how we created it:

  1. Initially, the head node points to null because the linked list is empty, i.e., head —> null.
  2. Let’s create the first node with data 10, which also points to null, i.e., 10 —> null.
  3. To hold nodes one by one, the head points to the node with 10. At this stage, the our list contains one node, i.e., head —> 10 —>.
  4. Let’s create the second node with data 8 and next pointing to null, i.e., 8 —> null.
  5. Connect the head node to the second node so that the list will contain 2 nodes, i.e., head –> 10 –> 8 –> null.
  6. Create the third node with data 1 and next pointing to null, i.e., 1 –> null.
  7. Here, we connect the third node to the second node so that the list contains 3 nodes, i.e., head –> 10 –> 8 –> 1 –> null.
  8. Finally, create the fourth node with data 11 and next pointing to null, i.e., 11 –> null.
  9. Connect this fourth node with the third node. Now, the linked list size is 4, i.e., head –> 10 –> 8 –> 1 –> 11 –> null.

Lets write code in above SinglyLinkedList empty main method.

Java
// Let's create a linked list demonstrated in the comment below

// 10 --> 8 --> 1 --> 11 --> null
// 10 as head node of linked list

ListNode head = new ListNode(10);
ListNode second = new ListNode(8);
ListNode third = new ListNode(1);
ListNode fourth = new ListNode(11);

// Attach them together to form a list 

head.next = second;      // 10 --> 8
second.next = third;     // 10 --> 8 --> 1
third.next = fourth;     // 10 --> 8 --> 1 --> 11 --> null

This code snippet demonstrates the creation of a linked list with four nodes, where each node contains an integer value. The comments above describe the structure of the linked list being created, and the subsequent lines of code link the nodes together to form the desired sequence.

Final code look like:

Java
public class SinglyLinkedList {

    private ListNode head; // head node to hold the list

    // It contains a static inner class ListNode
    private static class ListNode {
        private int data;
        private ListNode next;

        public ListNode(int data) {
            this.data = data;
            this.next = null;
        }
    }

    public static void main(String[] args) {

        // 10 --> 8 --> 1 --> 11 --> null
        // 10 as head node of linked list

        ListNode head = new ListNode(10);
        ListNode second = new ListNode(8);
        ListNode third = new ListNode(1);
        ListNode fourth = new ListNode(11);

        // Attach them together to form a list 

        head.next = second; // 10 --> 8
        second.next = third; // 10 --> 8 --> 1
        third.next = fourth; // 10 --> 8 --> 1 --> 11 --> null

    }
}

How to print elements of Singly Linked List in java

As we know, to print all elements in a linked list, we need to traverse through all nodes in the linked list until the end of the linked list.

So,

head –> 10 –> 8 –> 1 –> 11 –> null

Algorithm & Execution

Java
current = head;
while(current != null)
{
    Sop(current.data);
    current = current.next;
}
  1. Let’s create a current node which initially points to null, i.e., current –> null. At this point, the output is null.
  2. Now we will assign the head node of the linked list to this current node, i.e., current = head.
  3. To traverse the list node by node until the end of the list, we will use a while loop which checks whether the current node is null. If it is null, that means we reached the end of the list.
  4. So, for every iteration, we will print that particular node’s data and also shift the current node pointer to the next node so that we traverse to the next node.

Code

Java
// Given a ListNode, prints all elements it hold 
public void display(ListNode head)
{
  if(head == null)
  {
    return;
  }

  ListNode current = head;
  
 // loop each element till end of the list 
 // last node points to null

  while(current != null)
  {
    System.out.println(current.data + " --> ");   // prints current element's data
    // move to next element
    current = current.next;
  }
  System.out.print(current);  // here current wil be null 
  
  
  

Run with -->

SinglyLinkedList singlyLinkedList = new SinglyLinkedList();
singlyLinkedList.display(head);


o/p : 10 --> 8 --> 1 --> 11 --> null

This code snippet defines a method display within the SinglyLinkedList class, responsible for printing all elements of the linked list starting from the provided head node. It iterates through the linked list nodes, printing each node’s data, and ends with printing “null” once the end of the list is reached. Finally, the method is invoked with a head node, displaying the elements of the linked list.

How to find the length of singly linked list in java?

Let’s determine the length of the linked list programmatically

Head –> 10 –> 8 –> 1 –> 11 –> null

Algorithm & Execution

Java
current = head;
int count = 0;
while(current != null)
{
  count++;
  current = current.next;
}
  1. We will create a temporary node called current, initially pointing to null.
  2. The current node is set to point to the head of the linked list.
  3. We create a count variable to keep track of the number of nodes present in the list, initialized as int count = 0.
  4. Execute the while loop until the current node reaches the end of the list. We check if the current node equals null; if yes, it means we’ve reached the end. For each iteration, we increment the count variable by 1, and the current node pointer moves to the next node.

Code

Java
// Given a ListNode head, find out the length of the linked list 
public int length(ListNode head) {
  if (head == null) {
    return 0;
  }

  // Create a count variable to hold the length
  int count = 0;

  // Loop through each element and increment the count until the list ends 
  ListNode current = head;
  whi le (current != null) {
    count++;
    // Move to the next node 
    current = current.next;
  }
  
  return count;
}

Here, code snippet defines a method length within the SinglyLinkedList class, which calculates the length of the linked list starting from the provided head node. It iterates through the linked list nodes, incrementing a count variable for each node encountered, until it reaches the end of the list. Finally, it returns the count representing the length of the linked list.


Singly Linked List Insertion Operation

One of the primary operations performed on singly linked lists is insertion, which involves adding new nodes at various positions within the list. Let’s explore their complexities, implementation techniques, and best practices.

How to inser node at the beginning of singly linked list in java

Inserting a new node at the beginning of a singly linked list is a straightforward operation. It involves creating a new node with the desired data value and adjusting the pointers to ensure proper linkage.

Head –> 10 –> 8 –> 1 –> 11 –> null

Algorithm & Execution

Java
ListNode newNode = new ListNode(15);
newNode.next = head;
head = newNode;
  1. The first step creates a new node with data 15 and next pointing to null, i.e., newNode –> 15 –> null.
  2. To insert the new node at the beginning of the list, update the next pointer of the new node to point to the current head node. This means connecting the new node’s next reference to the head node of the list, i.e., newNode –> 15 –> head.
  3. The final step updates the head pointer to point to the new node, making the new node the head node of the linked list.After execution, the linked list becomes: head –> 15 –> 10 –> 8 –> 1 –> 11 –> null.”

Code

Java
public ListNode insertAtBeginning(ListNode head, int data) {
  ListNode newNode = new ListNode(data);
  if (head == null) {
    return newNode;
  }
  newNode.next = head;
  head = newNode;
  return head;   // this head will be the new head, having the new node at the beginning  
}

This method insertAtBeginning is designed to insert a new node with the provided data at the beginning of the linked list. If the list is initially empty (i.e., head is null), the new node becomes the head of the list. Otherwise, the new node is inserted before the current head, and the head pointer is updated to point to the new node. Finally, the updated head is returned.

How to insert a node at the end of singly linked list in java?

Inserting a new node at the end of a singly linked list requires traversing the list until reaching the last node, then updating the last node’s reference to point to the new node.

Algorithm & Execution

Java
ListNode newNode = new ListNode(15);
ListNode current = head;
while(null != current.next)
{
 current = current.next;
}
current.next = newNode;
  1. Creates a new node with data 15 and next pointing to null, i.e., newNode –> 15 –> null.
  2. To insert the new node at the end of the list, we need to traverse the list till the end and then assign the new node to the last node’s next pointer. For this, we create a temporary node called current, which initially points to null. Then we make this current node point to the head of the list, i.e., ListNode current = head.
  3. We use a while loop to reach the last node of the list. For each iteration, we check whether the current node’s next pointer points to null or not. Also, for each iteration, we update current to its next pointer. When we reach the last node, we stop and terminate the while loop.
Java
while(current.next != null)
{
  current = current.next;
}

Finally, we assign the new node to current.next pointer because at this point, current is the last node of the list.

Code

Java
public ListNode insertAtEnd(ListNode head, int data) {
  ListNode newNode = new ListNode(data);
  if (head == null) {
    return newNode;
  }
  ListNode current = head;
  // Loop until the last node of the list (Note: it's not until the end of the list; in this case, current == null. Here, current.next being null means we're at the last node of the list.)
  while (current.next != null) {
    current = current.next;  // Move to the next node 
  }
  current.next = newNode;    // Assign the new node to the last node 
  return head;
}

This method insertAtEnd is designed to insert a new node with the provided data at the end of the linked list. If the list is initially empty (i.e., head is null), the new node becomes the head of the list. Otherwise, it traverses the list until it reaches the last node. Then, it assigns the new node as the next node of the last node and returns the head of the list.

How to insert a node in singly linked list after a given node in java ?

In this scenario, the initial step involves traversing the linked list until the preceding node of the insertion point is reached. Subsequently, node pointers are adjusted accordingly.

Algorithm & Execution

Java
ListNode newNode = new ListNode(15);
newNode.next = previous.next;
previous.next = newNode;
  1. Creates a new node with data 15 and its next pointer pointing to null.
  2. Since we’re inserting the new node between the specified previous node and its next node, we first assign the previous node’s next pointer to the new node’s next pointer. This means connecting the new node’s next pointer to the node that was originally after the previous node, i.e., newNode –> 15 –> 11 –> null.
  3. Finally, we update the previous node’s next pointer to point to the new node, so that the new node is placed between the specified previous node and its next node.After execution, the linked list becomes: head –> 10 –> 8 –> 15 –> 11 –> null.

Code

Java
public void insertAfter(ListNode previous, int data) {
  if (previous == null) {
    System.out.println("The given previous node cannot be null.");
    return;
  }

  ListNode newNode = new ListNode(data);
  newNode.next = previous.next;
  previous.next = newNode;
}

This method insertAfter is designed to insert a new node with the provided data after a specified node in the linked list. It first checks if the given previous node is not null. If it is null, it prints an error message and returns. Otherwise, it creates a new node with the provided data, links it to the node following the specified previous node, and then updates the next reference of the previous node to point to the new node.

How to insert a node in singly linked list at a given position in java?

Inserting a node at a specific position in a singly linked list involves traversing the list to find the desired position, then adjusting the pointers accordingly.

head –> 10 –> 8 –> 11 –> null

Given a linked list with three nodes, where the first node contains the value 10 and points to position 1, the second node contains the value 8 and points to position 2, we need to insert a new node at position 3. Therefore, to insert the new node at position 3, we must traverse the linked list until we reach position 2 and then insert the new node after position 2.

Algorithm & Execution

Java
ListNode newNode = new ListNode(15);
ListNode previous = head;
int count = 1;
while(count < position - 1)
{
  previous = previous.next;
  count++;
}
ListNode current = previous.next;
newNode.next = current;
previous.next = newNode;
  1. Creates a new node with data 15 and its next pointer pointing to null, i.e., 15 –> null.
  2. To insert the new node at the given position, we need to traverse until position – 1.
  3. We create a temporary node named previous and point it to the head of the list, i.e., previous –> head.
  4. To track the number of nodes traversed, we create a count variable and initialize it to 1, because the first node has already been traversed by the previous node, i.e., int count = 1.
  5. Let’s execute a few steps within the while loop. We traverse until just before the given position node, checking how many nodes we’ve traversed using the while loop until position – 1.
Java
while(count < position - 1)   // When we reach position - 1, the loop will terminate (when count = 2), and at this point, our 'previous' will point to position - 1.
{
  previous = previous.next;
  count++;    // As 'previous' moves forward, we increment count by one.
}
  1. Moving ahead, we create a temporary node named current, which will hold the next node after ‘previous’, i.e., current –> 11, and previous –> 8.
  2. The next pointer of the new node will point to the ‘current’ node, establishing a new connection between the new node and the ‘current’ node, i.e., newNode –> 15 –> 11.
  3. The final step is to set the next pointer of ‘previous’ to point to the new node, i.e., previous –> 8 –> newNode –> 15.

Output: head –> 10 –> 8 –> 15 –> 11 –> null. So, we inserted 15 at position 3, where ‘previous’ –> 8 and ‘current’ –> 11, and newNode –> 15 will be between the ‘previous’ and ‘current’ node.

Code

Java
public ListNode insertAtPosition(ListNode head, int data, int position) {
   // Perform boundary checks 
   int size = length(head);      // We use the already defined length method which returns the size of the list
   if (position > size + 1 || position < 1) {   // If the position is greater than the number of nodes in the list or less than 1, then it is an invalid position 
     System.out.println("Invalid position");
     return head;
   }

   ListNode newNode = new ListNode(data);
   if (position == 1) {                     // If position == 1, it means the list contains only one node and we want to insert at that position. Then we assign newNode next to head and return newNode as the new head of the list 
     newNode.next = head;
     return newNode;
   }
   else {
     // Else we perform our regular algorithm 
    ListNode previous = head;
    int count = 1;
    while (count < position - 1) {
       previous = previous.next;
       count++;
    }

    ListNode current = previous.next;
    newNode.next = current;
    previous.next = newNode;
    return head;
   }
 }
}

This method insertAtPosition is designed to insert a new node with the provided data at a specified position in the linked list. It first performs boundary checks to ensure that the position is valid. If the position is valid, it creates a new node with the provided data. If the position is 1, indicating that the node needs to be inserted at the beginning of the list, it updates the new node’s next reference to the current head and returns the new node as the new head of the list. Otherwise, it traverses the list to the node just before the specified position, updates the next reference of the new node to the current node at the specified position, and updates the next reference of the previous node to the new node. Finally, it returns the head of the list.


Singly Linked List Deletion Operation

Deletion operations play a crucial role in managing linked lists efficiently. In this section, we’ll delve into the intricacies of deleting nodes from singly linked lists, exploring various scenarios, implementation techniques, and best practices.

How to delete first node from a singly linked list in java?

Deleting a node from the beginning of a singly linked list is a straightforward operation. It involves updating the reference of the head node to point to the next node in the list.

head –> 10 –> 8 –> 1 –> 11 –> null;

Algorithm & Execution

Java
ListNode temp = head;
head = head.next;
temp.next = null;
  1. Let’s create a temporary node named temp and point it to head, i.e., ListNode temp = head.
  2. In order to delete the first node from the list, we need to remove the reference to the first node from the head pointer. Thus, we update the head pointer to point to the next node in the list, i.e., head = head.next.
  3. We disconnect the first node from the linked list by assigning null to the temp node’s next pointer, i.e., temp.next = null.

Output: head --> 8 --> 1 --> 11 --> null.

Initially, the list has 5 nodes. After deleting the first node, only three nodes remain.

Code

Java
public ListNode deleteFirst(ListNode head) {
   if (head == null) {
      return head;
   }

   ListNode temp = head;
   head = head.next;
   temp.next = null;
   return temp;
}

This method deleteFirst is designed to delete the first node of the linked list. If the list is empty (i.e., head is null), it returns null. Otherwise, it stores the reference to the current head node in a temporary variable temp, updates the head to the next node in the list, sets the next reference of the original head to null, and then returns the deleted node.

How to delete last node of singly linked list in java?

Deleting a node from the end of a singly linked list requires traversing the list until reaching the second-to-last node, then updating its reference to NULL.

head –> 10 –> 8 –> 11 –> null

Algorithm & Execution

Java
ListNode last = head;
ListNode previousToLast = null;
while(last.next != null)
{
  previousToLast = last;
  last = last.next;
}
previousToLast.next = null; // Disconnect the last node from the list
  1. First, we create a temporary node called last, which initially points to the head node, i.e., ListNode last = head.
  2. We create another temporary node called previousToLast and assign null to it.
  3. We traverse until the last node’s next pointer points to null, so that last becomes the last node of the list and previousToLast points to the second last node of the list.
Java
while(last.next != null)
{
   previousToLast = last;  // Becoming the previous node of the current last node 
   last = last.next;       // Move to the next node to become the last node for every iteration.
}
  1. In the final step, we disconnect the second last node from the last node by assigning null to its next pointer, i.e., previousToLast.next = null.
  2. Optionally, you can return the deleted last node if such a requirement exists, for example, for printing purposes.

Code

Java
public ListNode deleteLast(ListNode head) {
   if (head == null) {
       return head;
   }

   ListNode last = head;
   ListNode previousToLast = null;Singly 
  
   while (last.next != null) {
     previousToLast = last;
     last = last.next;
   }

   previousToLast.next = null;
   return last;
}

This method deleteLast is designed to delete the last node of the linked list. If the list is empty (i.e., head is null), it returns null. Otherwise, it traverses the list to find the last node (last) and the node just before it (previousToLast). It then updates the next reference of previousToLast to null, effectively removing the last node from the list. Finally, it returns the deleted node.

How to delete a node from Singly Linked List at a given position in Java?

Deleting a specific node from a singly linked list involves finding the node to be deleted and adjusting the pointers of its neighboring nodes to bypass it.

head –> 10 –> 8 –> 15 –> 11 –> null

Algorithm & Execution

Java
Listwhile(count < position - 1)
{
  previous = previous.next;
  count++;
}
Node previous = head;
int count = 1;
while(count < position - 1)
{
  previous = previous.next;
  count++;
}

ListNode current = previous.next;
previous.next = current.next;
current.next = null;
  1. We create a temporary node called previous, which initially points to the head.
  2. To keep track of the number of nodes we traverse, we use a count variable. Initially, it’s 1 because the first node has already been traversed by the previous node, which points to 10.
  3. We traverse until the node before the position we want to delete. This means we stop at position – 1.
Java
while(count < position - 1)
{
  previous = previous.next;
  count++;
}
  1. When the while loop terminates, we are at position – 1, which is the node before the node we want to delete.
  2. We create a temporary node called current, which holds the reference of the node to be deleted.
  3. To delete the node, we make previous.next point to current.next, effectively bypassing the current node in the linked list.
  4. Finally, we set current.next to null to disconnect it from the list.

Output: head --> 10 --> 8 --> 11 --> null.

As a result of deleting the node at position 3, which contains 15, the linked list becomes head --> 10 --> 8 --> 11 --> null.

Code

Java
public ListNode deleteAtPosition(ListNode head, int position) {
  int size = length(head);  // Used the already defined method to find out the size of the list 
  if (position > size || position < 1) {
     System.out.println("Invalid position");
  }

  if (position == 1) {     
    // If position is 1, it means we will apply delete first node logic 
    ListNode temp = head;
    head = head.next;
    temp.next = null;
    return temp;
  }
  else {
    ListNode previous = head;
    int count = 1;
    while (count < position - 1) {
      previous = previous.next;
      count++;
    }

    ListNode current = previous.next;
    previous.next = current.next;
    current.next = null;
   
    return current;
  }
}

This method deleteAtPosition is designed to delete a node at the specified position in the linked list. It first finds the size of the list using the already defined length method and performs a boundary check to ensure that the position is valid. If the position is invalid, it prints an error message. If the position is 1, indicating that the first node needs to be deleted, it applies the logic of deleting the first node. Otherwise, it traverses the list to find the node just before the specified position (previous). It then updates the next reference of previous to skip the node to be deleted and returns the deleted node.

Conclusion

By mastering singly linked lists in Java, you’ll not only enhance your understanding of fundamental data structures but also improve your problem-solving skills and become a more proficient programmer. With the comprehensive knowledge provided in this guide, you’ll be well-equipped to tackle a wide range of programming challenges and develop efficient and elegant solutions.

Linked Lists in Java

Understanding Linked Lists in Java: A Comprehensive Guide & Best Guide for Developers

Linked lists are fundamental data structures in computer science that provide dynamic memory allocation and efficient insertion and deletion operations. In Java, linked lists are commonly used for various applications due to their flexibility and versatility. In this blog post, we will explore linked lists in Java in detail, covering their definition, types, operations, and implementation.

What is a Linked List?

A linked list is a linear data structure consisting of a sequence of elements called nodes. Each node contains two parts: the data, which holds the value of the element, and a reference (or link) to the next node in the sequence. Unlike arrays, which have fixed sizes, linked lists can dynamically grow and shrink as elements are added or removed.

Key Concepts

  • Node: The fundamental building block of a linked list. Each node consists of:
    • Data: The actual information you store (e.g., integer, string).
    • Next pointer: References the next node in the list.
    • Previous pointer (for doubly-linked lists): References the preceding node, enabling bidirectional traversal.
  • Head: The starting point of the list, pointing to the first node.
  • Tail: In singly-linked lists, points to the last node. In doubly-linked lists, points to the last node for forward traversal and the first node for backward traversal.

Types of Linked Lists

1. Singly Linked List

In a singly linked list, each node has only one link, which points to the next node in the sequence. Traversal in a singly linked list can only be done in one direction, typically from the head (start) to the tail (end) of the list. Singly-linked lists are relatively simple and efficient in terms of memory usage.

2. Doubly Linked List

In a doubly linked list, each node has two links: one points to the next node, and the other points to the previous node. This bidirectional linking allows traversal in both forward and backward directions. Doubly linked lists typically require more memory per node due to the additional reference for the previous node.

3. Circular Linked List

In a circular linked list, the last node points back to the first node, forming a circular structure. Circular linked lists can be either singly or doubly linked and are useful in scenarios where continuous looping is required.

How to represent a LinkedList in Java ?

Now we know that, A linked list is a data structure used for storing a collection of elements, objects, or nodes, possessing the following properties:

  1. It consists of a sequence of nodes.
  2. Each node contains data and a reference to the next node.
  3. The first node is called the head node.
  4. The last node contains data and points to null.
Java
| data | next | ===> | data | next | ===> | data | next | ===> null

Implementation Of a Node in a linked list

Generic Type Implementation

Java
public class ListNode<T> {
    private T data;
    private ListNode<T> next;
}

In the generic type implementation, the class ListNode<T> represents a node in a linked list that can hold data of any type. The line public class ListNode<T> declares a generic class ListNode with a placeholder type T, allowing flexibility for storing different data types. The private member variable data of type T holds the actual data stored in the node, while the next member variable is a reference to the next node in the linked list, indicated by ListNode<T> next. This design enables the creation of linked lists capable of storing elements of various types, offering versatility and reusability.

Integer Type Implementation

Java
public class ListNode {
    private int data;
    private ListNode next;
}

In contrast to the generic type, the integer type implementation, represented by the class ListNode, is tailored specifically for storing integer data. The class ListNode does not use generics and defines a private member variable data of type int to hold integer values within each node. Similarly, the next member variable is a reference to the next node in the linked list, indicated by ListNode next. This implementation is more specialized and optimized for scenarios where the linked list exclusively stores integer values, potentially offering improved efficiency due to reduced overhead from generics.

Node Diagram

Java
| data | next |===>

This node diagram depicts the structure of each node in the linked list. Each node consists of two components: data, representing the value stored in the node, and next, a pointer/reference to the next node in the sequence. The notation | data | next |===> illustrates this structure, where data holds the value of the node, and next points to the subsequent node in the linked list. The arrow (===>) signifies the connection between nodes, indicating the direction of traversal from one node to another within the linked list.

Representation of Linked List

Java
head ===> |10| ===> |8| ===> |9| ===> |11| ===> null

The representation of the linked list illustrates a sequence of nodes starting from the head node. Each node contains its respective data value, with arrows (===>) indicating the connections between nodes. The notation head ===> |10| ===> |8| ===> |9| ===> |11| ===> null shows the linked list structure, where head denotes the starting point of the list. The data values (10, 8, 9, 11) are enclosed within nodes, and null signifies the end of the linked list. This representation visually demonstrates the organization and connectivity of nodes within the linked list data structure.

Code Implementation

Java
public class LinkedList
{

  private ListNode head; // head node to hold the list
  
  // It contains a static inner class ListNode
  private static class ListNode
  {
     private int data;
     private ListNode next;

     public ListNode(int data) {
          this.data = data;
          this.next = null;
     }
  }

  public static void main(String[] args)
  {
   
  }
}

The above code snippet outlines the structure of a linked list in Java. The LinkedList class serves as the main container for the linked list, featuring a private member variable head that points to the first node. Within this class, there exists a static inner class named ListNode, which defines the blueprint for individual nodes. Each ListNode comprises an integer data field and a reference to the next node. The constructor of ListNode initializes a node with the given data and sets the next reference to null by default. The main method, though currently empty, signifies the program’s entry point where execution begins. It provides a foundational structure for implementing linked lists, enabling the creation and manipulation of dynamic data structures in Java programs.

Common Operations on Linked Lists

We will see each operation in much detail in the next article, where we will discuss Singly Linked Lists in more detail. Right now, let’s see a brief overview of each operation:

1. Insertion

Insertion in a linked list involves adding a new node at a specified position or at the end of the list. Depending on the type of linked list, insertion can be performed efficiently by updating the references of adjacent nodes.

2. Deletion

Deletion involves removing a node from the linked list. Similar to insertion, deletion operations need to update the references of adjacent nodes to maintain the integrity of the list structure.

3. Traversal

Traversal refers to visiting each node in the linked list sequentially. Traversal is essential for accessing and processing the elements stored in the list.

4. Searching

Searching involves finding a specific element within the linked list. Linear search is commonly used for searching in linked lists, where each node is checked sequentially until the desired element is found.

5. Reversing

Reversing a linked list means changing the direction of pointers to create a new list with elements in the opposite order. Reversing can be done iteratively or recursively and is useful in various algorithms and problem-solving scenarios.

Conclusion

Linked lists are powerful data structures in Java that offer dynamic memory allocation and efficient operations for managing collections of elements. Understanding the types of linked lists, their operations, and their implementation in Java is essential for building robust applications and solving complex problems efficiently. Whether you’re a beginner or an experienced Java developer, mastering linked lists will enhance your programming skills and enable you to tackle a wide range of programming challenges effectively. Happy coding!

Object Creation, Constructors, and Singleton Classes

Unveiling the Secrets of Flawless Object Creation, Mighty Constructors, and the Unrivaled Power of Singleton Classes

Object creation and constructors are fundamental concepts in Java programming. Understanding these concepts is crucial for writing efficient and maintainable code. Additionally, the singleton design pattern, which restricts the instantiation of a class to a single object, plays a vital role in various scenarios. This blog delves into these three key areas, providing a comprehensive guide for Java developers.

Object Creation in Java

In Java, objects are instances of classes, which act as blueprints defining the structure and behavior of the objects they create. The process of creating an object involves allocating memory and initializing its attributes. Let’s explore how object creation is done in Java:

Java
// Sample class definition
public class MyClass {
    // Class variables or attributes
    private int myAttribute;

    // Constructor
    public MyClass(int initialValue) {
        this.myAttribute = initialValue;
    }

    // Methods
    public void doSomething() {
        System.out.println("Doing something with myAttribute: " + myAttribute);
    }
}

// Object creation
MyClass myObject = new MyClass(42);
myObject.doSomething();

In the example above, we define a class MyClass with a private attribute myAttribute, a constructor that initializes this attribute, and a method doSomething that prints the attribute value. The object myObject is then created using the new keyword, invoking the constructor with an initial value of 42.

Total 5 ways we create new objects in java

Moving beyond the basics of how object created, let’s explore the five distinct methods to create new objects in Java.

  1. Using the ‘new’ Keyword: The most common and straightforward method involves the use of the ‘new’ keyword. This keyword, followed by the constructor, allocates memory for a new object.
  2. Utilizing ‘newInstance()’ Method: Another approach is the use of the ‘newInstance()’ method. This method is particularly useful when dealing with classes dynamically, as it allows the creation of objects without explicitly invoking the constructor.
  3. Leveraging Factory Methods: Factory methods offer a design pattern where object creation is delegated to factory classes. This approach enhances flexibility and encapsulation, providing a cleaner way to create objects.
  4. Employing Clone Methods: Java supports the cloning mechanism through the ‘clone()’ method. This method creates a new object with the same attributes as the original, offering an alternative way to generate objects.
  5. Object Creation via Deserialization: Deserialization involves reconstructing an object from its serialized form. By employing deserialization, objects can be created based on the data stored during serialization.

Constructors in Java

Constructors are special methods within a class responsible for initializing the object’s state when it is created. They have the same name as the class and are invoked using the new keyword during object creation. It’s important to note that both the instance block and the constructor serve distinct functions. The instance block is utilized for activities beyond initialization, such as counting the number of created objects.

Rules for writing constructors:

  1. The name of the class and the name of the constructor must be the same.
  2. The concept of return type is not applicable to constructors, including void. If, by mistake, void is used with the class name as a constructor, it won’t generate a compiler error because the compiler treats it as a method.
  3. The only applicable modifiers for constructors are public, private, protected, and default. Other types are not allowed.
  4. Only the compiler will generate a default constructor, not the JVM. If you do not write any constructor, it will be created automatically.

Default Constructor Prototype:

  1. It is always a no-argument constructor.
  2. The access modifier is exactly the same as the class; only consider ‘public’ as applicable, and others are not allowed.
  3. It contains only one line, i.e., ‘super()’. This is a no-argument call to the super constructor, but this rule is applicable only to ‘public’ and ‘default’.
  4. The first line is always ‘this()’ or ‘super()’. If you don’t write anything, the compiler places ‘super()’ in the default constructor.
  5. Within the constructor, we can use ‘super()’ or ‘this()’, but not simultaneously, and they cannot be used outside the constructor.
  6. We can call a constructor directly from another constructor only.

Understanding Programmers’ Code and Compiler-Generated Code for Constructors

Programmers write constructors to define how objects of a class should be instantiated and initialized. However, compilers also have a role in generating default constructors when programmers don’t explicitly provide them. Let’s explore it in detail.

Programmers’ Code for Constructors

Purpose of Constructors: Constructors are special methods within a class that are called when an object is created. They initialize the object’s state and set it up for use. Programmers design constructors to meet specific requirements of their classes.

Syntax and Naming Conventions: Programmers follow certain syntax rules and naming conventions when writing constructors. The constructor’s name must match the class name, and it can take parameters to facilitate customizable initialization.

Java
public class MyClass {
    // Programmers' code for constructor
    public MyClass(int parameter) {
        // Initialization logic here
    }
}

Custom Initialization Logic: Programmers have the flexibility to include custom initialization logic within constructors. This logic can involve setting default values, validating input parameters, or performing any necessary setup for the object.

Overloading Constructors: Programmers can overload constructors by providing multiple versions with different parameter lists. This allows for versatility when creating objects with various configurations.

Java
public class MyClass {
    // Parameterized constructor
    public MyClass(int parameter) {
        // Initialization logic
    }

    // Default constructor (no parameters)
    public MyClass() {
        // Default initialization logic
    }
}

Compiler-Generated Code for Constructors:

Default Constructors: If a programmer doesn’t explicitly provide a constructor, the compiler steps in and generates a default constructor. This default constructor is a no-argument constructor that initializes the object with default values.

Java
public class MyClass {
    // Compiler-generated default constructor
    public MyClass() {
        // Default initialization logic by the compiler
    }
}

Super Constructor Call: In the absence of explicit constructor calls by the programmer, the compiler inserts a call to the superclass constructor (via super()) as the first line of the constructor. This ensures proper initialization of the inherited components.

No-Argument Initialization: Compiler-generated default constructors are often no-argument constructors that perform basic initialization. However, this initialization might not suit the specific needs of the class, which is why programmers often provide their own constructors.

Compiler Warnings: While the compiler-generated default constructor is helpful, it may generate warnings if the class contains fields that are not explicitly initialized. Programmers can suppress these warnings by providing their own constructors with proper initialization.

Understanding super() and this() in Constructors

In the realm of object-oriented programming, the keywords super() and this() play a crucial role when it comes to invoking constructors. These expressions are used to call the constructor of the superclass (super()) or the current class (this()). Let’s explore the nuances of using super() and this() in constructors.

Purpose of super() and this():

  • super(): This keyword is used to invoke the constructor of the superclass. It allows the subclass to utilize the constructor of its superclass, ensuring proper initialization of inherited members.
  • this(): This keyword is employed to call the constructor of the current class. It is useful for scenarios where a class has multiple constructors, and one constructor wants to invoke another to avoid redundant code.

Usage Constraints:

Only in Constructor at First Line: Both super() and this() can be used only within the constructor, and they must appear as the first line of code within that constructor. This restriction ensures that necessary initialization steps are taken before any other logic in the constructor is executed.

Java
public class ExampleClass extends SuperClass {
    // Constructor using super()
    public ExampleClass() {
        super(); // Constructor call to superclass
        // Other initialization logic for the current class
    }

    // Constructor using this()
    public ExampleClass(int parameter) {
        this(); // Constructor call to another constructor in the same class
        // Additional logic based on the parameter
    }
}

Limited to Once in Constructor: Both super() and this() can be used only once in a constructor. This limitation ensures that constructor calls are clear and do not lead to ambiguity or circular dependencies.

Java
public class ExampleClass extends SuperClass {
    // Correct usage
    public ExampleClass() {
        super(); // Can be used once
        this();  // Can be used once
    }

    // Incorrect usage - leads to compilation error
    public ExampleClass(int parameter) {
        super();
        this();  // Compilation error: Constructor call can only be used once
    }
}

Understanding ‘super ‘ and ‘this' Keywords in Java

Same like super() and this(), the keywords super and this are essential for referencing instance members of the superclass and the current class, respectively. Let’s explore the characteristics and usage of super and this:

Purpose of super and this:

  • super: This keyword is used to refer to the instance members (fields or methods) of the superclass. It is particularly useful in scenarios where the subclass has overridden a method, and you want to call the superclass version.
  • this: This keyword is employed to refer to the instance members of the current class. It is beneficial when there is a need to disambiguate between instance variables of the class and parameters passed to a method or a constructor.

Usage Constraints:

Anywhere Except Static Context: Both super and this can be used anywhere within non-static methods, constructors, or instance blocks. However, they cannot be used directly in a static context, such as in a static method or a static block. Attempting to use super or this in a static context will result in a compilation error.

Java
public class ExampleClass {
    int instanceVariable;

    // Non-static method
    public void exampleMethod() {
        int localVar = this.instanceVariable;  // Using 'this' to reference instance variable
        // Additional logic
    }

    // Static method - Compilation error
    public static void staticMethod() {
        int localVar = this.instanceVariable; // CE: Cannot use 'this' in a static context
        // Additional logic
    }
}

Multiple Usages: Both super and this can be used any number of times within methods, constructors, or instance blocks. This flexibility allows developers to reference the appropriate instance members as needed.

Java
public class ExampleClass extends SuperClass {
    int subclassVariable;

    // Method using 'super' and 'this'
    public void exampleMethod() {
        int localVar1 = super.methodInSuperclass();  // Using 'super' to call a method from the superclass
        int localVar2 = this.subclassVariable;   // Using 'this' to reference a subclass instance variable
        // Additional logic
    }
}

Understanding Overloaded Constructors in Java

In Java programming, an overloaded constructor refers to the practice of defining multiple constructors within a class, each with a different set of arguments. This mirrors the concept of method overloading, where automatic promotion occurs. Let’s delve into the characteristics of overloaded constructors and some important considerations:

Overloaded Constructor Concept:

  • Definition: Overloaded constructors are multiple constructors within a class, distinguished by differences in their argument lists. This enables flexibility when creating objects, accommodating various initialization scenarios.
  • Automatic Promotion: Similar to method overloading, automatic promotion of arguments happens in overloaded constructors. Java automatically converts smaller data types to larger ones to match the constructor signature.

Inheritance and Overriding Constraints:

Not Applicable to Constructors: Inheritance and method overriding concepts do not apply to constructors. Each class, including abstract classes, can have constructors. However, interfaces, which consist of static variables, do not contain constructors.

Recursive Constructor Invocation:

Stack Overflow Exception: Unlike method recursion where a stack overflow exception occurs after execution, recursive constructor invocation leads to a compile-time error. It’s crucial to handle recursive constructor calls carefully to prevent code execution issues.

No-Argument Constructor Recommendation:

Avoiding Issues: When writing an argument constructor in a parent class, it is highly recommended to include a no-argument constructor. This is because the child class constructor automatically adds a super() call, which can create problems if a no-argument constructor is not present in the parent class.

Java
public class ParentClass {
    // Argument constructor
    public ParentClass(int parameter) {
        // Constructor logic
    }

    // Recommended no-argument constructor
    public ParentClass() {
        // No-argument constructor logic
    }
}

Exception Handling in Constructors:

Checked Exception Propagation: If a parent class constructor throws a checked exception, the child class constructor must compulsorily throw the same checked exception or its parent exception. This ensures proper exception handling across the class hierarchy.

Java
public class ParentClass {
    // Constructor with checked exception
    public ParentClass() throws SomeCheckedException {
        // Constructor logic
    }
}

public class ChildClass extends ParentClass {
    // Child class constructor must propagate the same or a parent checked exception
    public ChildClass() throws SomeCheckedException {
        super();  // Call to the parent constructor
        // Additional constructor logic
    }
}

Understanding the principles of overloaded constructors in Java is essential for creating flexible and robust class structures. Adhering to best practices, such as including a no-argument constructor and handling exceptions consistently, ensures smooth execution and maintainability of code within the context of constructors.

Understanding Singleton Design Pattern in Java

In Java, the Singleton pattern is a design pattern that ensures a class has only one instance and provides a global point to this instance. It is often employed in scenarios where having a single instance of a class is beneficial, such as in the case of Runtime, BusinessDelegate, or ServiceLocator. Let’s explore the characteristics and advantages of Singleton classes in Java:

Singleton Class Concept:

Single Private Constructor: The key feature of a Singleton class is the presence of a single private constructor. This constructor restricts the instantiation of the class from external sources, ensuring that only one instance can be created.

Java
public class SingletonClass {
    private static final SingletonClass instance = new SingletonClass();

    // Private constructor
    private SingletonClass() {
        // Constructor logic
    }

    // Access method to get the single instance
    public static SingletonClass getInstance() {
        return instance;
    }
}

Singleton Instances:

Usage Scenario:

Java
// Utilizing the Singleton instance across the application
Runtime r1 = Runtime.getRuntime();
Runtime r2 = Runtime.getRuntime();
// ...
Runtime r100000 = Runtime.getRuntime();   // Up to 100,000 or more requests use the same object

Advantages of Singleton Class:

  • Performance Improvement: Singleton classes offer performance benefits by providing a single instance shared among multiple clients. This avoids the overhead of creating and managing multiple instances.
  • Global Access: The Singleton pattern provides a global point of access to the single instance. This ensures that any part of the application can easily access and utilize the shared object.

Singleton Design Pattern: Two Approaches

In Java, the Singleton design pattern ensures that a class has only one instance and provides a global point of access to that instance. Two common approaches involve using one private constructor, one private static variable, and one public factory method. The Runtime class is a notable example implementing this pattern. Let’s explore both approaches:

Approach 1: Eager Initialization

In this approach, the singleton instance is created eagerly during class loading. The private constructor ensures that the class cannot be instantiated from external sources, and the public factory method provides access to the single instance.

Java
public class Test {
Approach 2: Lazy Initialization with Double-Checked Locking
This approach initializes the singleton instance lazily, creating it only when needed. The getTest method checks if the instance is null before creating it. Double-checked locking ensures thread safety in a multithreaded environment.    // Eagerly initialized static variable
    private static Test t = new Test();

    // Private constructor
    private Test() {
        // Constructor logic
    }

    // Public factory method
    public static Test getTest() {
        return t;
    }
}

Approach 2: Lazy Initialization with Double-Checked Locking

This approach initializes the singleton instance lazily, creating it only when needed. The getTest method checks if the instance is null before creating it. Double-checked locking ensures thread safety in a multithreaded environment.

Java
public class Test {
    // Lazily initialized static variable
    private static Test t = null;

    // Private constructor
    private Test() {
        // Constructor logic
    }

    // Public factory method with double-checked locking
    public static Test getTest() {
        if (t == null) {
            synchronized (Test.class) {
                if (t == null) {
                    t = new Test();
                }
            }
        }
        return t;
    }
}

Usage of Singleton Instances:

Instances of the Test class are obtained through the getTest method, ensuring that there is only one instance throughout the application.

Java
// Using Singleton instances
Test instance1 = Test.getTest();
Test instance2 = Test.getTest();

// Both instances refer to the same object
System.out.println(instance1 == instance2);  // Output: true

Restricting Child Class Creation in Java

In Java, final classes inherently prevent inheritance, making it impossible to create child classes. However, if a class is not declared as final, but there is a desire to prevent the creation of child classes, one effective method is to use a private constructor and declare all constructors in the class as private. This approach restricts the instantiation of both the superclass and any potential subclasses. Let’s explore this concept:

Java
public class Parent {
    // Private constructor to prevent external instantiation
    private Parent() {
        // Constructor logic
    }
}

In this scenario, attempting to create a child class that extends Parent would be problematic due to the private constructor:

Java
public class Child extends Parent {
    // Compiler error: Implicit super constructor Parent() is not visible for default constructor.
    public Child() {
        super(); // Attempting to access the private constructor of the superclass
    }
}

Explanation:

  1. Private Constructor in Parent Class: The Parent class has a private constructor, making it inaccessible from outside the class. This means that even if a child class attempts to call super(), it cannot access the private constructor of the parent class.
  2. Child Class Compilation Error: In the Child class, attempting to create a constructor that calls super() results in a compilation error. This is because the private constructor in the Parent class is not visible to the Child class.

Usage of Private Constructor:

The private constructor ensures that instances of the Parent class cannot be created externally. Therefore, it prevents not only the creation of child classes but also the instantiation of the parent class from outside the class itself.

By utilizing a private constructor in a class and declaring all constructors as private, it is possible to restrict the creation of both child classes and instances of the class from external sources. This approach adds an additional layer of control over class instantiation and inheritance in Java.

Conclusion

Understanding object creation, constructors, and the singleton design pattern is essential for writing robust and efficient Java code. These concepts enable you to create objects, initialize them properly, and control their lifecycle. By effectively utilizing these tools, you can enhance the maintainability and performance of your Java applications.

Advanced OOPs Features

Exploring Advanced OOP Concepts: A Deep Dive into Coupling, Cohesion, Object Type Casting, Static and Instance Control Flow

Object-Oriented Programming (OOP) is a powerful way of organizing and structuring code using objects. In advanced OOP, developers often focus on concepts like how closely or loosely objects are connected (coupling), how well elements within an object work together (cohesion), changing the type of an object (object type casting), and controlling the flow of code at both static and dynamic levels (static and instance control flow). Let’s take a closer look at each of these ideas.

Coupling in Advanced OOP

Coupling indicates how tightly two or more components are connected. Tight coupling occurs when components are highly interdependent, meaning changes in one component can significantly impact other components. This tight coupling can lead to several challenges, including:

  • Reduced maintainability: Changes in one component may require corresponding changes in other dependent components, making it difficult to modify the code without causing unintended consequences.
  • Limited reusability: Tightly coupled components are often specific to a particular context and may not be easily reused in other applications.

On the other hand, loose coupling promotes code reusability and maintainability. Loosely coupled components are less interdependent, allowing them to be modified or replaced without affecting other components. This decoupling can be achieved through techniques such as:

  • Abstraction: Using interfaces and abstract classes to define common behaviors and decouple specific implementations.
  • Dependency injection: Injecting dependencies into classes instead of creating them directly, promoting loose coupling and easier testing.

Tight Coupling : The Pitfalls

Tightly coupling occurs when one component relies heavily on another, creating a strong dependency. While this may seem convenient initially, it leads to difficulties in enhancing or modifying code. For instance, consider a scenario where a database connection is hardcoded into multiple classes. If the database schema changes, every class using the database must be modified, making maintenance a nightmare. Let’s explore one more a real-life java example:

Java
// Tightly Coupled Classes
class Order {
    private Payment payment;

    public Order() {
        this.payment = new Payment();
    }

    public void processOrder() {
        // Processing order logic
        payment.chargePayment();
    }
}

class Payment {
    public void chargePayment() {
        // Payment logic
    }
}

In this example, the Order class is tightly coupled to the Payment class. The Order class directly creates an instance of Payment, making it hard to change or extend the payment process without modifying the Order class.

Loose Coupling : The Path to Reusability

Loosely coupling, on the other hand, signifies a lower level of dependency between components. A loosely coupled system is designed to minimize the impact of changes in one module on other modules. This promotes a more modular and flexible codebase, enhancing maintainability and reusability. Loosely coupled systems are considered good programming practice, as they facilitate the creation of robust and adaptable software. An example is a plug-in architecture, where components interact through well-defined interfaces. If a module needs to be replaced or upgraded, it can be done without affecting the entire system.

Consider a web application where payment processing is handled by an external service. If the payment module is loosely coupled, switching to a different payment gateway is seamless and requires minimal code changes.

Let’s modify the previous example to achieve loose coupling:

Java
// Loosely Coupled Classes
class Order {
    private Payment payment;

    public Order(Payment payment) {
        this.payment = payment;
    }

    public void processOrder() {
        // Processing order logic
        payment.chargePayment();
    }
}

class Payment {
    public void chargePayment() {
        // Payment logic
    }
}

Now, the Order class accepts a Payment object through its constructor, making it more flexible. You can easily switch to a different payment method without modifying the Order class, promoting reusability and easier maintenance.

Cohesion

Cohesion measures the degree to which the methods and attributes within a class are related to each other. High cohesion implies that a class focuses on a well-defined responsibility, making it easier to understand and maintain. Conversely, low cohesion indicates that a class contains unrelated methods or attributes, making it difficult to grasp its purpose and potentially introducing bugs.

High cohesion can be achieved by following these principles:

  • Single responsibility principle (SRP): Each class should have a single responsibility, focusing on a specific task or functionality.
  • Meaningful methods and attributes: All methods and attributes within a class should be relevant to the class’s primary purpose.

Low cohesion can manifest in various ways, such as:

  • God classes: Classes that contain a vast amount of unrelated functionality, making them difficult to maintain and understand.
  • Data dumping: Classes that simply store data without any associated processing or behavior.

High Cohesion: The Hallmark of Good Design

High cohesion is achieved when a class or module has well-defined and separate responsibilities. Each class focuses on a specific aspect of functionality, making the codebase more modular and easier to understand. For instance, in a banking application, having separate classes for account management, transaction processing, and reporting demonstrates high cohesion.

Let’s consider a simple example with high cohesion:

Java
// High Cohesion Class
class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public int subtract(int a, int b) {
        return a - b;
    }
}

In this example, the Calculator class has high cohesion as it focuses on a clear responsibility—performing arithmetic operations. Each method has a specific and well-defined purpose, enhancing readability and maintainability.

Low Cohesion: A Recipe for Complexity

Conversely, low cohesion occurs when a module houses unrelated or loosely related functionalities. In a low cohesion system, a single class or module may have a mix of responsibilities that are not clearly aligned. This makes the code harder to comprehend and maintain. Low cohesion is generally discouraged in good programming practices as it undermines the principles of modularity and can lead to increased complexity and difficulty in debugging. If a single class handles user authentication, file I/O, and data validation, it exhibits low cohesion.

Low cohesion occurs when a class handles multiple, unrelated responsibilities. Let’s illustrate this with an example:

Java
// Low Cohesion Class
class Employee {
    private String name;
    private double salary;
    private Date hireDate;

    // Methods handling unrelated responsibilities
    public void calculateSalary() {
        // Salary calculation logic
    }

    public void trackEmployeeAttendance() {
        // Attendance tracking logic
    }
}

In this example, the Employee class has low cohesion as it combines salary calculation and attendance tracking, which are unrelated responsibilities. This can lead to code that is harder to understand and maintain.

Object Type Casting

Object type casting, also known as type conversion, is the process of converting an object of one data type to another. This can be done explicitly or implicitly.

Explicit type casting is done by using a cast operator, such as (String). Implicit type casting is done by the compiler, and it happens automatically when the compiler can determine that an object can be converted to another type.

Understanding Object Type Casting

Object type casting involves converting an object of one data type into another. In OOP, this typically occurs when dealing with inheritance and polymorphism. Object type casting can be broadly classified into two categories: upcasting and downcasting.

Upcasting, also known as widening, refers to casting an object to its superclass or interface. This is a safe operation, as it involves converting an object to a more generic type.

Downcasting, on the other hand, also known as narrowing, involves casting an object to its subclass. This operation is riskier, as it involves converting an object to a more specific type. If the object is not actually an instance of the subclass, a ClassCastException will be thrown.

Object Type Casting Syntax

The syntax for object type casting in Java is as follows:

Java
A b = (C) d;

Here, A is the name of the class or interface, b is the name of the reference variable, C is the class or interface, and d is the reference variable.

It’s important to note that C and d must have some form of inheritance or interface implementation relationship. If not, a compile-time error will occur, indicating “inconvertible types.”

Let’s dive into a practical example to understand this better:

Java
Object o = new String("Amol");

// Attempting to cast Object to StringBuffer
StringBuffer sb = (StringBuffer) o; // Compile Error: inconvertible types

In this example, we create an Object reference (o) and initialize it with a String object. Then, we try to cast it to a StringBuffer. Since String and StringBuffer do not share an inheritance relationship, a compile-time error occurs.

Dealing with ClassCastExceptions

It’s crucial to ensure that the underlying types of the reference variable (d) and the class or interface (C) are compatible; otherwise, a ClassCastException will be thrown at runtime.

Java
Object o = new String("Amol");

// Attempting to cast Object to String
String str = (String) o; // No issues, as the underlying type is String

In this case, the cast is successful because the underlying type of o is indeed String. If you attempt to cast to a type that is not compatible, a ClassCastException will be thrown.

Working Code Example

Here’s a complete working example to illustrate object type casting:

Java
public class ObjectTypeCastingExample {
    public static void main(String[] args) {
        // Creating an Object reference and initializing it with a String object
        Object o = new String("Amol");

        // Casting Object to String
        Object o1 = (String) o;

        // No issues, as the underlying type is String
        System.out.println("Casting successful: " + o1);
    }
}

In this example, an Object reference o is created and assigned a String object. Subsequently, o is cast to a String, and the result is stored in another Object reference o1. The program then confirms the success of the casting operation through a print statement.

Reference Transitions

In object type casting, the essence lies in providing a new reference type for an existing object rather than creating a new object. This process allows for a more flexible handling of objects within a Java program. Let’s delve into a specific example to unravel the intricacies of this concept.

Java
Integer I = new Integer(10);  // line 1
Number n = (Number) I;       // line 2
Object o = (Object) n;       // line 3

In the above code snippet, we start by creating an Integer object I and initializing it with the value 10 (line 1). Following this, we cast I to a Number type, resulting in the line Number n = (Number) I (line 2). Finally, we cast n to an Object, yielding the line Object o = (Object) n (line 3).

When we combine line 1 and line 2, we essentially have:

Java
Number n = new Integer(10);

This is a valid operation in Java since Integer is a subclass of Number. Similarly, if we combine all three lines, we get:

Java
Object o = new Integer(10);

Now, let’s explore the comparisons between these objects:

Java
System.out.println(I == n);  // true
System.out.println(n == o);  // true

Surprisingly, both comparisons yield true. This might seem counterintuitive, but it can be explained by the concept of autoboxing and reference types.

Autoboxing and Reference Types

n Java, autoboxing allows primitive data types to be automatically converted into their corresponding wrapper classes when needed. In the given example, the Integer object I is automatically unboxed to an int when compared with n. Therefore, I == n evaluates to true because both represent the same numerical value.

The comparison n == o also yields true. This is due to the fact that all objects in Java ultimately inherit from the Object class. Hence, regardless of the specific type of the object, if no specific behavior is overridden, the default Object methods will be invoked, leading to a successful comparison.

Type Casting in Multilevel Inheritance

Multilevel inheritance is the process of inheriting from a class that has already inherited from another class.

Suppose we have a multilevel inheritance hierarchy where class C extends class B, and class B extends class A.

Java
class A {
    // Some code for class A
}

class B extends A {
    // Some code for class B
}

class C extends B {
    // Some code for class C
}

Now, let’s look at type casting:

Casting from C to B

Java
C c = new C();   // Creating an object of class C
B b = (B) c;      // Casting C to B, creating a reference of type B pointing to the same C object

Here, b is now a reference of type B pointing to the object of class C. This is valid because class C extends class B.

Casting from C to A through B

Java
C c = new C();   // Creating an object of class C
A a = (A) ((B) c); // Casting C to B, then casting the result to A, creating a reference of type A

This line first casts C to B, creating a reference of type B. Then, it casts that reference to A, creating a reference of type A pointing to the same object of class C. This is possible due to the multilevel inheritance hierarchy (C extends B, and B extends A).

In a multilevel inheritance scenario, you can perform type casting up and down the hierarchy as long as the relationships between the classes allow it. The key is that the classes involved have an “is-a” relationship, which is a fundamental requirement for successful type casting in Java.

Type Casting With Respect To Method Overriding

Type casting and overriding are not directly related concepts. Type casting is used to change the perceived type of an object, while overriding is used to modify the behavior of a method inherited from a parent class. However, they can interact indirectly in certain situations.

Suppose we have a class hierarchy where class P has a method m1(), and class C extends P and has its own method m2().

Java
class P {
    void m1() {
        // Implementation of m1() in class P
    }
}

class C extends P {
    void m2() {
        // Implementation of m2() in class C
    }
}

Now, let’s look at some scenarios involving type casting:

Using Child Reference

Java
C c = new C();
c.m1(); // Can call m1() using a child reference
c.m2(); // Can call m2() using a child reference

This is straightforward. When you have an object of class C, you can directly call both m1() and m2() using the child reference c.

Type Casting for m1():

Java
((P) c).m1(); // Using type casting to call m1() using a parent reference

Here, we are casting the C object to type P and then calling m1(). This works because C is a subtype of P, and using a parent reference, we can call the overridden method m1() in the child class.

Type Casting for m2():

Java
((P) c).m2(); // Using type casting to call m2() using a parent reference

This line would result in a compilation error. Even though C is a subtype of P, the reference type determines which methods can be called. Since the reference is of type P, the compiler only allows calling methods that are defined in class P. Since m2() is specific to class C and not present in class P, a compilation error occurs.

Type casting in Java respects the reference type, and it affects which methods can be invoked. While you can cast an object to a parent type and call overridden methods, you cannot call methods that are specific to the child class unless the reference type supports them.

Type Casting and Static Method

In Java, method resolution is based on the dynamic type of the object, which is the class of the object at runtime. This is called dynamic dispatch. However, for static methods, method resolution is based on the compile-time type of the reference, which is the class that declared the method. This is called static dispatch.

Instance Method Invocation

Java
C c = new C();
c.m1();  // Output: C   //but if m1() is static --> C

In this case, you are creating an instance of class C and invoking the method m1() on it. Since C has a non-static method m1(), it will execute the method from class C.

If m1() were static, it would still execute the method from class C because static methods are not overridden in the same way as instance methods.

Static Method Invocation with Child Reference

Java
((B)c).m1();  // Output: C  // but if m1() is static --> B

Here, you are casting an instance of class C to type B and then calling the method m1(). Again, it will execute the non-static method from class C.

If m1() were static, the output would be from class B. This is because static methods are resolved at compile-time based on the reference type, not the runtime object type.

Static Method Invocation with Nested Type Casting

Java
((A)(B(C))).m1(); --> C  // but if m1() is static --> B

In this scenario, you are casting an instance of class C to type B and then casting it to type A before calling the method m1(). The result is still the non-static method from class C.

If m1() were static, it would output the result based on the reference type B, as static method resolution is based on the reference type at compile-time.

Variable resolution and Type Casting

Variable resolution in Java is based on the reference type of the variable, not the runtime type of the object. This means that when you access a variable through a parent class reference, you will always get the value of the variable from the parent class, even if the object being referenced is an instance of a subclass.

Instance Variable Access

Java
C c = new C();
c.x; // Accesses the x variable from class A, so the value is 777

In this case, you are creating an instance of class C and accessing the variable x. The result is 777, which is the value of x in class A, as the reference type is C, and the variable resolution is based on the reference type at compile-time.

Instance Method Invocation with Type Casting

Java
((B) c).m1(); // Calls m1() from class B, so the output is 888

Here, you are casting an instance of class C to type B and then calling the method m1(). The result is 888, which is the value of x in class B. This is because variable resolution for instance variables is based on the reference type at compile-time.

Instance Method Invocation with Nested Type Casting

Java
((A) ((B) c)).m1(); // Calls m1() from class C, so the output is 999

In this scenario, you are casting an instance of class C to type B and then casting it to type A before calling the method m1(). The result is 999, which is the value of x in class C. This is because variable resolution, similar to method resolution, is based on the reference type at compile-time.

The variable resolution is based on the reference type of the variable, and it is determined at compile-time, not at runtime. Each cast influences the resolution based on the reference type specified in the cast.

Static and Instance Control Flow

In OOP, control flow refers to the order in which statements and instructions are executed. There are two types of control flow: static and instance.

  • Static Control Flow: This refers to the flow of control that is determined at compile-time. Static control flow is associated with static methods and variables, and their behavior is fixed before the program runs.
  • Instance Control Flow: This refers to the flow of control that is determined at runtime. Instance control flow is associated with instance methods and variables, and their behavior can vary depending on the specific instance of the class.

Let’s explore each of them in much detail:

Static Control Flow

Static control flow in Java refers to the order in which static members (variables, blocks, and methods) are initialized and executed when a Java class is loaded. The static control flow process consists of three main steps:

1. Identification of static members from top to bottom

The Java compiler identifies all static members of a class as it parses the class declaration. This involves determining the name, data type, and default value of each static variable, as well as the content of each static block and the signature and body of each static method.

In Java, static members include static variables and static blocks. They are identified from top to bottom in the order they appear in the code. Here’s an example:

Java
class StaticExample {
    static int staticVariable1 = 10; // Static variable declaration (Step 1)

    static {
        System.out.println("Static block 1"); // Static block (Step 2)
    }

    static int staticVariable2 = 20; // Static variable declaration (Step 3)

    static {
        System.out.println("Static block 2"); // Static block (Step 4)
    }

    public static void main(String[] args) {
        System.out.println("Main method"); // Main method (Step 5)
    }
}

2. Execution of static variable assignments and static blocks from top to bottom:

Once all static members have been identified, the compiler executes the assignments to static variables and the code within static blocks. Static variable assignments simply assign the default value to the variable, while static blocks contain statements that are executed in the order they appear in the code. Static blocks are executed at the time of class loading, before any instance of the class is created.

The static variable assignments and static blocks are executed in the order they appear from top to bottom. So, in the example above:

  • Step 1: staticVariable1 is assigned the value 10.
  • Step 2: Static block 1 is executed.
  • Step 3: staticVariable2 is assigned the value 20.
  • Step 4: Static block 2 is executed.

3. Execution of the main method

If the class contains a main method, it is executed after all static members have been initialized and executed. The main method is the entry point for a Java application, and it typically contains the code that defines the application’s behavior.

The main method is the entry point of a Java program. It is executed after all static variable assignments and static blocks have been executed. So, in the example above, after Step 4, the main method will be executed.

Assuming you run this class as a Java program, the output will be:

Java
Static block 1
Static block 2
Main method

The static control flow process ensures that static members are initialized and executed in a predictable order, regardless of how or when an instance of the class is created. This is important for maintaining the consistency and integrity of the class’s state.

Static Block Execution

Static blocks in Java are executed at the time of class loading. This means that the statements within a static block are executed before any instance of the class is created or the main method is called. Static blocks are typically used to perform initialization tasks that are common to all objects of the class.

The execution of static blocks follows a top-down order within a class. This means that the statements in the first static block are executed first, followed by the statements in the second static block, and so on.

Java
class Test {
    static {
        System.out.println("Hello, I can Print");
        System.exit(0);
    }
}

In this code snippet, there is only one static block. When the Test class is loaded, the statements within this static block will be executed first. The output of this code snippet will be:

o/p – Hello I can Print

The System.exit(0) statement causes the program to terminate immediately after printing the output. Without this statement, the main method would not be found, resulting in a NoSuchMethodFoundException.

Now, let’s see the slightly modified code:

Java
class Test {
    static int x = m1();

    public static int m1() {
        System.out.println("Hello, I can Print");
        System.exit(0);
        return 10;
    }
}

In this code snippet, there is no static block, but there is a static variable x that is initialized using the value returned by the m1() method. The m1() method is also a static method.

When the Test class is loaded, the static variable x will be initialized first. This will cause the m1() method to be executed, which will print the following output:

o/p – Hello I can Print

The System.exit(0) statement in the m1() method causes the program to terminate immediately after printing the output.

Static Block Inheritance

Static block execution follows a parent-to-child order in inheritance. This means that the static blocks of a parent class are executed first, followed by the static blocks of its child class.

Let’s consider a scenario where you have a parent class and a child class. I’ll provide examples and explain the identification and execution steps:

Identification of static members from parent to child

When a child class inherits from a parent class, it inherits both instance and static members. However, it’s important to note that static members belong to the class itself, not to instances of the class. Therefore, when accessing static members in a child class, they are identified by the class name, not by creating an instance of the parent class.

Java
class Parent {
    static int staticVar = 10;

    static void staticMethod() {
        System.out.println("Static method in Parent class");
    }
}

class Child extends Parent {
    public static void main(String[] args) {
        // Accessing static variable from the parent class
        System.out.println("Static variable from Parent: " + Parent.staticVar);

        // Accessing static method from the parent class
        Parent.staticMethod();
    }
}

In the example above, the child class Child accesses the static variable and static method of the parent class Parent directly using the class name Parent.

Execution of static variable assignments and static blocks from parent to child

Inheritance also influences the execution of static members, including variable assignments and static blocks, from the parent to the child class. Static variable assignments and static blocks in the parent class are executed before those in the child class.

Java
class Parent {
    static int staticVar = initializeStaticVar();

    static {
        System.out.println("Static block in Parent");
    }

    static int initializeStaticVar() {
        System.out.println("Initializing staticVar in Parent");
        return 20;
    }
}

class Child extends Parent {
    static {
        System.out.println("Static block in Child");
    }

    public static void main(String[] args) {
        // Accessing static variable from the parent class
        System.out.println("Static variable from Parent: " + Parent.staticVar);
    }
}

In this example, the output will be:

Java
Initializing staticVar in Parent
Static block in Parent
Static variable from Parent: 20
Static block in Child

The static variable initialization and static block in the parent class are executed before the corresponding ones in the child class.

Execution of main method of only child class

When executing a Java program, the main method serves as the entry point. If a child class has its own main method, it will be executed when running the program. However, the main method in the parent class won’t be invoked unless explicitly called from the child’s main method.

Java
class Parent {
    public static void main(String[] args) {
        System.out.println("Main method in Parent");
    }
}

class Child extends Parent {
    public static void main(String[] args) {
        System.out.println("Main method in Child");
        
        // Calling the parent's main method explicitly
        Parent.main(args);
    }
}

In this example, if you run the Child class, the output will be:

Java
Main method in Child
Main method in Parent

The child class’s main method is executed, and it explicitly calls the parent class’s main method.

Instance Control Flow

Instance control flow in Java refers to the sequence of steps that are executed when an object of a class is created. It involves initializing instance variables, executing instance blocks, and calling the constructor. Instance control flow is different from static control flow, which is executed only once when the class is loaded into memory.

Let’s delve into the detailed steps of the instance control flow:

Identification of instance members from top to bottom

The first step in the instance control flow is the identification of instance members. These include instance variables and instance blocks, which are components of a class that belong to individual objects rather than the class itself. The order of identification is from top to bottom in the class definition.

Java
public class InstanceControlFlowExample {
    // Instance variable
    int instanceVar1 = 5;

    // Instance block
    {
        System.out.println("Instance block 1, instanceVar1: " + instanceVar1);
    }

    // Another instance variable
    String instanceVar2 = "Hello";

    // Another instance block
    {
        System.out.println("Instance block 2, instanceVar2: " + instanceVar2);
    }

    // Constructor
    public InstanceControlFlowExample() {
        System.out.println("Constructor");
    }

    public static void main(String[] args) {
        // Creating an object triggers instance control flow
        new InstanceControlFlowExample();
    }
}

In this example, instanceVar1 is identified first, followed by the first instance block, then instanceVar2 and the second instance block.

Execution of instance variable assignments and instance blocks from top to bottom

Once the instance members are identified, the next step is the execution of instance variable assignments and instance blocks in the order they were identified.

Java
// ... (previous code)

public class InstanceControlFlowExample {
    // ... (previous code)

    // Another instance variable
    String instanceVar3;

    // Another instance block
    {
        instanceVar3 = "World";
        System.out.println("Instance block 3, instanceVar3: " + instanceVar3);
    }

    // ... (previous code)

    public static void main(String[] args) {
        // Creating an object triggers instance control flow
        new InstanceControlFlowExample();
    }
}

In this modification, a new instance variable instanceVar3 is introduced along with a corresponding instance block that assigns a value to it.

Execution of the constructor

The final step in the instance control flow is the execution of the constructor. The constructor is a special method that is called when an object is created. It is responsible for initializing the object and performing any additional setup.

Java
// ... (previous code)

public class InstanceControlFlowExample {
    // ... (previous code)

    // Another instance variable
    String instanceVar4;

    // Another instance block
    {
        instanceVar4 = "!";
        System.out.println("Instance block 4, instanceVar4: " + instanceVar4);
    }

    // Constructor
    public InstanceControlFlowExample() {
        System.out.println("Constructor executed at 10:00");
    }

    public static void main(String[] args) {
        // Creating an object triggers instance control flow
        new InstanceControlFlowExample();
    }
}

In this final modification, a new instance variable instanceVar4 is introduced along with a corresponding instance block. The constructor now includes a timestamp indicating that it is executed at 10:00.

Avoiding unnecessary object creation

Object creation is a relatively expensive operation in Java. This is because the JVM needs to allocate memory for the object, initialize its instance variables, and set up its internal data structures. Therefore, it is important to avoid unnecessary object creation. One way to do this is to reuse objects whenever possible. For example, you can use a cache to store frequently used objects.

Static control flow Vs. Instance control flow

FeatureStatic control flowInstance control flow
ExecutionExecuted once when the class is loadedExecuted for every object of the class that is created
PurposeInitialize static membersInitialize instance members
ScopeClass-levelObject-level

Instance Control Flow in Parent and Child Classes

In Java, instance control flow plays a crucial role in determining the initialization sequence when an object of a subclass is created. It involves identifying and executing instance members from both the parent and subclass.

Let’s break down the steps involved in the instance control flow in this context:

Identification of Instance Members from Parent to Child

The instance control flow begins with the identification of instance members in both the parent and child classes. The order of identification is from the parent class to the child class.

Java
public class ParentClass {
    // Parent instance variable
    int parentInstanceVar = 10;

    // Parent instance block
    {
        System.out.println("Parent Instance block, parentInstanceVar: " + parentInstanceVar);
    }

    // Parent constructor
    public ParentClass() {
        System.out.println("Parent Constructor");
    }
}

public class ChildClass extends ParentClass {
    // Child instance variable
    String childInstanceVar = "Child";

    // Child instance block
    {
        System.out.println("Child Instance block, childInstanceVar: " + childInstanceVar);
    }

    // Child constructor
    public ChildClass() {
        System.out.println("Child Constructor executed at 43:20");
    }
}

In this example, the parent class ParentClass has an instance variable, instance block, and a constructor. The child class ChildClass extends the parent class and introduces its own instance variable, instance block, and constructor.

Execution of Instance Variable Assignments and Instance Blocks in Parent Class

Once the instance members are identified, the next step is the execution of instance variable assignments and instance blocks in the parent class, in the order they were identified.

Java
// ... (previous code)

public class ParentClass {
    // ... (previous code)

    // Parent instance variable
    int parentInstanceVar2;

    // Parent instance block
    {
        parentInstanceVar2 = 20;
        System.out.println("Parent Instance block 2, parentInstanceVar2: " + parentInstanceVar2);
    }

    // ... (previous code)
}

// ... (previous code)

In this modification, a new instance variable parentInstanceVar2 is introduced along with a corresponding instance block in the parent class.

Execution of Parent Constructor

Following the execution of instance variable assignments and instance blocks in the parent class, the parent constructor is executed.

Java
// ... (previous code)

public class ParentClass {
    // ... (previous code)

    // Parent instance variable
    int parentInstanceVar3;

    // Parent instance block
    {
        parentInstanceVar3 = 30;
        System.out.println("Parent Instance block 3, parentInstanceVar3: " + parentInstanceVar3);
    }

    // Parent constructor
    public ParentClass() {
        System.out.println("Parent Constructor executed");
    }

    // ... (previous code)
}

// ... (previous code)

In this modification, a new instance variable parentInstanceVar3 is introduced along with a corresponding instance block in the parent class. The parent constructor now includes a print statement indicating its execution.

Execution of Instance Variable Assignments and Instance Blocks in Child Class

After the parent class’s instance control flow is completed, the control flow moves to the child class, where instance variable assignments and instance blocks are executed.

Java
// ... (previous code)

public class ChildClass extends ParentClass {
    // ... (previous code)

    // Child instance variable
    String childInstanceVar2;

    // Child instance block
    {
        childInstanceVar2 = "Java";
        System.out.println("Child Instance block 2, childInstanceVar2: " + childInstanceVar2);
    }

    // ... (previous code)
}

// ... (previous code)

In this modification, a new instance variable childInstanceVar2 is introduced along with a corresponding instance block in the child class.

Execution of Child Constructor

The final step in the instance control flow is the execution of the child constructor.

Java
// ... (previous code)

public class ChildClass extends ParentClass {
    // ... (previous code)

    // Child instance variable
    String childInstanceVar3;

    // Child instance block
    {
        childInstanceVar3 = "Programming";
        System.out.println("Child Instance block 3, childInstanceVar3: " + childInstanceVar3);
    }

    // Child constructor
    public ChildClass() {
        System.out.println("Child Constructor executed at 43:20");
    }
}

In this modification, a new instance variable childInstanceVar3 is introduced along with a corresponding instance block in the child class. The child constructor now includes a print statement indicating its execution at 43:20.

The sequence of execution follows the inheritance hierarchy, starting from the parent class and moving down to the child class. The instance control flow ensures that instance members are initialized and blocks are executed in the appropriate order during object creation.

One important point I want to highlight here is:

A non-static variable is inaccessible within a static block unless an object is instantiated. Access to the variable becomes possible only after creating an object. This occurs because, during the execution of static members, the JVM cannot recognize instance members without the creation of an object.

Conclusion

In conclusion, these advanced OOP features, including coupling, cohesion, object type casting, and control flow, play pivotal roles in shaping the structure, flexibility, and maintainability of object-oriented software. A thorough understanding of these concepts empowers developers to create robust and scalable applications.

object-oriented programming in java

Java Mastery: Top 3 Powerful Strategies for Object-Oriented Programming Success

Java, known for its versatility and portability, has been a stalwart in the world of programming for decades. One of its key strengths lies in its support for Object-Oriented Programming (OOP), a paradigm that facilitates modular and organized code. To truly master Java, one must delve deep into the intricacies of OOP. In this blog, we will explore powerful strategies that will elevate your Java OOP skills and set you on the path to programming success.

Understanding Object-Oriented Programming (OOP)

Before diving into Java-specific strategies, it’s crucial to have a solid understanding of OOP fundamentals. Grasp concepts like data hiding, data abstraction, encapsulation, inheritance, and polymorphism. These pillars form the foundation of Java’s OOP paradigm.

Object-Oriented Programming (OOP)

Data Hiding:

Data hiding is an object-oriented programming (OOP) feature where external entities are prevented from directly accessing our data. This means that our internal data should not be exposed directly to the outside. Through the use of encapsulation and access control mechanisms, such as validation, we can restrict access to our own functions, ensuring that only the intended parts of the program can interact with and manipulate the data. This helps enhance the security and integrity of the codebase.

Java
public class Account {
    private int balance;

    public Account() {
        this.balance = 0; // Initial balance is set to zero
    }

    public int getBalance() {
        return balance;
    }

    public void deposit(int amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Deposited: " + amount);
        } else {
            System.out.println("Invalid deposit amount");
        }
    }

    public void withdraw(int amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Withdrawn: " + amount);
        } else {
            System.out.println("Invalid withdrawal amount or insufficient balance");
        }
    }
}

 

In the above example, the concept of data hiding is implemented through the use of private access modifiers for the balance field. Let’s break down how this example adheres to the principle of data hiding:

Private Access Modifier:

Java
private int balance;

 

The balance field is declared as private. This means that it can only be accessed within the Account class itself. Other classes cannot directly access or modify the balance field.

Encapsulation:

The concept of data hiding is closely tied to encapsulation. Encapsulation involves bundling data and methods that operate on that data into a single unit or class. We will explore this further later. In this context, the balance field and the associated methods (getBalance, deposit, withdraw) are integral components of the Account class.

Public Interface:

The class provides a public interface (getBalance, deposit, withdraw) through which other parts of the program can interact with the Account object. Class users don’t need to know the internal details of how the balance is stored or manipulated; they interact with the public methods.

Controlled Access:

By keeping the balance field private, the class can control how it is accessed and modified. The class can enforce rules and validation (like checking for non-negative amounts in deposit and withdrawal) to ensure that the object’s state remains valid.

In short, data hiding in this example is achieved by making the balance field private, encapsulating it within the Account class, and providing a controlled public interface for interacting with the object. This helps maintain a clear separation between the internal implementation details and the external usage of the class.

Data Abstractions

Data Abstraction involves concealing the internal implementation details and emphasizing a set of service offerings. An example of this is an ATM GUI screen. Instead of exposing the intricate workings behind the scenes, the user interacts with a simplified interface that provides specific services. This abstraction allows users to utilize the functionality without needing to understand or interact with the complex internal processes.

Encapsulation

Encapsulation is the binding of data members and methods (behavior) into a single unit, namely a class. It encompasses both data hiding and abstraction. In encapsulation, the internal workings of a class, including its data and methods, are encapsulated or enclosed within the class itself. This means that the implementation details are hidden from external entities, and users interact with the class through a defined interface. The combination of data hiding and abstraction in encapsulation contributes to the organization and security of an object-oriented program.

Java
public class Person {
    private String name;
    private int age;

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

    // Getter for name
    public String getName() {
        return name;
    }

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

    // Getter for age
    public int getAge() {
        return age;
    }

    // Setter for age
    public void setAge(int age) {
        if (age > 0) {
            this.age = age;
        } else {
            System.out.println("Invalid age");
        }
    }
}

 

The above class encapsulates the data (name and age) and the methods that operate on that data. Users of the Person class can access the information through the getters and modify it through the setters, but they don’t have direct access to the internal fields.

Using encapsulation in this way helps to control access to the internal state of the Person object allows for validation and additional logic in the setters, and provides a clean and understandable interface for interacting with Person objects.

Tightly Encapsulated Class

A tightly encapsulated class is a class that enforces strict data hiding by declaring all of its data members (attributes) as private. This means that the data members can only be accessed and modified within the class itself, and not directly from other classes. This helps to protect the integrity of the data and prevent it from being unintentionally or maliciously modified.

Java
// Superclass (Parent class)
class Animal {
    private String species;

    // Constructor
    public Animal(String species) {
        this.species = species;
    }

    // Getter for species
    public String getSpecies() {
        return species;
    }
}

// Subclass (Child class)
class Dog extends Animal {
    private String breed;

    // Constructor
    public Dog(String species, String breed) {
        super(species);
        this.breed = breed;
    }

    // Getter for breed
    public String getBreed() {
        return breed;
    }
}

public class EncapsulationExample {
    public static void main(String[] args) {
        // Creating an instance of Dog
        Dog myDog = new Dog("Canine", "Labrador");

        // Accessing information through getters
        System.out.println("Species: " + myDog.getSpecies());
        System.out.println("Breed: " + myDog.getBreed());
    }
}

 

This example demonstrates a tightly encapsulated class structure where both the superclass (Animal) and the subclass (Dog) have private variables and provide getters to access those variables. This ensures that the internal state of objects is not directly accessible from outside the class hierarchy, promoting information hiding and encapsulation.

Inheritance (IS-A Relationships)

An IS-A relationship, also known as inheritance, is a fundamental concept in object-oriented programming (OOP) that allows a class to inherit the properties and methods of another class. This is achieved using the extends keyword in Java.

The main advantage of using IS-A relationships is code reusability. By inheriting from a parent class, a subclass can automatically acquire all of the parent class’s methods and attributes. This eliminates the need to recode these methods and attributes in the subclass, which can save a significant amount of time and effort.

Additionally, inheritance promotes code modularity and maintainability. By organizing classes into a hierarchical structure, inheritance makes it easier to understand the relationships between classes and to manage changes to the codebase. When a change is made to a parent class, those changes are automatically reflected in all of its subclasses, which helps to ensure that the code remains consistent and up-to-date.

Java
public class P {
    public void m1() {
        System.out.println("m1");
    }
}

public class C extends P {
    public void m2() {
        System.out.println("m2");
    }
}

 

There are two classes: P (parent class) and C (child class).

The child class C extends the parent class P, indicating an IS-A relationship, and it uses the extends keyword for inheritance.

Case 1: Parent class cannot called child methods

Java
P p1 = new P();
p1.m1();   // Calls m1 from class P

p1.m2(); // Results in a compilation error, as m2 is not defined in class P

 

Case 2: Child class called Parent and its own method, if it extends the Parent class.

Java
C c1 = new C();
c1.m1();   // Calls m1 from class P (inherited)
c1.m2();   // Calls m2 from class C

 

Case 3: Parent reference can hold child object but by using this it only calls parent methods, child-specific can’t be called

Java
P p2 = new C();
p2.m1();   // Calls m1 from class P (inherited)

p2.m2(); // Results in a compilation error, as m2 is not defined in class P

 

Case 4: Child class reference can not hold parent class object

Java
C c2 = new P(); // Not possible, results in a compilation error

 

In short, this example demonstrates the basic principles of inheritance, polymorphism, and the limitations on method access based on the type of reference used. The use of extends signifies that C is a subclass of P, inheriting its properties and allowing for code reusability.

Multiple Inheritance

Java doesn’t support multiple inheritance in classes, meaning that a class can extend only one class at a time; extending multiple classes simultaneously is not allowed. This restriction is in place to avoid the ambiguity problems that arise in the case of multiple inheritance.

In multiple inheritance, if a class A extends both B and C, and both B and C have a method with the same name, it creates ambiguity regarding which method should be inherited. To prevent such ambiguity, Java allows only single inheritance for classes.

However, it’s important to note that Java supports multilevel inheritance. For instance, if class A extends class B, and class B extends Object (the default superclass for all Java classes), then it is considered multilevel inheritance, not multiple inheritance.

In the case of interfaces, Java supports multiple inheritance because interfaces provide only method signatures without implementation. Therefore, a class can implement multiple interfaces with the same method name, and the implementing class must provide the method implementations. This avoids the ambiguity problem associated with multiple inheritance in classes.

Cyclic inheritance is not allowed in Java. Cyclic inheritance occurs when a class extends itself or when there is a circular reference, such as class A extends B and class B extends A. Java prohibits such cyclic inheritance to maintain the integrity and clarity of the class hierarchy.

HAS-A relationships

HAS-A relationships, also known as composition or aggregation, represent a type of association between classes where one class contains a reference to another class. This relationship indicates that an object of the containing class “has” or owns an object of the contained class.

Consider a Car class that contains an Engine object. This represents a HAS-A relationship, as the Car “has” an Engine.

Java
class Engine {
  // Engine specific functionality in m1 method
}

class Car {
  Engine e = new Engine();

  void start() {
    e.m1();
  }
}

 

In this case, we say that “Car HAS-A Engine reference.”

Composition vs. Aggregation

Composition and aggregation are two types of HAS-A relationships that differ in the strength of the association between the classes:

Composition: 

Composition signifies a strong association between classes. In composition, one class, known as the container object, contains another class, referred to as the contained object. An example is the relationship between a University (container object) and a Department (contained object). In composition, the existence of the contained object depends on the container object. Without an existing University object, a Department object doesn’t exist.

Java
class University {
  Department department = new Department();
}

class Department {
  // Department-specific functionality
}

 

Here, University a class might contain an Department object. This represents a composition relationship, as they (Department) can not exist without the University.

Aggregation: 

Aggregation represents a weaker association between classes. An example is the relationship between a Department (container object) and Professors (contained object). In aggregation, the existence of the contained object doesn’t entirely depend on the container object. Professors may exist independently of any specific Department.

Java
class Department {
  List<Professor> professors = new ArrayList<>();
}

class Professor {
  // Professor-specific functionality
}

 

Here, Department class might contain a list of Professors. This represents an aggregation relationship, as they (Professors) can exist without the Department.

When to Use HAS-A Relationships

When choosing between IS-A (inheritance) and HAS-A relationships, consider the following guideline: if you need the entire functionality of a class, opt for IS-A relationships. On the other hand, if you only require specific functionality, choose HAS-A relationships.

HAS-A relationships, also known as compositions or aggregations, don’t use specific keywords like “extends” in IS-A relationships. Instead, the “new” keyword is used to create an instance of the contained class. HAS-A relationships are often employed for reusability purposes, allowing classes to be composed or aggregated to enhance flexibility and modularity in the codebase.

HAS-A relationships are a fundamental concept in object-oriented programming that allows you to model complex relationships between objects. Understanding the distinction between composition and aggregation and when to use HAS-A vs. IS-A relationships is crucial for designing effective object-oriented software.

Method Overloading

Before exploring polymorphism, it’s essential to understand method signature and related concepts.

Method Signature

A method signature is a concise representation of a method, encompassing its name and the data types of its parameters. It does not include the method’s return type. The compiler primarily uses the method signature to identify and differentiate methods during method calls.

Here’s an example of a method signature:

Java
public static int m1(int i, float f)

 

This signature indicates a method named m1 that takes two parameters: int i and float f, and returns an int.

Method Overloading

Method overloading refers to the concept of having multiple methods with the same name but different parameter signatures within a class. This allows for methods to perform similar operations with different data types or a different number of arguments.

Consider the following methods:

Java
public void m1(int i) {
  // Method implementation
}

public int m1(float f) {
  // Method implementation
}

 

These two methods are overloaded because they share the same name (m1) but have different parameter signatures.

Method Resolution

Method resolution is the process by which the compiler determines the specific method to be invoked when a method call is encountered. The compiler primarily relies on the method signature to identify the correct method.

In the case of method overloading, the compiler resolves the method call based on the reference types of the arguments provided. This means that the method with the parameter types matching the argument types is chosen for execution.

Compile-Time Polymorphism

Method overloading is also known as compile-time polymorphism, static polymorphism, or early binding polymorphism. This is because the method to be invoked is determined during compilation, based on the method signature and argument types.

Method Overloading Loopholes and Ambiguities

Method overloading is a powerful feature of object-oriented programming that allows multiple methods with the same name to exist within a class, provided they have different parameter types. However, this flexibility can also lead to potential loopholes and ambiguities that can cause unexpected behavior or compiler errors.

Case 1: Implicit Type Promotion

Java employs implicit type promotion, where a value of a smaller data type is automatically converted to a larger data type during method invocation. This can lead to unexpected method calls if the compiler promotes an argument to a type that matches an overloaded method.

For instance, in the below code:

Java
public class Test {
    public void m1(int i) {
        System.out.println("int-arg");
    }

    public void m1(float f) {
        System.out.println("float-arg");
    }

    public static void main(String[] args) {
        Test t1 = new Test();
        t1.m1(10);     // Output: int-arg
        t1.m1(10.5f);   // Output: float-arg
        t1.m1('a');     // Output: int-arg
        t1.m1(10L);     // Output: float-arg
        t1.m1(10.5);  // Compilation Error: cannot find symbol method m1(double) in Test class
    }
}

 

byteshortintlongfloatdouble

charintlongfloatdouble

The provided code calls a specific method if the exact argument types match. However, if an exact match is not found, the arguments are promoted to the next level, and this process continues until all checks are completed.

Calling t1.m1(10l) results in the “float-arg” output because long is automatically promoted to float. However, calling t1.m1(10.5) causes a compiler error because there’s no m1(double) method. This highlights the potential for implicit type promotion to lead to unexpected method calls.

Case 2: Inheritance and Method Resolution

In Java, inheritance plays a role in method resolution. If a class inherits multiple methods with the same name from its parent classes, the compiler determines the method to invoke based on the reference type of the object.

Consider the following example:

Java
public void m1(String s) {
    System.out.println("String-Version");
}

public void m1(Object o) {
    System.out.println("Object-Version");
}

 

If we call these overloaded methods:

Java
public static void main(String[] args) {
        Test t1 = new Test();
        t1.m1("Amol Pawar");            // Output: String-Version
        t1.m1(new Object());      // Output: Object-Version
        t1.m1(null);              // Output: String-Version
}

 

In the case of overloading with String and Object, when a String argument is passed, the method with the String parameter is chosen. However, if null is passed, the compiler chooses the String version because String extends Object.

Case 3: Ambiguity with String and StringBuffer

When passing null to overloaded methods that accept both String and StringBuffer, a compiler error occurs: “reference to m1() is ambiguous”. This is because null can be considered both a String and a StringBuffer, leading to ambiguity in method resolution.

Case 4: Ambiguity with Different Order of Arguments

If two overloaded methods have the same parameter types but in different orders, a compiler error occurs if only one argument is passed. This is because the compiler cannot determine the intended method without both arguments.

For instance, if methods m1(int, float) and m1(float, int) exist, passing only an int or float value will result in a compiler error.

Java
public void m1(int i, float f) { ... }
public void m1(float f, int i) { ... }

 

If we pass only an int or float value, a compilation error occurs because the compiler cannot decide which method to call.

Case 5: Varargs Method Priority

In the case of varargs methods, if a general method and a varargs method are present, the general method gets priority. Varargs has the least priority in method resolution. This is because var-args methods were introduced in Java 1.5, while general methods have been available since Java 1.0.

Case 6: Method Resolution and Runtime Object

Method resolution in method overloading is based on the reference type of the object, not the runtime object. This means that if a subclass object is passed as a reference to its superclass, the method defined in the superclass will be invoked, even if the actual object is a subclass instance.

For example, if Class Monkey extends Animal and m1(Animal) and m1(Monkey) methods exist, passing an Animal reference that holds a Monkey object will invoke the m1(Animal) method.

Method Overriding

Method overriding is a mechanism in object-oriented programming where, if dissatisfied with the implementation of a method in the parent class, a child class provides its own implementation with the same method signature.

In the context of method overriding:

  • The method in the parent class is referred to as the overridden method.
  • The method in the child class providing its own implementation is referred to as the overriding method.
Java
class Parent {
    void marry() {
        System.out.println("Parent's choice");
    }
}

class Child extends Parent {
    @Override
    void marry() {
        System.out.println("Child's choice");
    }
}

 

Java
Parent p = new Parent();
p.marry();  // calls the parent class method

Child c = new Child();
c.marry();  // calls the child class method

Parent pc = new Child();
pc.marry();  // calls the child class method; runtime polymorphism in action

 

In the last example, even though the reference is of type Parent, the JVM checks at runtime whether the actual object is of type Child. If so, it calls the overridden method in the child class.

Method Resolution in Overriding

Method resolution in method overriding always takes place at runtime, and it is handled by the Java Virtual Machine (JVM). The JVM checks if the runtime object has any overriding method. If it does, the overriding method is called; otherwise, the superclass method is invoked.

Here are a few important points to remember:

  • Method resolution in method overriding always takes place at runtime by the JVM.
  • This phenomenon is known as runtime polymorphism, dynamic binding, or late binding.
  • The method called is determined by the actual runtime type of the object rather than the reference type.

This dynamic method resolution allows for flexibility and extensibility in the code, as it enables the use of different implementations of the same method based on the actual type of the object at runtime.

Rules for Method Overriding

Here are the rules and considerations regarding method overriding in Java:

Method Signature

The method name and argument types must be the same in both the parent and child class.

Return Type

  • The return type should be the same in the parent and child classes.
  • Co-variant return types are allowed from Java 1.5 onwards. This means the child method can have the same or a subtype of the return type in the parent method.
  • For example, if the parent method returns an object, the child method can return a more specific type like String or StringBuffer. Similarly, if the parent method returns a type like Number, the child methods can return more specific types like Integer, Float, or Double. This makes Java methods more expressive and versatile.
  • Co-variant return types are not applicable to primitive types.
Java
// Valid co-variant return type
class Parent {
    Object m1() { ... }
}
class Child extends Parent {
    String m1() { ... }
}

 

Private and Final Methods

  • Private methods in the parent class can be used in the child class with exactly the same private method based on requirements. This is valid but is not considered method overriding. Method overriding concept is not applicable to private methods.
  • Final methods cannot be overridden in the child class. A final method has a constant implementation that cannot be changed.

Abstract Methods

Abstract methods in an abstract class must be overridden in the child class. Non-abstract methods in the parent class can also be overridden in the child class, but if overridden, the child class must be declared abstract.

Modifiers

There are no restrictions on abstract, synchronized, strictfp, and native modifiers in method overriding.

Scope of Access Modifiers

  • While overriding, you cannot reduce the scope of access modifiers. You can, however, increase the scope. The order of accessibility is private < default < protected < public.
  • Method overriding is not applicable to private methods. Private methods are only accessible within the class in which they are defined.
  • In public methods, you cannot reduce the scope. However, in protected methods, you can reduce the scope to protected or public. Similarly, in default methods, you can reduce the scope to default, protected, or public.
  • For example, if the parent method is public, the child method can be public or protected but not private.
Java
class Parent {
    // Public method in the parent class
    public void display() {
        System.out.println("Public method in the Parent class");
    }
}

class Child extends Parent {
    // Valid override: Increasing the scope from public to protected
    protected void display() {
        System.out.println("Protected method in the Child class");
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.display(); // Outputs: Protected method in the Child class
    }
}

 

In this example, the display method in the Child class overrides the display method in the Parent class. The access level is increased from public to protected, which is allowed during method overriding.

These rules ensure that method overriding maintains consistency, adheres to the principles of object-oriented programming, and prevents unintended side effects.

Why we can’t reduce scope in method overriding?

The principle of not reducing the scope in method overriding is tied to the concept of substitutability and the Liskov Substitution Principle, which is one of the SOLID principles in object-oriented design.

When you override a method in a subclass, it’s essential to maintain compatibility with the superclass. If a client code is using a reference to the superclass to access an object of the subclass, it should be able to rely on the same level of accessibility for the overridden method. Reducing the scope could potentially break this contract.

Let’s break down the reasons:

  1. Substitutability: Method overriding is a way of providing a specific implementation in a subclass that is substitutable for the implementation in the superclass. Substitutability implies that wherever an object of the superclass is expected, you should be able to use an object of the subclass without altering the correctness of the program.
  2. Client Expectations: Clients (other parts of the code using the class hierarchy) expect a certain level of accessibility for methods. Reducing the scope could lead to unexpected behavior for client code that relies on the superclass interface.
  3. Security and Encapsulation: Allowing a subclass to reduce the scope of a method could potentially violate the encapsulation principle, as it might expose implementation details that were intended to be private.

Consider the following example:

Java
class Parent {
    public void doSomething() {
        // implementation
    }
}

class Child extends Parent {
    // This would break substitutability and client expectations
    // as the method becomes less accessible
    private void doSomething() {
        // overridden implementation
    }
}

If you were able to reduce the scope in the child class, code that expects a Parent reference might not be able to access doSomething, violating the contract expected from a subclass.

In short, not allowing a reduction in scope during method overriding is a design choice to ensure that the principle of substitutability is maintained and client code expectations are not violated.

Additional Rules for Method Overriding

Come back to our discussion and continuing with the few more rules for method overriding in Java:

Checked and Unchecked Exceptions

In the case of checked exceptions, the child class must always throw the same checked exception as thrown by the parent class method or its subclass. However, this rule is not applicable to unchecked exceptions, so there are no restrictions in that case.

Static Methods

A non-static method cannot override a static method, and a static method cannot override a non-static method. Static methods are associated with the class itself, not with individual objects, and their resolution is based on the class name, not the object reference.

Attempting to override a static method with a non-static method or vice versa results in a compiler error because it violates the principle of static methods being bound to classes, not objects.

Method Hiding with Static Methods

  • If a static method is used with the same signature in the child class, it is not considered method overriding; instead, it is method hiding. This is because the static method resolution is based on the class name, not the object reference. In method hiding, the method resolution is always taken care of by the compiler based on the reference type of the parent class.

Example:

Java
class Parent {
    static void method() { ... }
}
class Child extends Parent {
    static void method() { ... } // It's method hiding, not overriding
}

In this case, if we use Parent reference to call the method, the compiler resolves it based on the reference type.

This is different from dynamic method overriding, where the method resolution is determined at runtime based on the actual object type.

Varargs Method Overloading

When a varargs method is used in the parent class, such as m1(int... x), it means you can pass any number of arguments, including no arguments (m1()). If you attempt to use the same varargs method in the child class, it is considered overloading, not overriding. Overloading occurs when you provide a different method in the child class, either with a different number or type of parameters.

Example:

Java
class Parent {
    void m1(int... x) { ... }
}

class Child extends Parent {
    // Overloading, not overriding
    void m1(int x, int y) { ... }
}

 

Overriding Not Applicable to Variables

Method overriding is a concept that applies to methods, not variables. Variables are resolved at compile time based on the reference type, and this remains the same regardless of whether the reference is to a parent class or a child class.

Static and non-static variables behave similarly in this regard. The static or non-static nature of a variable does not affect the concept of method overriding.

Java
class Parent {
    int x = 10;
}

class Child extends Parent {
    int x = 20; // Variable in Child, not overridden
}

 

In this case, if you use Parent reference to access the variable, the compiler resolves it based on the reference type.

Method Overloading Vs Method Overriding

Method OverloadingMethod Overriding
Method overloading occurs when two or more methods in the same class have the same name but different parameters (number, type, or order).Method overriding occurs when a subclass provides a specific implementation for a method that is already defined in its superclass.
Method overloading is determined at compile-time based on the method signature (name and parameter types).Method overriding is determined at runtime based on the actual type of the object.
The return type may or may not be different. Overloading is not concerned with the return type.The return type must be the same or a subtype of the return type in the superclass.
The access modifier can be different for overloaded methods.The overridden method cannot be more restrictive in terms of access; it can be the same or less restrictive.
Overloading can occur in the same class or its subclasses.Overriding occurs in a subclass that inherits from a superclass.

 

Polymorphism

Polymorphism, characterized by a single name representing multiple forms, encompasses method overloading, where the same name is used with different method signatures, and method overriding, where the same name is employed with distinct method implementations in both child and parent classes.

Additionally, the utilization of a parent reference to encapsulate a child object is demonstrated, such as a List reference being able to hold objects of ArrayList, LinkedList, Stack, and Vector. When the runtime object is uncertain, employing a parent reference to accommodate the object is recommended.

Java
List<String> myList = new ArrayList<>();
List<String> anotherList = new LinkedList<>();

Difference between P p = new C() and C c = new C()

  • P p = new C():
    • This uses polymorphism, where a parent reference (P) is used to hold a child object (C). The type of reference (P) determines which methods can be called on the object.
    • Only methods defined in the parent class (P) are accessible through the reference. If there are overridden methods in the child class (C), the overridden implementations are called at runtime.
  • C c = new C():
    • This creates an object of the child class (C) and uses a reference of the same type (C). This allows access to both the methods defined in the child class and those inherited from the parent class.

In short, the difference lies in the type of reference used, affecting the visibility of methods and the level of polymorphism achieved. Using a parent reference (P p = new C()) enhances flexibility and allows for interchangeable objects, while using a child reference (C c = new C()) provides access to all methods defined in both the parent and child classes.

Polymorphism Types

There are two main types of polymorphism:

Static polymorphism (Compile-time polymorphism/Early binding)

Static polymorphism occurs when the compiler determines which method to call based on the method signature, which is the method name and the number and type of its parameters. This type of polymorphism is also known as compile-time polymorphism or early binding because the compiler resolves the method call at compile time.

Examples – Method Overloading and Method Hiding
Dynamic polymorphism (Run-time polymorphism/Late binding)

Dynamic polymorphism occurs when the method to call is determined at runtime based on the dynamic type of the object. This means that the same method call can have different results depending on the actual object that is called upon. This type of polymorphism is also known as run-time polymorphism or late binding because the compiler does not determine the method call until runtime.

Example – Method Overriding

Three Pillars of Object-Oriented Programming (OOP)

The three pillars of object-oriented programming (OOP) are encapsulation, polymorphism, and inheritance. These three concepts form the foundation of OOP and are essential for designing well-structured, maintainable, and scalable software applications.

Encapsulation – Security: Encapsulation involves bundling data and the methods that operate on that data into a single unit, known as a class. It enhances security by restricting access to certain components, allowing for better control and maintenance of the code.

Polymorphism – Flexibility: Polymorphism provides flexibility by allowing objects of different types to be treated as objects of a common type. This can be achieved through method overloading and overriding, enabling code to adapt to various data types and structures.

Inheritance – Reusability: Inheritance allows a new class (subclass or derived class) to inherit attributes and behaviors from an existing class (base class or parent class). This promotes code reuse, as common functionality can be defined in a base class and inherited by multiple derived classes, reducing redundancy and enhancing maintainability.

Conclusion

Java’s Object-Oriented Programming, built upon encapsulation, inheritance, polymorphism, and abstraction, establishes a robust framework for crafting well-organized and efficient code. Proficiency in these principles is indispensable, whether you’re embarking on your coding journey or an experienced developer. This blog has covered essential aspects of Object-Oriented Programming (OOP). Nevertheless, there are pivotal advanced OOP features yet to be explored, and we intend to address them comprehensively in our forthcoming article.

java inner and nested classes

Unlocking Java’s Potential: A Comprehensive Exploration of Inner and Nested Classes for Superior Code Structure

In the world of Java programming, the concept of classes is central to the object-oriented paradigm. But did you know that classes can be nested within other classes? This unique feature is known as inner classes, and it opens up a whole new realm of possibilities in terms of code organization, encapsulation, and design patterns. In this blog post, we’ll delve into the fascinating world of inner classes, exploring their types, use cases, and benefits.

Introduction to Inner Classes

Sometimes, we can put a class inside another class. These are called “inner classes.” They were introduced in Java version 1.1 to fix problems with how events are handled in graphical interfaces. But because inner classes have useful features, programmers began using them in regular coding too.

We use inner classes when one type of object can’t exist without another type. For example, a university has departments. If there’s no university, there are no departments. So, we put the department class inside the university class.

Java
class University {  // Outer class
   class Department {  // Inner class
   }
}

Similarly, a car needs an engine to exist. Since an engine can’t exist on its own without a car, we put the engine class inside the car class.

Java
class Car {  // Outer class
  class Engine {  // Inner class
  }
}

Also, think of a map that has pairs of keys and values. Each pair is called an entry. Since entries depend on maps, we define an entry interface inside the map interface.

Java
interface Map {  // Outer interface
  interface Entry {  // Inner interface
  }
}

Remember, inner classes can’t exist without the outer class. This relationship is not “is-a,” but more like “has-a” (composition or aggregation).

Types of Inner Classes

There are four types of inner classes based on where they’re declared and their behavior:

Normal or Regular Inner Classes:

  • These are named classes declared inside another class without the static modifier.
  • They can access both static and instance members of the outer class.
  • Example:
Java
class Outer {
  class Inner {
  }
}

Method Local Inner Classes:

  • These classes are defined within a method.
  • They have access only to final variables of the method.
  • They are used to implement a specific functionality within a method.
  • Example:
Java
class Outer {
  void someMethod() {
    class Inner {
    }
  }
}

Anonymous Inner Classes:

  • These classes are defined without a name.
  • They are often used for implementing interfaces or extending classes on-the-fly.
  • Example:
Java
class Outer {
  interface MyInterface {
    void doSomething();
  }

  MyInterface obj = new MyInterface() {
    public void doSomething() {
    }
  };
}

Static Nested Classes:

  • These are declared as static inside another class.
  • They can access only static members of the outer class.
  • Example:
Java
class Outer {
  static class Nested {
  }
}

Remember:

  • Normal inner classes can access both static and instance members of the outer class.
  • Method local inner classes are declared inside methods and can only access final variables.
  • Anonymous inner classes are often used for implementing interfaces or extending classes without creating separate files.
  • Static nested classes are like regular classes but are defined within another class and can access only static members of the outer class.

When working with inner classes

Normal or Regular Inner Classes:

  • These are named classes declared within another class without the static keyword.
  • Compiling the below example generates two .class files: Outer.class and Outer$Inner.class.
  • Example:
Java
class Outer {
  class Inner {
  }
}

Running Inner Classes:

  • You can’t directly run an inner class from the command prompt unless it has a main method.
  • Attempting to run java Outer or java Outer$Inner without a main method leads to “NoSuchMethodError:main”.

Main Method Inside Outer Class:

  • By adding a main method in the outer class, you can run it.
  • Now, if we run below code java Outer will produce “Outer class main method”.
  • Example:
Java
class Outer {
  class Inner {
  }
  public static void main(String[] args) {
    System.out.println("Outer class main method");
  }
}

Static Members in Inner Classes:

  • Inner classes can’t include static members, such as main methods.
  • Trying to place a main method inside an inner class results in a compile error: “Inner classes cannot have static declarations”.

In short, normal inner classes are named classes within another class, they can’t have static members, and their ability to be run directly depends on the presence of a main method.

Accessing Inner class code

Case 1: Accessing Inner Class Code from Static Area of Outer Class

Java
class Outer {
  class Inner {
    public void m1() {
      System.out.println("Inner class method");
    }
  }
  
  public static void main(String[] args) {
    Outer o = new Outer();
    Outer.Inner i = o.new Inner();
    i.m1();
    // Alternatively:
    // 1. Outer.Inner i = new Outer().new Inner();
    // 2. new Outer().new Inner().m1();
  }
}

In this code:

  • The Outer class contains an inner class named Inner.
  • Inside the main method, we create an instance of Outer called o.
  • We then create an instance of the inner class using o.new Inner(), and call the m1() method on it.
  • The two alternative ways to create the inner class instance are shown as comments.
  • Running this code will print “Inner class method” to the console.

Case 2: Accessing Inner Class Code from Instance Area of Outer Class

Java
class Outer {
  class Inner {
    public void m1() {
      System.out.println("Inner class method");
    }
  }
  
  public void m2() {
    Inner i = new Inner();
    i.m1();
  }
  
  public static void main(String[] args) {
    Outer o = new Outer();
    o.m2();
  }
}

In this code:

  • The Outer class has an inner class named Inner.
  • The m2() method in the Outer class creates an instance of the inner class (Inner i = new Inner();) and calls the m1() method on it.
  • The main method in the Outer class creates an instance of Outer called o, and then calls the m2() method on it using o.m2().
  • Running this code will also print “Inner class method” to the console, just like the previous example.

Case 3: Accessing Inner Class Code from Outside of Outer Class

Java
class Outer {
  class Inner {
    public void m1() {
      System.out.println("Inner class method");
    }
  }
}

class Test {
  public static void main(String[] args) {
    Outer o = new Outer();
    Outer.Inner i = o.new Inner();
    i.m1();
  }
}

In this code:

  • The Outer class contains an inner class named Inner.
  • The Test class is separate from the Outer class and has its own main method.
  • Inside the main method of the Test class, we create an instance of Outer called o.
  • We then create an instance of the inner class using o.new Inner(), and call the m1() method on it.
  • Running the Test class will also print “Inner class method” to the console.

Case 4: Accessing Inner Class Code

a) From Static Area of Outer Class or Outside of Outer Class:
  • Create an instance of the outer class.
  • Use that instance to create an instance of the inner class.
  • Call methods on the inner class instance.
Java
Outer o = new Outer();
Outer.Inner i = o.new Inner();
i.m1();
b) From Instance Area of Outer Class
  • Directly create an instance of the inner class.
  • Call methods on the inner class instance.
Java
Inner i = new Inner();
i.m1();

Remember, the approach you choose depends on where you are accessing the inner class from and the context in which you want to use it.


Normal inner class / Regular inner class

1. In a normal or regular inner class, you can access both static and non-static members of the outer class directly. This makes it convenient to use and interact with the outer class’s members from within the inner class.

Java
class Outer {
  int x = 10;
  static int y = 20;
  
  class Inner {
    public void m1() {
      System.out.println(x);  // Accessing non-static member of outer class
      System.out.println(y);  // Accessing static member of outer class
    }
  }

  public static void main(String[] args) {
    new Outer().new Inner().m1();
  }
}

In this code:

  • The Outer class has an instance variable x and a static variable y.
  • The Inner class within Outer can directly access both x and y from the outer class.
  • Inside the m1() method of Inner, the non-static member x and the static member y are both printed.
  • When you run the main method, the output will be:
Java
10
20

This demonstrates how a normal inner class can freely access both static and non-static members of its enclosing outer class.

2. In an inner class, the keyword this refers to the current instance of the inner class itself. If you want to refer to the instance of the outer class, you can use the syntax OuterClassName.this. This is particularly useful when there might be naming conflicts or when you explicitly want to access the outer class’s instance.

Java
class Outer {
  int x = 10;

  class Inner {
    int x = 100;

    public void m1() {
      int x = 1000;
      System.out.println(x);           // 1000
      System.out.println(this.x);      // 100 (Inner class's x)
      System.out.println(Outer.this.x); // 10 (Outer class's x)
    }
  }

  public static void main(String[] args) {
    new Outer().new Inner().m1();
  }
}

In this code:

  • The Outer class contains an instance variable x with a value of 10.
  • Inside the Outer class, there’s an Inner class with its own instance variable x set to 100.
  • The m1() method inside the Inner class has a local variable x set to 1000.
  • Printing x will show the value of the local variable (1000).
  • Printing this.x inside the Inner class refers to the x within the Inner class (100).
  • Printing Outer.this.x refers to the x within the Outer class (10).
  • When you run the main method, the output will be:
Java
1000
100
10

This code demonstrates the different levels of scope and how you can use this and OuterClassName.this to access variables from various contexts within an inner class.

Applicable access modifiers for both outer and inner classes in Java

For outer classes:

  • The access modifiers that can be applied are public, default (no modifier), final, abstract, and strictfp.

For inner classes:

  • The access modifiers that can be applied are: private, protected, and static.

Nesting of inner classes

Nesting of inner classes is possible, which means you can define one inner class inside another inner class. This creates a hierarchical structure of classes within classes. This is also known as nested inner classes.

Here’s an example to illustrate this concept:

Java
class A {
  class B {
    class C {
      public void m1() {
        System.out.println("Innermost class method");
      }
    }
  }
}

class Test {
  public static void main(String[] args) {
    A a = new A();
    A.B b = a.new B();
    A.B.C c = b.new C();
    c.m1();
  }
}

In this example:

  • Class A has an inner class B, which itself has an inner class C.
  • The m1() method in class C prints “Innermost class method”.
  • In the main method of the Test class, you create instances step by step: A -> A.B -> A.B.C.
  • Then you call the m1() method on the instance of class C.
  • Running this code will print “Innermost class method” to the console.

The above code effectively demonstrates the concept of nested inner classes and how to work with them.

Let’s see one more example:

Java
class Outer {
  int outerVar = 10;

  class Inner {
    int innerVar = 20;

    class NestedInner {
      int nestedVar = 30;

      public void display() {
        System.out.println("NestedVar: " + nestedVar);
        System.out.println("InnerVar: " + innerVar);
        System.out.println("OuterVar: " + outerVar);
      }
    }
  }

  public static void main(String[] args) {
    Outer outer = new Outer();
    Outer.Inner inner = outer.new Inner();
    Outer.Inner.NestedInner nestedInner = inner.new NestedInner();

    nestedInner.display();
  }
}

In this example, we have an Outer class with an Inner class inside it, and within the Inner class, there’s a NestedInner class. You can create instances of each class and access their members accordingly.

When you run the code, it will display:

Java
NestedVar: 30
InnerVar: 20
OuterVar: 10

This shows that nesting of inner classes allows you to organize your code in a structured manner and access members at different levels of nesting.

Method Local Inner Classes

Method local inner classes are inner classes that are defined within a method’s scope. They are only accessible within that specific method and provide a way to encapsulate functionality that is needed only within that method. This type of inner class is particularly useful when you want to confine a class’s scope to a specific method, keeping the code organized and localized.

Main Purpose of Method Local Inner Classes:

  • Method local inner classes are intended to define functionality that is specific to a particular method.
  • They encapsulate code that is required repeatedly within that method.
  • Method local inner classes are well-suited for addressing nested, localized requirements within a method’s scope.
  • Method local inner classes can only be accessed within the method where they are defined.
  • They have a limited scope and aren’t accessible outside of that method.
  • Method local inner classes are the least commonly used type of inner classes.
  • They are employed when specific circumstances demand a highly localized class definition.

Example:

Java
class Test {
  public void m1() {
    class Inner {
      public void sum(int x, int y) {
        System.out.println("The Sum: " + (x + y));
      }
    }

    Inner i = new Inner();
    i.sum(10, 20);
    // More code...
    i.sum(100, 200);
    // More code...
    i.sum(1000, 2000);
  }

  public static void main(String[] args) {
    Test t = new Test();
    t.m1();
  }
}

In this example:

  • The Test class has a method m1() that contains a method local inner class named Inner.
  • The Inner class has a method sum() that calculates and prints the sum of two numbers.
  • Within m1(), you create an instance of the Inner class and call its sum() method multiple times.
  • Running the code produces the following output:
Java
The Sum: 30
The Sum: 300
The Sum: 3000

The above code effectively demonstrates how method local inner classes can be used to encapsulate functionality within a specific method’s scope.

We can declare a method-local inner class inside both instance and static methods.

  • If we declare an inner class inside an instance method, we can access both static and non-static members of the outer class directly from that method-local inner class.
  • On the other hand, If we declare an inner class inside a static method, we can access only static members of the outer class directly from that method-local inner class.

Example

Java
class Test {
  int x = 10;
  static int y = 20;

  public void m1() {
    class Inner {
      public void m2() {
        System.out.println(x); // Accessing instance member of outer class
        System.out.println(y); // Accessing static member of outer class
      }
    }

    Inner i = new Inner();
    i.m2();
  }

  public static void main(String[] args) {
    Test t = new Test();
    t.m1();
  }
}

Now, when we run this code, the output will be:

Java
10
20

This demonstrates that method local inner classes can access both instance and static members of the outer class within the context of an instance method.

Now, If we declare the m1() method as static, you will indeed get a compilation error at line 1 where you’re trying to access the non-static variable x from a static context. Here’s how the code would look with the error:

Java
class Test {
  int x = 10;
  static int y = 20;

  public static void m1() {
    class Inner {
      public void m2() {
        System.out.println(x); // Compilation error: non-static variable x cannot be referenced from a static context
        System.out.println(y);
      }
    }

    Inner i = new Inner();
    i.m2();
  }

  public static void main(String[] args) {
    Test.m1();
  }
}

In this version of the code, since m1() is declared as static, it can’t access instance variables like x directly. The compilation error mentioned in a comment will occur at the line where you’re trying to access x from the method local inner class’s m2() method. The y variable, being static, can still be accessed without an issue.

We will now look at a Very Important Concept in Inner Classes.

From a method-local inner class, we can’t access local variables of the method in which we declare the inner class. However, if the local variable is declared as final, then we can access it.

Java
class Test {
  public void m1() {
    final int x = 10; // Declaring a final local variable 'x' with a value of 10
    class Inner {
      public void m2() {
        System.out.println(x); // Accessing the final local variable 'x' within the inner class
      }
    }
    Inner i = new Inner(); // Creating an instance of the inner class
    i.m2(); // Calling the method of the inner class to print the value of 'x'
  }
   
  public static void main(String[] args) {
    Test t = new Test(); // Creating an instance of the outer class
    t.m1(); // Calling the method of the outer class
  }
}

Explanation:

  1. In the m1() method of the Test class, a local variable x is declared and initialized with the value 10. The variable x is marked as final, indicating that its value cannot be changed after initialization.
  2. Inside the m1() method, an inner class named Inner is defined. This inner class contains a method m2().
  3. The m2() method of the Inner class prints the value of the final local variable x. Since x is declared as final, it can be accessed within the inner class.
  4. Back in the m1() method, an instance of the Inner class is created using Inner i = new Inner();.
  5. The m2() method of the inner class is called using the instance i, which prints the value of the final local variable x.
  6. In the main method, an instance of the Test class is created (Test t = new Test();).
  7. The m1() method of the outer class is called using the instance t, which triggers the creation of an instance of the inner class and the printing of the value of the final local variable x.

Output: When you run the code, the output will be

Java
10

This output confirms that the inner class is able to access the final local variable x.

Now Few Questions: Consider the following code

Java
class Test {
    int i = 10;
    static int j = 20;

    public void m1() {
        int k = 30;
        final int m = 40;

        class Inner {
            public void m2() {
                // Line 1
            }
        }
    }
}

a) At Line 1, which of the following variables can we access directly? i, j, k, m

Answer → We can access all variables except ‘k’ directly.

b) If we declare m1() as static, then at Line 1, which variables can we access directly? i, j, k, m

Answer –> We can access only ‘j’ and ‘m’.

c) If we declare m2() as static, then at Line 1, which variables can we access directly? i, j, k, m

Answer –> We will get a compilation error (CE) because we cannot declare static members inside inner classes.

Note → The only applicable modifiers for method-local inner classes are final, abstract, and strictfp. If we try to apply any other modifier, we will get a compilation error (CE).


Anonymous Inner Class

Sometimes, inner classes can be declared without a name. Such inner classes are called ‘anonymous inner classes.’ The main purpose of anonymous inner classes is for instant use, typically for one-time usage.

Anonymous Inner Classes:

  • Anonymous inner classes are inner classes declared without a name.
  • They are primarily used for instant (one-time) usage.
  • Anonymous inner classes can be categorized into three types based on their declaration and behavior.

Types of Anonymous Inner Classes

Based on their declaration and behavior, there are three types of anonymous inner classes:

1. Anonymous Inner Class that Extends a Class

  • An anonymous inner class can extend an existing class.
  • It provides an implementation for the methods of the superclass or overrides them.

2. Anonymous Inner Class that Implements an Interface

  • An anonymous inner class can implement an interface.
  • It provides implementations for the methods declared in the interface.

3. Anonymous Inner Class Defined Inside Arguments

  • An anonymous inner class can be defined as an argument to a method.
  • It’s often used for callbacks or event handling.

Anonymous Inner class that extends a class

Java
class PopCorn {
    public void taste() {
        System.out.println("salty");
    }
}

class Test {
    public static void main(String[] args) {
        PopCorn p = new PopCorn() {
            public void taste() {
                System.out.println("spicy");
            }
        };
        p.taste();    // spicy

        PopCorn p1 = new PopCorn();
        p1.taste();    // salty

        PopCorn p2  = new PopCorn() {
            public void taste() {
                System.out.println("sweet");
            }
        };
        p2.taste(); // sweet
        System.out.println(p.getClass().getName());  // Test$1
        System.out.println(p1.getClass().getName()); // PopCorn
        System.out.println(p2.getClass().getName()); // Test$2
    }
}

The generated .class files are:

  • PopCorn.class: Compiled class file for the PopCorn class.
  • Test.class: Compiled class file for the Test class.
  • Test$1.class: Compiled class file for the first anonymous inner class within the Test class.
  • Test$2.class: Compiled class file for the second anonymous inner class within the Test class.

This is a common naming convention used by Java to generate class files for inner classes.

Analysis:

1. PopCorn p = new PopCorn();

  • In this line, you are creating an instance of the PopCorn class using its constructor.

2. PopCorn p = new PopCorn() { }

  • In this case, you are declaring an anonymous inner class that extends PopCorn (anonymously).
  • You’re creating an object of the anonymous inner class using the PopCorn reference p.

3. PopCorn p = new PopCorn() { public void taste() { … } };

Java
PopCorn p = new PopCorn() {
    public void taste() {
        System.out.println("Spicy");
    }
};
  • You’re declaring an anonymous inner class that extends PopCorn.
  • You’re overriding the taste() method within this anonymous inner class.
  • You’re creating an object of this anonymous inner class using the PopCorn reference p.

Different approaches to working with threads

Normal Class Approach:

Java
// Example using a normal class that extends Thread
class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Child Thread");
        }
    }
}

class ThreadDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

Anonymous Inner Class Approach:

Java
// Example using an anonymous inner class extending Thread
class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new Thread() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("Child Thread");
                }
            }
        };
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

Anonymous Inner class that implements an Interface

Normal Class Approach:

Java
class MyRunnable implements Runnable {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Child Thread");
        }
    }
}

class ThreadDemo {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t = new Thread(r); // where r is target runnable
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

Anonymous Inner Class Implementing an Interface:

Note -> Defining a thread by implementing a runnable interface

Java
// Example using an anonymous inner class implementing Runnable interface
class ThreadDemo {
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("Child Thread");
                }
            }
        };

        Thread t = new Thread(r);
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

Anonymous Inner class that defines inside arguments

Java
// Example using an anonymous inner class inside arguments
class ThreadDemo {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("Child Thread");
                }
            }
        }).start();

        for (int i = 0; i < 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

All the above code examples effectively illustrate the different ways to work with threads using both normal classes and anonymous inner classes


Normal Java Class Vs Anonymous Inner Class

The differences between normal Java classes and anonymous inner classes when it comes to extending classes, implementing interfaces, and defining constructors.

Extending a Class:

  • A normal Java class can extend only one class at a time.
  • An anonymous inner class can also extend only one class at a time.

Implementing Interfaces:

  • A normal Java class can implement any number of interfaces simultaneously.
  • An anonymous inner class can implement only one interface at a time.

Combining Extension and Interface Implementation:

  • A normal Java class can extend a class and implement any number of interfaces simultaneously.
  • An anonymous inner class can either extend a class or implement an interface, but not both simultaneously.

Constructors:

  • A normal Java class can have multiple constructors.
  • Anonymous inner classes cannot have explicitly defined constructors, primarily because they don’t have a specific name. The name of the class and the constructor must match, which is not feasible for anonymous classes.

Note: If the requirement is standard and required several times, then we should go for a normal top-level class. If the requirement is temporary and required only once (for instant use), then we should go for an anonymous inner class.

Where exactly Anonymous inner classes are used?

We can use anonymous inner classes frequently in GUI-based applications to implement event handling.

Anonymous inner classes are often used in GUI-based applications to implement event handling. Event handling in GUI applications involves responding to user interactions such as button clicks, mouse movements, and keyboard inputs. Anonymous inner classes provide a concise way to define event listeners and handlers directly inline within the code, making the code more readable and reducing the need for separate classes for each event.

Java
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class MyGUIFrame extends JFrame {
    private JButton b1, b2, b3;

    public MyGUIFrame() {
        // Initialize components
        b1 = new JButton("Button 1");
        b2 = new JButton("Button 2");
        b3 = new JButton("Button 3");

        // Add buttons to the frame
        add(b1);
        add(b2);
        add(b3);

        // Attach anonymous action listeners to buttons
        b1.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                // Button 1 specific functionality
                JOptionPane.showMessageDialog(MyGUIFrame.this, "Button 1 clicked!");
            }
        });

        b2.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                // Button 2 specific functionality
                JOptionPane.showMessageDialog(MyGUIFrame.this, "Button 2 clicked!");
            }
        });

        b3.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                // Button 3 specific functionality
                JOptionPane.showMessageDialog(MyGUIFrame.this, "Button 3 clicked!");
            }
        });

        // Set layout and size
        setLayout(new FlowLayout());
        setSize(300, 150);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new MyGUIFrame());
    }
}

In this example, an ActionListener is implemented as an anonymous inner class for each button (b1, b2, b3) to handle their click events. The JOptionPane is used to show a message dialog when each button is clicked. The SwingUtilities.invokeLater() is used to ensure the GUI is created on the Event Dispatch Thread.

Remember to import the necessary classes (JFrame, JButton, ActionEvent, ActionListener, JOptionPane, SwingUtilities, etc.) from the appropriate packages.


Static Nested Classes

Sometimes, we can declare an inner class with the static modifier. Such types of inner classes are called static nested classes.

In the case of a normal or regular inner class, without an existing outer class object, there is no chance of an existing inner class object. That is, the inner class object is strongly associated with the outer class object.

However, in the case of static nested classes, without an existing outer class object, there may be a chance of an existing nested class object. Hence, a static nested class object is not strongly associated with the outer class object.

Java
class Outer 
{
   static class Nested 
   {
     public void m1()
     {
       System.out.println("Static nested class method");
     }
   }

   public static void main(String[] args)
   {
     Nested n = new Nested();
     n.m1();
   }
}

If you want to create a nested class object from outside of the outer class, you can do so as follows:

Java
Outer.Nested n = new Outer.Nested();

Static Method Declaration

In normal or regular inner classes, we can’t declare static members. However, in static nested classes, we can declare static members, including the main method. This allows us to invoke a static nested class directly from the command prompt.

Java
class Test
{
  static class Nested
  {
    public static void main(String[] args)
    {
      System.out.println("Static nested class main method");
    }
  }
 
  public static void main(String[] args)
  {
    System.out.println("Outer class main method");
  }
}

Explanation:

  1. The main method of the outer class (Test) will be invoked when you execute java Test.
  2. The main method of the static nested class (Nested) will be invoked when you execute java Test$Nested because the nested class is essentially a separate class named Test$Nested.

Output:

  • Running java Test will output: Outer class main method
  • Running java Test$Nested will output: Static nested class main method

Accessing static and non-static members from outer classes

In normal or regular inner classes, we can directly access both static and non-static members of the outer class. However, in static nested classes, we can only directly access the static members of the outer class and cannot access non-static members.

Java
class Test
{
  int x = 10;
  static int y = 20;
  static class Nested 
  {
    public void m1()
    {
      // Compilation Error: non-static variable x cannot be referenced from a static context 
      // System.out.println(x);
      System.out.println(y); // valid
    }
  }
}

Explanation:

  • You cannot directly access the non-static variable x from the static method m1() in the static nested class because the static nested class and its methods are associated with the class itself, not an instance of the outer class.
  • However, you can access the static variable y since static members are associated with the class itself and can be accessed from both static and non-static contexts.

Differences between normal or regular inner class and static nested class

There are several significant differences between normal or regular inner classes and static nested classes. These differences revolve around aspects such as their association with the outer class, member accessibility, and more.

Normal or Regular Inner Class:

  1. Without an existing outer class object, there is no chance of an existing inner class object. In other words, the inner class object is strongly associated with the outer class object.
  2. In normal or regular inner classes, we can’t declare static members.
  3. Normal or regular inner classes cannot declare a main method, thus we cannot directly invoke the inner class from the command prompt.
  4. From normal or regular inner classes, we can directly access both static and non-static members of the outer class.

Static Nested Classes:

  1. Without an existing outer class object, there may be a chance of an existing static nested class object. However, the static nested class object is not strongly associated with the outer class object.
  2. In static nested classes, we can declare static members.
  3. In static nested classes, we can declare a main method, allowing us to invoke the nested class directly from the command prompt.
  4. From static nested classes, we can access only the static members of the outer class.

Various combinations of nested classes and interfaces

Case 1: Class Inside a Class

When there is no possibility of one type of object existing without another type of object, we can declare a class inside another class. For instance, consider a university that consists of several departments. Without the existence of a university, the concept of a department cannot exist. Therefore, it’s appropriate to declare the ‘Department’ class within the ‘University’ class.

Java
class University
{
   class Department
   {
    
   }
}

Case 2: Interface Inside a Class

When there is a need for multiple implementations of an interface within a class, and all these implementations are closely related to that particular class, defining an interface inside the class becomes advantageous. This approach helps encapsulate the interface implementations within the context of the class.

Java
class VehicleTypes
{
  interface Vehicle
  {
    int getNoOfWheels();
  }
 
  class Bus implements Vehicle
  {
    public int getNoOfWheels()
    {
      return 6;
    }
  }

  class Auto implements Vehicle
  {
    public int getNoOfWheels()
    {
      return 3;
    }
  }
 
  // Other classes and implementations can follow...
}

Case 3: Interface Inside an Interface

We can declare an interface inside another interface. For instance, consider a ‘Map’ which is a collection of key-value pairs. Each key-value pair is referred to as an ‘Entry.’ Since the existence of an ‘Entry’ object is reliant on the presence of a ‘Map’ object, it’s logical to define the ‘Entry’ interface inside the ‘Map’ interface. This approach helps encapsulate the relationship between the two interfaces.

Java
interface Map
{
   interface Entry
   {
     // Define methods and members for the Entry interface
     // ...
     // ...
     // ...
   }
}

Any interface declared inside another interface is always implicitly public and static, regardless of whether we explicitly declare them as such. This means that we can directly implement an inner interface without necessarily implementing the outer interface. Similarly, when implementing the outer interface, there’s no requirement to also implement the inner interface. In essence, outer and inner interfaces can be implemented independently.

Java
interface Outer 
{
  void m1();
  interface Inner 
  {
    void m2();
  }
}

class Test1 implements Outer 
{
  public void m1()
  {
    System.out.println("Outer interface method implementation");
  }
}

class Test2 implements Outer.Inner
{
  public void m2()
  {
    System.out.println("Inner interface method implementation");
  }
}

public class Test
{
  public static void main(String[] args)
  {
    Outer t1 = new Test1();
    t1.m1();

    Outer.Inner t2 = new Test2();
    t2.m2();
  }
}

Case 4: Class Inside an Interface

When a particular functionality of a class is closely associated with an interface, it is highly recommended to declare that class inside the interface. This approach helps maintain a strong relationship between the class and the interface, emphasizing the specialized functionality encapsulated within the interface.

Java
interface EmailService
{
  void sendMail(EmailDetails e);
 
  class EmailDetails
  {
    String to_list;
    String cc_list;
    String subject;
    String body;
  }
}

In the given example, the EmailDetails class is specifically required only for the EmailService interface and is not used elsewhere. Thus, it’s recommended to declare the EmailDetails class inside the EmailService interface. This approach ensures that the class is tightly associated with the interface it serves.

Furthermore, class declarations inside interfaces can also be used to provide default implementations for methods defined in the interface, contributing to the interface’s flexibility and usability.

Java
interface Vehicle
{
  int getNoOfWheels();
  
  class DefaultVehicle implements Vehicle
  {
    public int getNoOfWheels()
    {
      return 2;
    }
  }
}

class Bus implements Vehicle
{
  public int getNoOfWheels()
  {
    return 6;
  }
}

public class Test
{
  public static void main(String[] args)
  {
    Vehicle.DefaultVehicle d = new Vehicle.DefaultVehicle();
    System.out.println(d.getNoOfWheels());  // 2
   
    Bus b = new Bus();
    System.out.println(b.getNoOfWheels());  // 6
  }
}

In the above example, the DefaultVehicle class serves as the default implementation of the Vehicle interface, while the Bus class provides a customized implementation of the same interface.

It’s worth noting that a class declared inside an interface is always implicitly public and static, regardless of whether we explicitly declare them as such. As a result, it’s possible to create an instance of the inner class directly without needing an instance of the outer interface.

Conclusions

1. In Java, both classes and interfaces can be declared inside each other, allowing for a flexible and versatile approach to structuring and organizing code.

Declaring a class inside a class:

Java
class A
{
  class B
  {
  } 
}

Declaring an interface inside a class:

Java
class A
{
  interface B
  {
  }
}

Declaring an interface inside an interface:

Java
interface A
{
  interface B
  {
  }
}

Declaring a class inside an interface:

Java
interface A
{
  class B
  {
  }
}

2. The interface declared inside an interface is always implicitly public and static, regardless of whether we explicitly declare them as such.

Java
interface A {
    interface B {
        // You can add methods and other members here
    }
}

3. The class which is declared inside an interface is always public and static, whether we explicitly declare it as such or not.

Java
interface A {
    class B {
        // You can add fields, methods, and other members here
    }
}

4. The interface declared inside a class is always implicitly static, but it doesn’t need to be declared as public.

Java
class A {
    interface B {
        // You can add methods and other members here
    }
}

Conclusion

Inner classes are a powerful and versatile feature in Java, enabling you to create complex relationships and encapsulate functionality with elegance. Whether you’re organizing code, implementing event handling, or providing default implementations, inner classes offer a rich toolkit to tackle a variety of scenarios. By understanding the types of inner classes and their benefits, you can wield this feature to enhance code readability, maintainability, and design patterns.

Java StringBuilder

Java String, StringBuffer, and StringBuilder: Features and Differences

In Java, Strings are widely used for storing and manipulating textual data. However, if the content of the string is not fixed and needs to be changed frequently, using the String class is not recommended as it creates a new object every time a change is made, causing performance issues. In such cases, it is better to use the StringBuffer class.

StringBuffer

StringBuffer is a mutable sequence of characters that provides various methods to modify its content. The main advantage of StringBuffer over String is that all the required changes are performed on the existing object, avoiding the creation of new objects for each change. This improves performance and reduces memory consumption.

There are three constructors available for creating StringBuffer objects. The first constructor creates an empty StringBuffer object with a default capacity of 16. When the StringBuffer reaches its maximum capacity, a new StringBuffer object is created with a new capacity of (currentCapacity + 1) * 2. The second constructor creates an empty StringBuffer object with the specified initial capacity. The third constructor creates a StringBuffer object for a given String with a capacity of string.length() + 16.

StringBuffer provides several methods for manipulating its content. Some of the important methods are:

  • length(): returns the length of the StringBuffer.
  • capacity(): returns the total number of characters that the StringBuffer can accommodate.
  • charAt(int index): returns the character at the specified index.
  • setCharAt(int index, char ch): replaces the character at the specified index with the provided character.
  • append(): appends the specified argument to the end of the StringBuffer. This method is overloaded for different argument types.
  • insert(): inserts the specified argument at the specified index. This method is overloaded for different argument types.
  • delete(int begin, int end): deletes characters from the begin index to end — 1 index.
  • deleteCharAt(int index): deletes the character at the specified index.
  • reverse(): reverses the content of the StringBuffer.
  • setLength(int length): sets the length of the StringBuffer to the specified length.
  • ensureCapacity(int capacity): increases the capacity of the StringBuffer based on our requirement.
  • trimToSize(): deallocates extra allocated free memory.

StringBuilder

In addition to StringBuffer, there is another class called StringBuilder that provides similar functionality but is not synchronized. Every method present in StringBuffer is synchronized, allowing only one thread to operate on the StringBuffer object at a time, which may cause performance problems. To handle this problem, the StringBuilder concept was introduced in Java 1.5. StringBuilder is non-synchronized and multiple threads can operate on it at a time, making it faster than StringBuffer.

Examples

Example 1: Using StringBuffer to concatenate strings in a loop

Java
String[] words = {"Hello", "world", "!"};
StringBuffer sb = new StringBuffer();
for (String word : words) {
    sb.append(word);
}
String result = sb.toString(); // "Helloworld!"

In this example, we use a StringBuffer to concatenate the strings in the words array inside a loop. Since we are performing multiple concatenations, it is more efficient to use a StringBuffer than to create new String objects with each concatenation.

Example 2: Using StringBuilder for single-threaded string operations

Java
StringBuilder sb = new StringBuilder();
sb.append("The quick brown ");
sb.append("fox jumped over ");
sb.append("the lazy dog.");
String result = sb.toString(); // "The quick brown fox jumped over the lazy dog."

In this example, we use a StringBuilder to concatenate three strings together. Since there is no need for synchronization in this single-threaded example, we can use a StringBuilder instead of a StringBuffer for improved performance.

Example 3: Using StringBuffer for multi-threaded string operations

Java
String[] words = {"The", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog."};
StringBuffer sb = new StringBuffer();
Arrays.stream(words)
    .parallel()
    .forEach(word -> sb.append(word).append(" "));
String result = sb.toString(); // "The quick brown fox jumped over the lazy dog."

In this example, we use a StringBuffer to concatenate the strings in the words array in a multi-threaded environment. Since StringBuffer is synchronized, it can safely be used by multiple threads. Note that if we were to use a StringBuilder instead of a StringBuffer in a multi-threaded environment, we could run into synchronization issues.

String Vs StringBuffer Vs StringBuilder

  1. If the content is fixed and won’t change frequently, we can use the String class.
  2. If the content is not fixed and keeps changing frequently, and we want thread safety, we can use the StringBuffer class.
  3. If the content is not fixed and keeps changing frequently, and we don’t want thread safety, we can use the StringBuilder class.

Java string constant pool

Unlocking the Java String Constant Pool (SCP): 10 Proven Strategies for Success

Java String Constant Pool (SCP)

String is a widely used data type in Java, and it has a special place in memory management. In Java, every time a new object is created, it is allocated memory in the heap area. However, Java has a special feature called String Constant Pool (SCP) that allows for efficient memory management of String objects. In this blog post, we will discuss what SCP is, how it works, and how it impacts memory management in Java.

What is Java String Constant Pool (SCP)?

SCP is a special memory area in Java where all String literals are stored. When a String literal is encountered, Java looks for the same String literal in the SCP. If the String literal already exists in SCP, Java uses the existing String literal from SCP. Otherwise, Java creates a new String literal in SCP. This ensures that only one copy of a particular String literal is stored in the memory.

SCP is part of the runtime constant pool, which is a shared pool of constants that are loaded with the class definition. SCP is created when the class is loaded by the JVM and is destroyed when the JVM shuts down. SCP is not accessible by the garbage collector, so even if a String object does not have any references pointing to it, it will not be eligible for garbage collection if it is in SCP.

How SCP works?

SCP works differently for String objects created using the “new” keyword and String literals.

When a String object is created using the “new” keyword, two objects are created: one in the heap area and one in SCP. The reference variable points to the object in the heap area. For example:

Java
String s = new String(“softAai”);

In this case, the String literal “softAai” is created in SCP, and a new String object is created in the heap area. The reference variable “s” points to the String object in the heap area.

On the other hand, when a String literal is encountered, Java checks if the same String literal exists in SCP. If it does, Java uses the existing String literal from SCP. Otherwise, Java creates a new String literal in SCP. For example:

Java
String s = “softAai”;

In this case, the String literal “softAai” is created in SCP, and the reference variable “s” points to the String literal in SCP. No new object is created in the heap area.

Note that object creation in SCP is optional. If a String object with the required content is already present in SCP, the existing object will be reused instead of creating a new object.

Impact on Memory Management

SCP has a significant impact on memory management in Java. When a program uses many String literals, SCP ensures that only one copy of each String literal is stored in the memory, which saves a lot of memory space. This also improves the performance of the program because fewer objects need to be created and garbage collected.

However, if a program creates many String objects using the “new” keyword, SCP does not have a significant impact on memory management. In this case, each String object is created in the heap area, and SCP only contains the String literals.

SCP Limitations

The SCP has some limitations, such as the fact that objects in the SCP cannot be garbage collected, even if they are no longer being used by the program. This can lead to memory leaks if too many objects are created in the SCP. Additionally, because objects in the SCP are shared among all threads in the JVM, changes to these objects made by one thread can affect the behavior of other threads in unpredictable ways.

Despite these limitations, the SCP remains an essential part of Java’s memory management system. Storing string literals and constant values in the SCP can help to reduce memory usage and improve performance by minimizing the number of objects that need to be created. Additionally, the SCP can help to improve string comparison performance by allowing the JVM to compare string objects by reference instead of by value

Conclusion

SCP is a powerful feature of Java that allows for efficient memory management of String objects. By storing String literals in a shared memory area, SCP ensures that only one copy of each String literal is stored in memory, which saves a lot of memory space and improves the performance of the program. While SCP has some limitations, still it remains an essential part of Java’s memory management system.

error: Content is protected !!