1. Overview
In this article, we’ll take a holistic approach to understand the support for arrays in Kotlin.
2. Get and Set Functions
First, let’s initialize an array with String values to store the names of a few sports:
sports = arrayOf("Soccer", "Cricket", "Rugby")
Like most modern programming languages, Kotlin uses zero-based array indexing. Further, the Array.kt class in Kotlin provides a get function, which is turned into the [] by operator overloading:
public operator fun get(index: Int): T
Let’s use the get function to access the array members using the indices:
Assertions.assertEquals("Soccer", sports.get(0))
Assertions.assertEquals("Cricket", sports.get(1));
Assertions.assertEquals("Rugby", sports.get(2));
If we try to access an invalid index of the array, Kotlin throws the IndexOutOfBoundsException:
Assertions.assertThrows(IndexOutOfBoundsException::class.java) {
sports.get(100)
}
Assertions.assertThrows(IndexOutOfBoundsException::class.java) {
sports.get(-1)
}
Next, let’s use the set function to modify a value in the array:
sports.set(0, "Football")
Assertions.assertEquals("Football", sports[0])
Finally, let’s try to modify a value using an invalid index and validate that we see an IndexOutOfBoundsException:
Assertions.assertThrows(IndexOutOfBoundsException::class.java) {
sports.set(10, "Football")
}
3. Array Traversal
Kotlin supports multiple ways to traverse an array. In this section, we’ll explore most of the typical array traversal patterns. But, before that, let’s define an array of integer values to store the value written on each of the six faces of a regular dice:
val dice = arrayOf(1, 2, 3, 4, 5, 6)
First, let’s use a for loop to traverse the dice array:
for (faceValue in dice) {
println(faceValue)
}
Next, let’s see how we can initialize an array iterator to iterate over the values:
val iterator = dice.iterator()
while (iterator.hasNext()) {
val faceValue = iterator.next()
println(faceValue)
}
Another typical pattern to loop through an array is using the forEach loop:
dice.forEach { faceValue ->
println(faceValue)
}
Finally, let’s see how to use the forEachIndexed pattern, which gives us both the index and the value at that index within an array:
dice.forEachIndexed { index, faceValue ->
println("Value at $index position is $faceValue")
}
4. In-Place Reordering
In this section, we’ll learn a few common ways to do an in-place reordering of the values of an array.
4.1. Setup
Let’s define helper functions to generate and print the values of an integer array so that we can reuse them to validate multiple scenarios.
Firstly, we have the initArray() function to return an int array:
fun initArray(): Array<Int> {
return arrayOf(3, 2, 50, 15, 10, 1)
}
Secondly, we have the printArray() function to print the values of the array:
fun printArray(array: Array<Int>) {
array.forEach { print("$it ") }
println()
}
4.2. Reverse
Kotlin supports both partial and complete in-place reversal of an array. We’ll treat each scenario independently by running it on the same array:
3 2 50 15 10 1
First, let’s see a complete reversal of the numbers array:
numbers = initArray()
numbers.reverse()
printArray(numbers)
As expected, the output shows that the whole array was reversed:
1 10 15 50 2 3
Now, let’s reverse the array between indices 4 (inclusive) and 6 (exclusive):
numbers = initArray()
numbers.reverse(4, 6)
printArray(numbers)
We can see that only the last two values in the array changed, while the values till index=3 remain unchanged:
3 2 50 15 1 10
4.3. Sort
Kotlin supports in-place stable sort for an array according to the natural order of the elements. Again, we’ll use the original numbers array for all the sorting scenarios:
3 2 50 15 10 1
First, let’s sort the numbers array over its entire range:
numbers = initArray()
numbers.sort()
printArray(numbers)
As expected, the numbers are sorted in ascending order:
1 2 3 10 15 50
Next, let’s sort the original numbers array from index = 4 (inclusive) till the last position:
numbers = initArray()
numbers.sort(4)
printArray(numbers)
We can see that values till index=3 remain unchanged, while values from index=4 are sorted in ascending order:
3 2 50 15 1 10
Finally, let’s sort the numbers array between indices 0 (inclusive) and 2 (exclusive):
numbers = initArray()
numbers.sort(0, 2)
printArray(numbers)
As expected, only the first two elements are sorted in place:
2 3 50 15 10 1
4.4. Shuffle
Kotlin provides two overloaded functions that shuffle an array using the Fisher-Yates shuffle algorithm:
public fun <T> Array<T>.shuffle(): Unit
public fun <T> Array<T>.shuffle(random: Random): Unit
First, let’s call the no-args shuffle() function and see the reordered array values:
numbers = initArray()
numbers.shuffle()
printArray(numbers)
As expected, the values are shuffled in place:
2 10 3 1 50 15
Next, let’s pass an instance of Random with a seed value of 2:
numbers = initArray()
numbers.shuffle(Random(2))
printArray(numbers)
Again, the shuffling is quite evident from the output:
1 3 50 15 10 2
We must note that the seed value plays a crucial role in shuffling. Given the same array values and a Random instance with the same seed value, we’ll always get the same order of values after the first shuffle.
5. Building Maps
In this section, we’ll explore a few functions that can help us write beautiful code to build a map by transforming the array values.
5.1. associate, associateBy, and associateWith
We can transform the values of an array to generate keys and values of a map. Moreover, we can also decide to keep the array values to denote either the set of keys or values. To support these multiple use cases, Kotlin provides the functions associate(), associateBy(), and asssociateWith().
Let’s initialize an array to store names of a few common fruits:
val fruits = arrayOf("Pear", "Apple", "Papaya", "Banana")
Now, if we want to build a mapping of a fruit’s name with the length of the name, then we can use the associate() function:
println(fruits.associate { Pair(it, it.length) })
{Pear=4, Apple=5, Papaya=6, Banana=6}
However, as we’re generating keys directly from the array values, we can also use the associateWith() function to decide how to generate the map’s value for each key:
println(fruits.associateWith { it.length })
{Pear=4, Apple=5, Papaya=6, Banana=6}
Lastly, if we want to build a map with the length of the fruit’s name as the key and the fruit’s name as the values, then we can use the associateBy() function:
println(fruits.associateBy { it.length })
{4=Pear, 5=Apple, 6=Banana}
5.2. associateTo, associateByTo, and associateWithTo
When we use associate(), associateBy(), or associateWith(), Kotlin creates a new map and populate its data. However, we can also populate data in an existing map by using the associateTo(), associateByTo(), and associateWithTo() variants of these functions.
Let’s say we have an existing mapping of the fruit’s name and length of the name:
val nameVsLengthMap = mutableMapOf("Pomegranate" to 11, "Pea" to 3)
val lengthVsNameMap = mutableMapOf(11 to "Pomegranate", 3 to "Pea")
Now, we can use associateWithTo() and associateByTo() functions to populate these maps with new values:
println(fruits.associateWithTo(nameVsLengthMap, { it.length }))
println(fruits.associateByTo(lengthVsNameMap, { it.length }))
{Pomegranate=11, Pea=3, Pear=4, Apple=5, Papaya=6, Banana=6}
{11=Pomegranate, 3=Pea, 4=Pear, 5=Apple, 6=Banana}
6. Conclusion
In this article, we explored our way into several functions supported by Kotlin to work with arrays.
As always, the source code is available over on GitHub.