Generic Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this quick tutorial, we'll introduce the 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 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<String> and avoiding ending up with List<List<String>>:

@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.

Generic bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Comments are closed on this article!