1. Overview

Kotlin collections are powerful data structures with many beneficial methods that put them over and beyond Java collections.

We’re going to cover a handful of filtering methods available in enough detail to be able to utilize all of the others that we don’t explicitly cover in this article.

All of these methods return a new collection, leaving the original collection unmodified.

We’ll be using lambda expressions to perform some of the filters. To read more about lambdas, have a look at our Kotlin Lambda article here.

2. Drop

We’ll start with a basic way of trimming down a collection. Dropping allows us to take a portion of the collection and return a new List missing the number of elements listed in the number:

@Test
fun whenDroppingFirstTwoItemsOfArray_thenTwoLess() {
    val array = arrayOf(1, 2, 3, 4)
    val result = array.drop(2)
    val expected = listOf(3, 4)

    assertIterableEquals(expected, result)
}

On the other hand, if we want to drop the last n elements, we call dropLast:

@Test
fun givenArray_whenDroppingLastElement_thenReturnListWithoutLastElement() {
    val array = arrayOf("1", "2", "3", "4")
    val result = array.dropLast(1)
    val expected = listOf("1", "2", "3")

    assertIterableEquals(expected, result)
}

Now we’re going to look at our first filter condition which contains a predicate.

This function will take our code and work backward through the list until we reach an element that does not meet the condition:

@Test
fun whenDroppingLastUntilPredicateIsFalse_thenReturnSubsetListOfFloats() {
    val array = arrayOf(1f, 1f, 1f, 1f, 1f, 2f, 1f, 1f, 1f)
    val result = array.dropLastWhile { it == 1f }
    val expected = listOf(1f, 1f, 1f, 1f, 1f, 2f)

    assertIterableEquals(expected, result)
}

dropLastWhile removed the final three 1fs from the list as the method cycled through each item until the first instance where an array element did not equal 1f.

The method stops removing elements as soon as an element does not meet the condition of the predicate.

dropWhile is another filter that takes a predicate but dropWhile works from index 0 -> n and dropLastWhile works from index n -> 0.

If we try to drop more elements than the collection contains, we’ll just be left with an empty List.

3. Take

Very similar to drop, take will keep the elements up to the given index or predicate:

@Test
fun `when predicating on 'is String', then produce list of array up until predicate is false`() {
    val originalArray = arrayOf("val1", 2, "val3", 4, "val5", 6)
    val actualList = originalArray.takeWhile { it is String }
    val expectedList = listOf("val1")

    assertIterableEquals(expectedList, actualList)
}

The difference between drop and take is that drop removes the items, whereas take keeps the items.

Attempting to take more items than are available in the collection – will just return a List that is the same size as the original collection

An important note here is that takeIf is NOT a collection method. takeIf uses a predicate to determine whether to return a null value or not – think Optional#filter.

Although it may seem that it’d meet the function name pattern to take all of the items that match the predicate into the returned List, we use the filter to perform that action.

4. Filter

The filter creates a new List based on the predicate provided:

@Test
fun givenAscendingValueMap_whenFilteringOnValue_ThenReturnSubsetOfMap() {
    val originalMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3)
    val filteredMap = originalMap.filter { it.value < 2 }
    val expectedMap = mapOf("key1" to 1)

    assertTrue { expectedMap == filteredMap }
}

When filtering we have a function that allows us to accumulate the results of our filters of different arrays. It is called filterTo and takes a mutable list copy to a given mutable array.

This allows us to take several collections and filter them into a single, accumulative collection.

This example takes; an array, a sequence, and a list.

It then applies the same predicate to all three to filter the prime numbers contained in each collection:

@Test
fun whenFilteringToAccumulativeList_thenListContainsAllContents() {
    val array1 = arrayOf(90, 92, 93, 94, 92, 95, 93)
    val array2 = sequenceOf(51, 31, 83, 674_506_111, 256_203_161, 15_485_863)
    val list1 = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
    val primes = mutableListOf<Int>()
    
    val expected = listOf(2, 3, 5, 7, 31, 83, 15_485_863, 256_203_161, 674_506_111)

    val primeCheck = { num: Int -> Primes.isPrime(num) }

    array1.filterTo(primes, primeCheck)
    list1.filterTo(primes, primeCheck)
    array2.filterTo(primes, primeCheck)

    primes.sort()

    assertIterableEquals(expected, primes)
}

Filters with or without predicate also work well with Maps:

val originalMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3)
val filteredMap = originalMap.filter { it.value < 2 }

A very beneficial pair of filter methods is filterNotNull and filterNotNullTo which will just filter out all null elements.

