Binary trees are a fundamental data structure in computer science, widely used for representing hierarchical data. In this blog post, we will delve into the concept of binary trees, their implementation in Java, traversal algorithms, and some common operations associated with them. Understanding Binary Trees What is a Tree? A tree is a non-linear data...
Doubly Linked Lists are a fundamental data structure in computer science, particularly in Java programming. They offer efficient insertion, deletion, and traversal operations compared to other data structures like arrays or singly linked lists. In this blog, we’ll delve into the concept of doubly linked lists, their implementation in Java, and explore some common operations...
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,...
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?
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.
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.
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) {returntrue; } current = current.next; // Move to the next node in each iteration }
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
publicbooleanfind(ListNode head, int searchKey) {if (head == null) {returnfalse; }ListNodecurrent = head;while (current != null) {if (current.data == searchKey) {returntrue; } current = current.next; // Move to the next node }returnfalse;}
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.
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.
We create three temporary nodes:
current points to head.
previous initially points to null.
next also initially points to null.
We traverse the list node by node using a while loop. The loop iterates until current becomes null.
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 nodecurrent.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.
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.
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
ListNodeslowPtr = head;ListNodefastPtr = 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.
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
ListNodefastPtr = head;ListNodeslowPtr = 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 aheadif(slowPtr == fastPtr) { // If slow pointer meets fast pointer, it indicates a loopreturntrue; }}returnfalse; // 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
publicbooleancontainsLoop() {ListNodefastPtr = head;ListNodeslowPtr = 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) {returntrue; } }returnfalse;}publicvoidcreateALoopInLinkedList() {ListNodefirst = newListNode(1); ListNodesecond = newListNode(2);ListNodethird = newListNode(3);ListNodefourth = newListNode(4);ListNodefifth = newListNode(5);ListNodesixth = newListNode(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 Listin 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
ListNodemainPtr = head; // It will move forward when the reference pointer covers the nth position forward from the head ListNodereferencePtr = 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 positionintcount = 0; // It is to track the number of nodes the reference pointer moved forwardwhile(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
publicListNodegetNthNodeFromEnd(int n) {if (head == null) {returnnull; }if (n <= 0) {thrownewIllegalArgumentException("Invalid value: n = " + n); }ListNodemainPtr = head; // It will move forward when the reference pointer covers the nth position forward from the head ListNoderefPtr = 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.intcount = 0; // It is to track the number of nodes refPtr moved forwardwhile (count < n) {if (refPtr == null) {thrownewIllegalArgumentException(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
ListNodecurrent = 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.
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
ListNodecurrent = head;ListNodeprevious = 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
publicListNodeinsertInSortedList(int value) {ListNodenewNode = newListNode(value);if (head == null) {return newNode; }ListNodecurrent = head;ListNodeprevious = 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
ListNodecurrent = head;ListNodeprevious = null;// Traverse the list to find the node with the key valuewhile(current != null && current.data != key) { previous = current; current = current.next;}// If we reached the end of the list without finding the key, returnif(current == null) {return;}// If we found the key, remove the node by adjusting the previous node's next referenceif(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
publicvoiddeleteNode(int key) {ListNodecurrent = head;ListNodeprevious = 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:
We iterate through the array, examining each element.
At each element, we calculate the difference between the target and the current element.
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.
We return the indices of the current element and the element with the required difference.
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.
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.
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.
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.
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:
It consists of a sequence of nodes.
Each node contains data and a reference to the next node.
The first node is called the head node.
The last node contains data and points to null.
Java
| data | next | ===> | data | next | ===> | data | next | ===> null
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.
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.
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
publicclassLinkedList{privateListNodehead; // head node to hold the list// It contains a static inner class ListNodeprivatestaticclassListNode {privateintdata;privateListNodenext;publicListNode(intdata) {this.data = data;this.next = null; } }publicstaticvoidmain(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!
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...
Imagine a world where you could test drive a car, play a game, or edit a photo without ever downloading an app. Enter the realm of Android Instant Apps, a revolutionary technology that lets users experience apps directly from their web browsers, without committing to the storage space or installation hassle. Android Instant Apps have...
Functional programming has gained widespread popularity for its emphasis on immutability, higher-order functions, and declarative style. Kotlin, a versatile and modern programming language, seamlessly incorporates functional programming concepts, allowing developers to write concise, expressive, and maintainable code. In this blog post, we’ll delve into the world of functional programming in Kotlin, exploring its key features,...
Kotlin, the JVM’s rising star, isn’t just known for its conciseness and elegance. It’s also a powerful object-oriented language, packing a punch with its intuitive and modern take on OOP concepts. Whether you’re a seasoned Java veteran or a curious newbie, navigating Kotlin’s object-oriented playground can be both exciting and, well, a bit daunting.
But fear not, fellow programmer! This blog takes you on a guided tour of Kotlin’s OOP constructs, breaking down each element with practical examples and clear explanations. Buckle up, and let’s dive into the heart of Kotlin’s object-oriented magic!
BTW, What is Contruct?
The term “construct” is defined as a fancy way to refer to allowed syntax within a programming language. It implies that when creating objects, defining categories, specifying relationships, and other similar tasks in the context of programming, one utilizes the permissible syntax provided by the programming language. In essence, “language constructs” are the syntactic elements or features within the language that enable developers to express various aspects of their code, such as the creation of objects, organization into categories, establishment of relationships, and more.
In simple words, Language constructs are the specific rules and structures that are permitted within a programming language to create different elements of a program. They are essentially the building blocks that programmers use to express concepts and logic in a way that the computer can understand.
Kotlin Construct
Kotlin provides a rich set of language constructs that empower developers to articulate their programs effectively. In this section, we’ll explore several of these constructs, including but not limited to: Class Definitions, Inheritance Mechanisms, Abstract Classes, Interface Implementations, Object Declarations, and Companion Objects.
Classes
Classes serve as the fundamental building blocks in Kotlin, offering a template that encapsulates state, behavior, and a specific type for instances (more details on this will be discussed later). Defining a class in Kotlin requires only a name. For instance:
Kotlin
classVeryBasic
While VeryBasic may not be particularly useful, it remains a valid Kotlin syntax. Despite lacking state or behavior, instances of the VeryBasic type can still be declared, as demonstrated below:
In this example, the basic value is of type VeryBasic, indicating that it is an instance of the VeryBasic class. Kotlin’s type inference capability allows for a more concise declaration:
In this revised version, Kotlin infers the type of the basic variable. As a VeryBasic instance, basic inherits the state and behavior associated with the VeryBasic type, which, in this case, is none—making it a somewhat melancholic example.
Properties
As mentioned earlier, classes in Kotlin can encapsulate a state, with the class’s state being represented by properties. Let’s delve into the example of a BlueberryCupcake class:
Kotlin
classBlueberryCupcake {var flavour = "Blueberry"}
Here, the BlueberryCupcake class possesses a property named flavour of type String. Instances of this class can be created and manipulated, as demonstrated in the following code snippet:
Kotlin
funmain(args: Array<String>) {val myCupcake = BlueberryCupcake()println("My cupcake has ${myCupcake.flavour}")}
Given that the flavour property is declared as a variable, its value can be altered dynamically during runtime:
In reality, cupcakes do not change their flavor, unless they become stale. To mirror this in code, we can declare the flavour property as a value, rendering it immutable:
Kotlin
classBlueberryCupcake {val flavour = "Blueberry"}
Attempting to reassign a value to a property declared as a val results in a compilation error, as demonstrated below:
Kotlin
funmain(args: Array<String>) {val myCupcake = BlueberryCupcake() myCupcake.flavour = "Almond"// Compilation error: Val cannot be reassignedprintln("My cupcake has ${myCupcake.flavour}")}
Now, let’s introduce a new class for almond cupcakes, the AlmondCupcake class:
Kotlin
classAlmondCupcake {val flavour = "Almond"}
Interestingly, both BlueberryCupcake and AlmondCupcake share identical structures; only the internal value changes. In reality, you don’t need different baking tins for distinct cupcake flavors. Similarly, a well-designed Cupcake class can be employed for various instances:
Kotlin
classCupcake(val flavour: String)
The Cupcake class features a constructor with a flavour parameter, which is assigned to the flavour property. In Kotlin, to enhance readability, you can use syntactic sugar to define it more succinctly:
Kotlin
classCupcake(val flavour: String)
This streamlined syntax allows us to create several instances of the Cupcake class with different flavors:
In essence, this example showcases how Kotlin’s concise syntax and flexibility in property declaration enable the creation of classes representing real-world entities with ease.
Methods
In Kotlin, a class’s behavior is defined through methods, which are technically member functions. Let’s explore an example using the Cupcake class:
Executing this code will produce the following output:
Kotlin
nom, nom, nom... delicious Blueberry cupcake
While this example may not be mind-blowing, it serves as an introduction to methods. As we progress, we’ll explore more intricate and interesting aspects of defining and utilizing methods in Kotlin.
Inheritance
Inheritance is a fundamental concept that involves organizing entities into groups and subgroups and also establishing relationships between them. In an inheritance hierarchy, moving up reveals more general features and behaviors, while descending highlights more specific ones. For instance, a burrito and a microprocessor are both objects, yet their purposes and uses differ significantly.
Remarkably, this class closely resembles the Cupcake class. To address code duplication, we can refactor these classes by introducing a common superclass, BakeryGood:
Here, both Cupcake and Biscuit extend BakeryGood, sharing its behavior and state. This establishes an is-a relationship, where Cupcake (and Biscuit) is a BakeryGood, and BakeryGood is the superclass.
Note the use of the open keyword to indicate that BakeryGood is designed to be extended. In Kotlin, a class must be marked as open to enable inheritance.
The process of consolidating common behaviors and states in a parent class is termed generalization. However, our initial attempt encounters unexpected results when calling the eat() method with a reference to BakeryGood:
Now, calling the eat() method produces the expected output:
Kotlin
nom, nom, nom... delicious Blueberry cupcake
Here, the process of extending classes and overriding behavior in a hierarchy is called specialization. A key guideline is to place general states and behaviors at the top of the hierarchy (generalization) and specific states and behaviors in subclasses (specialization).
We can further extend subclasses, such as introducing a new Roll class:
Subclasses, like CinnamonRoll, can be extended as well, marked as open. We can also create classes with additional properties and methods, exemplified by the Donut class:
This flexibility in inheritance and specialization allows for a versatile and hierarchical organization of classes in Kotlin.
Abstract classes
Up to this point, our bakery model has been progressing smoothly. However, a potential issue arises when we realize we can instantiate the BakeryGood class directly, making it too generic. To address this, we can mark BakeryGood as abstract:
By marking it as abstract, we ensure that BakeryGood can’t be instantiated directly, resolving our concern. The abstract keyword denotes that the class is intended solely for extension, and it cannot be instantiated on its own.
The distinction between abstract and open lies in their instantiation capabilities. While both modifiers allow for class extension, open permits instantiation, whereas abstract does not.
Now, given that we can’t instantiate BakeryGood directly, the name() method in the class becomes less useful. Most subclasses, except for CinnamonRoll, override it. Therefore, we redefine the BakeryGood class:
Here, the name() method is marked as abstract, lacking a body, only declaring its signature. Any class directly extending BakeryGood must implement (override) the name() method.
Let’s introduce a new class, Customer, representing a bakery customer:
Kotlin
classCustomer(val name: String) {funeats(food: BakeryGood) {println("$name is eating... ${food.eat()}") }}
The eats(food: BakeryGood) method accepts a BakeryGood parameter, allowing any instance of a class that extends BakeryGood, regardless of hierarchy levels. It’s important to note that we can’t instantiate BakeryGood directly.
Consider the scenario where we want a simple BakeryGood instance, like for testing purposes. An alternative approach is using an anonymous subclass:
Here, the object keyword introduces an object expression, defining an instance of an anonymous class that extends a type. The anonymous class must override the name() method and pass a value for the BakeryGood constructor, similar to how a standard class would.
Additionally, an object expression can be used to declare values:
This demonstrates how Kotlin’s flexibility with abstract classes, inheritance, and anonymous subclasses allows for a versatile and hierarchical organization of classes in a bakery scenario.
Interfaces
Creating hierarchies is effectively facilitated by open and abstract classes, yet their utility has limitations. In certain cases, subsets may bridge seemingly unrelated hierarchies. Take, for instance, the bipedal nature shared by birds and great apes; both belong to the categories of animals and vertebrates, despite lacking a direct relationship. To address such scenarios, Kotlin introduces interfaces as a distinct construct, recognizing that other programming languages may handle this issue differently.
While our bakery goods are commendable, their preparation involves an essential step: cooking. The existing code employs an abstract class named BakeryGood to define various baked products, accompanied by methods like eat() and bake().
However, a complication arises when considering items like donuts, which are not baked but fried. One potential solution is to move the bake() method to a separate abstract class named Bakeable.
Kotlin
abstractclassBakeable {funbake(): String {return"is hot here, isn't??" }}
By doing so, the code attempts to address the issue and introduces a class called Cupcake that extends both BakeryGood and Bakeable. Unfortunately, Kotlin imposes a restriction, allowing a class to extend only one other class at a time. This limitation prompts the need for an alternative approach.
The subsequent code explores a different strategy to resolve this limitation, emphasizing the intricate nature of class extension in Kotlin.
Kotlin
classCupcake(flavour: String) : BakeryGood(flavour), Bakeable() { // Compilation error: Only one class // may appear in a supertype listoverridefunname(): String {return"cupcake" }}
The above code snippets illustrate the attempt to reconcile the challenge of combining the BakeryGood and Bakeable functionalities in a single class, highlighting the restrictions imposed by Kotlin’s class extension mechanism.
Kotlin doesn’t allow a class to extend multiple classes simultaneously. Instead, we can make Cupcake extend BakeryGood and implement the Bakeable interface:
Kotlin
interfaceBakeable {funbake(): String {return"It's hot here, isn't it??" }}
An interface named Bakeable is defined with a method bake() that returns a string. Interfaces in Kotlin define a type that specifies behavior, such as the bake() method in the Bakeable interface.
A class named Cupcake is created, which extends both BakeryGood and implements the Bakeable interface. It has a method name() that returns “cupcake.”
Now, let’s highlight the similarities and differences between open/abstract classes and interfaces:
Similarities
Both are types with an is-a relationship.
Both define behaviors through methods.
Neither abstract classes nor interfaces can be instantiated directly.
Differences
A class can extend just one open or abstract class but can extend many interfaces.
An open/abstract class can have constructors, whereas interfaces cannot.
An open/abstract class can initialize its own values, whereas an interface’s values must be initialized in the classes that implement the interface.
An open class must declare methods that can be overridden as open, while an abstract class can have both open and abstract methods.
In an interface, all methods are open, and a method with no implementation doesn’t need an abstract modifier.
Here’s an example demonstrating the use of an interface and an open class:
Kotlin
interfaceFried {funfry(): String}openclassDonut(flavour: String, val topping: String) : BakeryGood(flavour), Fried {overridefunfry(): String {return"*swimming in oil*" }overridefunname(): String {return"donut with $topping topping" }}
When choosing between an open class, an abstract class, or an interface, consider the following guidelines:
Use an open class when the class should be both extended and instantiated.
Use an abstract class when the class can’t be instantiated, a constructor is needed, or there is initialization logic (using init blocks).
Use an interface when multiple inheritances must be applied, and no initialization logic is needed.
It’s recommended to start with an interface for a more straightforward and modular design. Move to abstract or open classes when data initialization or constructors are required.
Finally, object expressions can also be used with interfaces:
Kotlin
val somethingFried = object : Fried {overridefunfry(): String {return"TEST_3" }}
This showcases the flexibility of Kotlin’s object expressions in conjunction with interfaces.
Objects
Objects in Kotlin serve as natural singletons, meaning they naturally come as language features and not just as implementations of behavioral patterns seen in other languages. In Kotlin, every object is a singleton, presenting interesting patterns and practices, but they can also be risky if misused to maintain global state.
Object expressions are a way to create singletons, and they don’t need to extend any type. Here’s an example:
Kotlin
funmain(args: Array<String>) {val expression = object {val property = ""funmethod(): Int {println("from an object expression")return42 } }val i = "${expression.method()}${expression.property}"println(i)}
In this example, the expression value is an object that doesn’t have any specific type. Its properties and functions can be accessed as needed.
However, there is a restriction: object expressions without a type can only be used locally, inside a method, or privately, inside a class. Here’s an example demonstrating this limitation:
In this case, trying to access the property value outside the Outer class results in a compilation error.
It’s important to note that while object expressions provide a convenient way to create singletons, their use should be considered carefully. They are especially useful for coordinating actions across the system, but if misused to maintain global state, they can lead to potential issues. Careful consideration of the design and scope of objects in Kotlin is crucial to avoid unintended consequences.
Object Declaration
An object declaration is a way to create a named singleton:
In this example, Oven is a named singleton. It’s a singleton because there’s only one instance of Oven, and it’s named as an object declaration. You don’t need to instantiate Oven to use it.
Here, an instance of the Cupcake class is created, and the Oven.process method is called to process the myAlmondCupcake. Objects, being singletons, allow you to access their methods directly without instantiation.
Objects Extending Other Types
Objects can also extend other types, such as interfaces:
In this case, ElectricOven is an object that extends the Oven interface. It provides an implementation for the process method defined in the Oven interface.
Here, an instance of Cupcake is created, and the ElectricOven.process method is called to process the myAlmondCupcake.
In short, object declarations are a powerful feature in Kotlin, allowing the creation of singletons with or without names. They provide a clean and concise way to encapsulate functionality and state, making code more modular and maintainable.
Companion objects
Objects declared inside a class/interface and marked ascompanion object are called companion objects. They are associated with the class/interface and can be used to define methods or properties that are related to the class as a whole.
In this example, the Cupcake class has a companion object with two methods: almond() and cheese(). These methods can be called directly using the class name without instantiating the class.
Here, various instances of Cupcake are created using the companion object’s methods. Note that Cupcake.almond() and Cupcake.cheese() can be called without creating an instance of the Cupcake class.
Limitation on Usage from Instances
Companion object’s methods can’t be used from instances:
In this example, attempting to call cheese() on an instance of Cupcake results in a compilation error. Companion object’s methods are meant to be called directly on the class, not on instances.
Using Companion Objects Outside the Class
Companion objects can be used outside the class as values with the name Companion:
In this example, Cupcake without parentheses refers to the companion object itself. This usage is equivalent to Cupcake.Factory and can be seen as a shorthand syntax.
Don’t be confused by this syntax. The Cupcake value without parenthesis is the companion object; Cupcake() is an instance.
Conclusion
Kotlin’s support for object-oriented programming constructs empowers developers to build robust, modular, and maintainable code. With features like concise syntax, interoperability with Java, and modern language features, Kotlin continues to be a top choice for developers working on a wide range of projects, from mobile development to backend services. As we’ve explored in this guide, Kotlin’s OOP constructs provide a solid foundation for creating efficient and scalable applications.Kotlin’s language constructs are more than just features; they’re a philosophy. They encourage conciseness, expressiveness, and safety, making your code a joy to write. So, take your first step into the Kotlin world, and prepare to be amazed by its magic!