1. Overview
In Kotlin, working with lists is a common task in many applications. Sometimes, we need to add an element to an existing list.
In this tutorial, we’ll explore how to prepend an element to a list in Kotlin.
2. Introduction to the Problem
First of all, prepending an element to a list means adding an element to the very beginning of a list.
Kotlin has two commonly used List interfaces: List and MutableList. List is read-only. However, as its name implies, MutableList is mutable.
We’ll discuss both cases in this tutorial and explore different approaches to prepending an element to a list. As usual, for simplicity, we’ll use unit test assertions to verify whether each approach works as expected.
First, let’s prepend an element to a MutableList.
3. Prepending an Element to a MutableList
We can insert or remove elements from a MutableList in Kotlin. Therefore, it’s pretty straightforward to prepend an element to a MutableList.
Let’s say we have a MutableList of Strings*,* which stores three song titles from the Beatles:
val mutableSongs = mutableListOf("Let it be", "Don't let me down", "I want to hold your hand")
Now, we’d like to prepend another popular song, “*Hey Jude,*” to the list. Then, the expected result should hold the following elements:
val expected = listOf("Hey Jude", "Let it be", "Don't let me down", "I want to hold your hand")
MutableList is a subtype of the List interface. Therefore, the List.add(index, element) method is available for MutableList objects too. Then, solving the problem becomes a piece of cake:
mutableSongs.add(0, "Hey Jude")
assertEquals(expected, mutableSongs)
4. Prepending an Element to a (Read-Only) List
In Kotlin, List instances are read-only. In other words, we cannot insert an element into a List. So, prepending an element E to a List LIST produces a new List object, which carries the element E and all elements in LIST.
Again, let’s create the beatlesSongs read-only list to hold the same three songs:
val beatlesSongs = listOf("Let it be", "Don't let me down", "I want to hold your hand")
Next, let’s see various ways to prepend the lovely song “Hey Jude” to the read-only list above.
4.1. Using the ‘*+*‘ Operator
In Kotlin, we have the convenience of using the ‘*+*‘ operator to concatenate two collections into a single list. However, in our case, we’re focused on prepending a single element to an existing list rather than combining two collections. So, let’s create a list using listOf(“Hey Jude”) and “plus” it with the beatlesSongs list:
val result = listOf("Hey Jude") + beatlesSongs
assertEquals(expected, result)
As we can see in the test above, the returned result is a new list.
4.2. Creating the prepend() Extension
We’ve seen that the ‘*+*‘ operation solves the problem. But it always creates a single-element list unnecessarily. If we often need to prepend elements to a list in our project, we’d like to have a more efficient and fluent approach. We can make use of Kotlin’s extension function to achieve that.
Next, let’s create an extension function on the List interface:
infix fun <T> List<T>.prepend(e: T): List<T> {
return buildList(this.size + 1) {
add(e)
addAll(this@prepend)
}
}
We use the standard buildList() function to build a read-only list. Here, we should note that there are two “this” references in the extension function, this.size and this@prepend. The latter is qualified this to refer to the outer instance, which is the list we’re about to prepend the element to.
By implementing the prepend() function as an extension function for the List interface, we can conveniently make it available for all List objects. This means we can directly call prepend() on any list instance, such as beatlesSongs.prepend(“Hey Jude”), to prepend an element to the list.
Furthermore, we’ve used the infix notation for the extension function in our implementation. This notation allows us to call the function more natural and fluently, making the code easier to read and understand.
Here’s an example of how the prepend() function can be used:
val result = beatlesSongs prepend "Hey Jude"
assertEquals(expected, result)
4.3. Creating the prependTo() Extension
The prepend() extension’s receiver is the list. Similarly, we can create an extension function to make the element the receiver:
infix fun <T> T.prependTo(theList: List<T>): List<T> {
return buildList(theList.size + 1) {
add(this@prependTo)
addAll(theList)
}
}
In this way, we can call the function from the element side. This can be convenient sometimes:
val result = "Hey Jude" prependTo beatlesSongs
assertEquals(expected, result)
5. Using the Deque Structure
So far, we’ve seen different approaches to prepending an element to a List or a MutableList. Finally, let’s see a solution using the Deque structure.
LinkedList is an implementation of the Deque interface. So next, let’s take LinkedList as an example to see how easy prepending operation to a Deque object can be:
val linkedList = LinkedList(beatlesSongs)
linkedList.addFirst("Hey Jude")
assertEquals(expected, linkedList)
The addFirst() method can be replaced with the push() method. They work in the same way in LinkedList:
public void push(E e) {
this.addFirst(e);
}
Some of us may ask, both mutableListOf() and LinkedList are MutableList. How to choose between mutableListOf() and LinkedList?
The mutableListOf() function returns an ArrayList. It offers fast random access. Of course, the add() operation is pretty fast too. *ArrayList.add()*‘s time complexity is O(1) in the best and average cases. However, **in the worst case, due to creating a new array and copying all the elements from the old backing array, ArrayList.add()‘s time complexity is O(n).
On the other hand, LinkedList.add()* is always *O(1). However, unlike ArrayList, LinkedList doesn’t support fast random access. Of course, there are a few other differences between ArrayList and LinkedList as well.
So, in general, if we must perform the “prepend” operation frequently to a list and don’t have much random access to it, choosing LinkedList can improve performance.
6. Conclusion
In this article, we’ve delved into different techniques for prepending an element to a list in Kotlin. We have examined scenarios involving both read-only List and MutableList, looking at practical examples for each case. Additionally, we’ve explored considerations for selecting the appropriate List implementation based on performance requirements.
As usual, all code snippets presented here are available over on GitHub.