Lastly, if we ever need to use the index of the collection item, filterIndexed and filterIndexedTo provide the ability to use a predicate lambda with both the element and its position index.

5. Slice

We may also use a range to perform slicing. To perform a slice, we just define a Range that our slice wants to extract:

@Test
fun whenSlicingAnArrayWithDotRange_ThenListEqualsTheSlice() {
    val original = arrayOf(1, 2, 3, 2, 1)
    val actual = original.slice(3 downTo 1)
    val expected = listOf(2, 3, 2)

    assertIterableEquals(expected, actual)
}

The slice can go either upwards or down.

When using Ranges, we may also set the range step size.

Using a range without steps and slicing beyond the bounds of a collection we will create many null objects in our result List.

However, stepping beyond the bounds of a collection using a Range with steps can trigger an ArrayIndexOutOfBoundsException:

@Test
fun whenSlicingBeyondRangeOfArrayWithStep_thenOutOfBoundsException() {
    assertThrows(ArrayIndexOutOfBoundsException::class.java) {
        val original = arrayOf(12, 3, 34, 4)
        original.slice(3..8 step 2)
    }
}

6. Distinct

Another filter we’re going to look at in this article is distinct. We can use this method to collect unique objects from our list:

@Test
fun whenApplyingDistinct_thenReturnListOfNoDuplicateValues() {
    val array = arrayOf(1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 5, 6, 7, 8, 9)
    val result = array.distinct()
    val expected = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)

    assertIterableEquals(expected, result)
}

We also have the option of using a selector function. The selector returns the value we’re going to evaluate for uniqueness.

We’ll implement a small data class called SmallClass to explore working with an object within the selector:

data class SmallClass(val key: String, val num: Int)

using an array of SmallClass:

val original = arrayOf(
  SmallClass("key1", 1),
  SmallClass("key2", 2),
  SmallClass("key3", 3),
  SmallClass("key4", 3),
  SmallClass("er", 9),
  SmallClass("er", 10),
  SmallClass("er", 11))

We can use various fields within the distinctBy:

val actual = original.distinctBy { it.key }
val expected = listOf(
  SmallClass("key1", 1),
  SmallClass("key2", 2),
  SmallClass("key3", 3),
  SmallClass("key4", 3),
  SmallClass("er", 9))

The function doesn’t need to directly return a variable property, we can also perform calculations to determine our distinct values.

For example, to numbers for each 10 range (0 – 9, 10 – 19, 20-29, etc.), we can round down to the nearest 10, and that is the value that our selector:

val actual = array.distinctBy { Math.floor(it.num / 10.0) }

7. Chunked

One interesting feature of Kotlin 1.2 is chunked. Chunking is taking a single Iterable collection and creating a new List of chunks matching the defined size. *This doesn’t work with Arrays; only Iterables*.

We can chunk with either simply a size of the chunk to extract:

@Test
fun givenDNAFragmentString_whenChunking_thenProduceListOfChunks() {
    val dnaFragment = "ATTCGCGGCCGCCAA"

    val fragments = dnaFragment.chunked(3)

    assertIterableEquals(listOf("ATT", "CGC", "GGC", "CGC", "CAA"), fragments)
}

Or a size and a transformer:

@Test
fun givenDNAString_whenChunkingWithTransformer_thenProduceTransformedList() {
    val codonTable = mapOf(
      "ATT" to "Isoleucine", 
      "CAA" to "Glutamine", 
      "CGC" to "Arginine", 
      "GGC" to "Glycine")
    val dnaFragment = "ATTCGCGGCCGCCAA"

    val proteins = dnaFragment.chunked(3) { codon ->
        codonTable[codon.toString()] ?: error("Unknown codon")
    }

    assertIterableEquals(listOf(
      "Isoleucine", "Arginine", 
      "Glycine", "Arginine", "Glutamine"), proteins)
}

The above selector example of DNA Fragments is extracted from the Kotlin documentation on chunked available here.

When passing chunked a size that is not a divisor of our collection size. In those instances, the last element in our list of chunks will simply be a smaller list.

Be careful not to assume that every chunk is full size and encounter an ArrayIndexOutOfBoundsException!

8. Conclusion

All of the Kotlin filters allow us to apply lambdas to determine whether an item should be filtered or not. Not all of these functions can be used on Maps, however, all filter functions that work on Maps will work on Arrays.

The Kotlin collections documentation gives us information on whether we can use a filter function on only arrays or both. The documentation can be found here.

As always, all of the examples are available over on GitHub.