1. Overview
In this quick tutorial, we’ll introduce Kotlin’s Collections API, and we’ll discuss the different collection types in Kotlin and some common operations on collections.
2. Collection vs. Mutable Collection
First, let’s take a look at different types of collections in Kotlin. We will see how to initialize the basic types of collections.
The Collection interface supports read-only methods while MutableCollection support read/write methods.
2.1. List
We can create a simple read-only List using method listOf() and read-write MutableList using mutableListOf():
val theList = listOf("one", "two", "three")
val theMutableList = mutableListOf("one", "two", "three")
2.2. Set
Similarly we can create a read-only Set using method setOf() and read-write MutableSet using mutableSetOf():
val theSet = setOf("one", "two", "three")
val theMutableSet = mutableSetOf("one", "two", "three")
2.3. Map
We can also create a read-only Map using method mapOf() and read-write MutableMap using mutableMapOf():
val theMap = mapOf(1 to "one", 2 to "two", 3 to "three")
val theMutableMap = mutableMapOf(1 to "one", 2 to "two", 3 to "three")
3. Useful Operators
Kotlin’s Collections API is much richer than the one we can find in Java – it comes with a set of overloaded operators.
3.1. The “in” Operator
We can use the expression “x in collection” which can be translated to collection.contains(x):
@Test
fun whenSearchForExistingItem_thenFound () {
val theList = listOf("one", "two", "three")
assertTrue("two" in theList)
}
3.2. The “+” Operator
We can an element or entire collection to another using “+” operator:
@Test
fun whenJoinTwoCollections_thenSuccess () {
val firstList = listOf("one", "two", "three")
val secondList = listOf("four", "five", "six")
val resultList = firstList + secondList
assertEquals(6, resultList.size)
assertTrue(resultList.contains("two"))
assertTrue(resultList.contains("five"))
}
3.3. The “-“ Operator
Similarly, we can remove an element or multiple elements using “-” operator:
@Test
fun whenExcludeItems_thenRemoved () {
val firstList = listOf("one", "two", "three")
val secondList = listOf("one", "three")
val resultList = firstList - secondList
assertEquals(1, resultList.size)
assertTrue(resultList.contains("two"))
}
4. Other Methods
Finally, we will explore some common methods for collection. In Java, if we wanted to leverage advanced methods, we would need to use Stream API.
In Kotlin, we can find similar methods available in the Collections API.
4.1. Slicing
We can obtain a sublist from a given List:
@Test
fun whenSliceCollection_thenSuccess () {
val theList = listOf("one", "two", "three")
val resultList = theList.slice(1..2)
assertEquals(2, resultList.size)
assertTrue(resultList.contains("two"))
}
4.2. Removing
We can easily remove all nulls from a List:
@Test
fun whenFilterNullValues_thenSuccess () {
val theList = listOf("one", null, "two", null, "three")
val resultList = theList.filterNotNull()
assertEquals(3, resultList.size)
}
4.3. Filtering
We can filter collection items easily using filter*(),* which works similarly to the filter() method from Java Stream API:
@Test
fun whenFilterNonPositiveValues_thenSuccess () {
val theList = listOf(1, 2, -3, -4, 5, -6)
val resultList = theList.filter{ it > 0}
assertEquals(3, resultList.size)
assertTrue(resultList.contains(1))
assertFalse(resultList.contains(-4))
}
4.4. Dropping
We can drop the first N items:
@Test
fun whenDropFirstItems_thenRemoved () {
val theList = listOf("one", "two", "three", "four")
val resultList = theList.drop(2)
assertEquals(2, resultList.size)
assertFalse(resultList.contains("one"))
assertFalse(resultList.contains("two"))
}
We can drop the first few items if they satisfy the given condition:
@Test
fun whenDropFirstItemsBasedOnCondition_thenRemoved () {
val theList = listOf("one", "two", "three", "four")
val resultList = theList.dropWhile{ it.length < 4 }
assertEquals(2, resultList.size)
assertFalse(resultList.contains("one"))
assertFalse(resultList.contains("two"))
}
4.5. Grouping
We can group elements:
@Test
fun whenGroupItems_thenSuccess () {
val theList = listOf(1, 2, 3, 4, 5, 6)
val resultMap = theList.groupBy{ it % 3}
assertEquals(3, resultMap.size)
assertTrue(resultMap[1]!!.contains(1))
assertTrue(resultMap[2]!!.contains(5))
}
4.6. Mapping
We can map all elements using the provided function:
@Test
fun whenApplyFunctionToAllItems_thenSuccess () {
val theList = listOf(1, 2, 3, 4, 5, 6)
val resultList = theList.map{ it * it }
assertEquals(4, resultList[1])
assertEquals(9, resultList[2])
}
We can use flatMap() to flatten nested collections. Here, we are converting Strings to List
@Test
fun whenApplyMultiOutputFunctionToAllItems_thenSuccess () {
val theList = listOf("John", "Tom")
val resultList = theList.flatMap{ it.toLowerCase().toList() }
assertEquals(7, resultList.size)
}
4.7. Reduction
We can perform fold/reduce operation:
@Test
fun whenApplyFunctionToAllItemsWithStartingValue_thenSuccess () {
val theList = listOf(1, 2, 3, 4, 5, 6)
val finalResult = theList.fold(0, {acc, i -> acc + (i * i)})
assertEquals(91, finalResult)
}
4.8. Chunking
To break a collection into chunks of a given size, we can use the chunked() method:
@Test
fun whenApplyingChunked_thenShouldBreakTheCollection() {
val theList = listOf(1, 2, 3, 4, 5)
val chunked = theList.chunked(2)
assertThat(chunked.size).isEqualTo(3)
assertThat(chunked.first()).contains(1, 2)
assertThat(chunked[1]).contains(3, 4)
assertThat(chunked.last()).contains(5)
}
Since the collection has five elements, the chunked(2) method call returns two collections with two elements each and one single-element collection.
It’s also possible to map each chunk to something else after breaking up the collection:
@Test
fun whenApplyingChunkedWithTransformation_thenShouldBreakTheCollection() {
val theList = listOf(1, 2, 3, 4, 5)
val chunked = theList.chunked(3) { it.joinToString(", ") }
assertThat(chunked.size).isEqualTo(2)
assertThat(chunked.first()).isEqualTo("1, 2, 3")
assertThat(chunked.last()).isEqualTo("4, 5")
}
After creating chunks of size 3, we convert each chunk to a comma-separated string.
4.9. Windowing
The windowed() function returns a list of element ranges by moving a sliding window of a given size over a collection of elements.
In order to better understand this, let’s see how windowed(3) works on a collection of 6 elements:
At first, the window size is 3, therefore the first list would contain 1, 2, and 3. Then the sliding window moves one element further:
The sliding window moves forward until it fails to create another list of the given size:
This sequence of transitions manifests itself in the Kotlin code as:
@Test
fun whenApplyingWindowed_thenShouldCreateSlidingWindowsOfElements() {
val theList = (1..6).toList()
val windowed = theList.windowed(3)
assertThat(windowed.size).isEqualTo(4)
assertThat(windowed.first()).contains(1, 2, 3)
assertThat(windowed[1]).contains(2, 3, 4)
assertThat(windowed[2]).contains(3, 4, 5)
assertThat(windowed.last()).contains(4, 5, 6)
}
By default, the sliding window moves one step further each time. We can, of course, change that by passing a custom step value:
@Test
fun whenApplyingWindowedWithTwoSteps_thenShouldCreateSlidingWindowsOfElements() {
val theList = (1..6).toList()
val windowed = theList.windowed(size = 3, step = 2)
assertThat(windowed.size).isEqualTo(2)
assertThat(windowed.first()).contains(1, 2, 3)
assertThat(windowed.last()).contains(3, 4, 5)
}
The windowed() function, by default, always and only creates ranges of the given size. To change that, we can set the partialWindows parameter to true:
@Test
fun whenApplyingPartialWindowedWithTwoSteps_thenShouldCreateSlidingWindowsOfElements() {
val theList = (1..6).toList()
val windowed = theList.windowed(size = 3, step = 2, partialWindows = true)
assertThat(windowed.size).isEqualTo(3)
assertThat(windowed.first()).contains(1, 2, 3)
assertThat(windowed[1]).contains(3, 4, 5)
assertThat(windowed.last()).contains(5, 6)
}
Similar to the chunked() function, it’s possible to map each range to something else:
@Test
fun whenApplyingTransformingWindows_thenShouldCreateSlidingWindowsOfElements() {
val theList = (1..6).toList()
val windowed = theList.windowed(size = 3, step = 2, partialWindows = true) { it.joinToString(", ") }
assertThat(windowed.size).isEqualTo(3)
assertThat(windowed.first()).isEqualTo("1, 2, 3")
assertThat(windowed[1]).isEqualTo("3, 4, 5")
assertThat(windowed.last()).isEqualTo("5, 6")
}
5. Conclusion
We explored Kotlin’s Collections API and some of the most interesting methods.
And, as always, the full source code can be found over on GitHub.