Chapter 5. Collections

As in Java, Kotlin uses typed collections to hold multiple objects. Unlike Java, Kotlin adds many interesting methods directly to the collection classes, rather than going through a stream intermediary.

Recipes in this chapter discuss ways to process both arrays and collections, ranging from sorting and searching, to providing read-only views, to accessing windows of data, and more.

5.1 Working with Arrays

Problem

You want to create and populate arrays in Kotlin.

Solution

Use the arrayOf function to create them, and the properties and methods inside the Array class to work with the contained values.

Discussion

Virtually every programming language has arrays, and Kotlin is no exception. This book focuses on Kotlin running on the JVM, and in Java arrays are handled a bit differently than they are in Kotlin. In Java you instantiate an array using the keyword new and dimensioning the array, as in Example 5-1.

Example 5-1. Instantiating an array in Java
String[] strings = new String[4];
strings[0] = "an";
strings[1] = "array";
strings[2] = "of";
strings[3] = "strings";

// or, more easily,
strings = "an array of strings".split(" ");

Kotlin provides a simple factory method called arrayOf for creating arrays, and while it uses the same syntax for accessing elements, in Kotlin Array is a class. Example 5-2 shows how the factory method works.

Example 5-2. Using the arrayOf factory method
val strings = arrayOf("this", "is", "an", "array", "of", "strings")

You can also use the factory method arrayOfNulls to create (as you might guess) an array containing only nulls, as in Example 5-3.

Example 5-3. Creating an array of nulls
val nullStringArray = arrayOfNulls<String>(5)

It’s interesting that even though the array contains only null values, you still have to choose a particular data type for it. After all, it may not contain nulls forever, and the compiler needs to know what type of reference you plan to add to it. The factory method emptyArray works the same way.

There is only one public constructor in the Array class. It takes two arguments:

  • size of type Int

  • init, a lambda of type (Int) -> T

The lambda is invoked on each index when creating the array. For example, to create an array of strings containing the first five integers squared, see Example 5-4.

Example 5-4. Array of strings containing the squares of 0 through 4
val squares = Array(5) { i -> (i * i).toString() }  1
1

Resulting array is {"0", "1", "4", "9", "16"}

The Array class declares public operator methods get and set, which are invoked when you access elements of the array using square brackets, as in squares[1].

Kotlin has specialized classes to represent arrays of primitive types to avoid the cost of autoboxing and unboxing. The functions booleanArrayOf, byteArrayOf, shortArrayOf, charArrayOf, intArrayOf, longArrayOf, floatArrayOf, and doubleArrayOf create the associated types (BooleanArray, ByteArray, ShortArray, etc.) exactly the way you would expect.

Tip

Even though Kotlin doesn’t have explicit primitives, the generated bytecodes use Java wrapper classes like Integer and Double when the values are nullable, and primitive types like int and double if not.

Many of the extension methods on arrays are the same as their counterparts on collections, which are discussed in the rest of this chapter. A couple are unique to arrays, however. For example, if you want to know the valid index values for a given array, use the property indices, as in Example 5-5.

Example 5-5. Getting the valid index values from an array
@Test
fun `valid indices`() {
    val strings = arrayOf("this", "is", "an", "array", "of", "strings")
    val indices = strings.indices
    assertThat(indices, contains(0, 1, 2, 3, 4, 5))
}

Normally you iterate over an array using the standard for-in loop, but if you want the index values as well, use the function withIndex.

fun <T> Array<out T>.withIndex(): Iterable<IndexedValue<T>>

data class IndexedValue<out T>(public val index: Int,
                               public val value: T)

The class IndexedValue is the data class shown, with properties called index and value. Use it as shown in Example 5-6.

Example 5-6. Accessing array values using withIndex
@Test
fun `withIndex returns IndexValues`() {
    val strings = arrayOf("this", "is", "an", "array", "of", "strings")
    for ((index, value) in strings.withIndex()) {      1
        println("Index $index maps to $value")         2
        assertTrue(index in 0..5)
    }
}
1

Call withIndex

2

Access individual indices and values

The results printed to standard output are:

Index 0 maps to this
Index 1 maps to is
Index 2 maps to an
Index 3 maps to array
Index 4 maps to of
Index 5 maps to strings

In general, Kotlin arrays behave the same way arrays in other languages do.

5.2 Creating Collections

Problem

You want to generate a list, set, or map.

Solution

Use one of the functions designed to produce either an unmodifiable collection, like listOf, setOf, and mapOf, or their mutable equivalents, mutableListOf, mutableSetOf, and mutableMapOf.

Discussion

If you want an immutable view of a collection, the kotlin.collections package provides a series of utility functions for doing so.

One example is listOf(vararg elements: T): List<T>, whose implementation is shown in Example 5-7.

Example 5-7. Implementation of the listOf function
public fun <T> listOf(vararg elements: T): List<T> =
    if (elements.size > 0) elements.asList() else emptyList()

The referenced asList function is an extension function on Array that returns a List that wraps the specified array. The resulting list is called immutable, but should more properly be considered read-only: you cannot add to nor remove elements from it, but if the contained objects are mutable, the list will appear to change.

Note

The implementation of asList delegates to Java’s Arrays.asList, which returns a read-only list.

Similar functions in the same package include the following:

  • listOf

  • setOf

  • mapOf

Example 5-8 shows how to create lists and sets.

Example 5-8. Creating “immutable” lists, sets, and maps
var numList = listOf(3, 1, 4, 1, 5, 9)                1
var numSet = setOf(3, 1, 4, 1, 5, 9)                  2
// numSet.size == 5                                   3
var map = mapOf(1 to "one", 2 to "two", 3 to "three") 4
1

Creates an unmodifiable list

2

Creates an unmodifiable set

3

Set does not contain duplicates

4

Creates a map from Pair instances

By default, Kotlin collections are “immutable,” in the sense that they do not support methods for adding or removing elements. If the elements themselves can be modified, the collection can appear to change, but only read-only operations are supported on the collection itself.

Methods to modify collections are in the “mutable” interfaces, provided by the factory methods:

  • mutableListOf

  • mutableSetOf

  • mutableMapOf

Example 5-9 shows the analogous mutable examples.

Example 5-9. Creating mutable lists, sets, and maps
var numList = mutableListOf(3, 1, 4, 1, 5, 9)
var numSet = mutableSetOf(3, 1, 4, 1, 5, 9)
var map = mutableMapOf(1 to "one", 2 to "two", 3 to "three")

The implementation of the mapOf function in the standard library is shown here:

public fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V> =
    if (pairs.size > 0)
        pairs.toMap(LinkedHashMap(mapCapacity(pairs.size)))
    else emptyMap()

The argument to the mapOf function is a variable argument list of Pair instances, so the infix to operator function is used to create the map entries. A similar function is used to create mutable maps.

You can also instantiate classes that implement the List, Set, or Map interfaces directly, as shown in Example 5-10.

Example 5-10. Instantiating a linked list
@Test
internal fun `instantiating a linked list`() {
    val list = LinkedList<Int>()
    list.add(3)                            1
    list.add(1)
    list.addLast(999)                      1
    list[2] = 4                            2
    list.addAll(listOf(1, 5, 9, 2, 6, 5))
    assertThat(list, contains(3, 1, 4, 1, 5, 9, 2, 6, 5))
}
1

The add method is an alias for addLast

2

Array-type access invokes get or set

5.3 Creating Read-Only Views from Existing Collections

Problem

You have an existing mutable list, set, or map, and you want to create a read-only version of it.

Solution

To make a new, read-only collection, use the toList, toSet, or toMap methods. To make a read-only view on an existing collection, assign it to a variable of type List, Set, or Map.

Discussion

Consider a mutable list created with the mutableList factory method. The resulting list has methods like add, remove, and so on that allow the list to grow or shrink as desired:

val mutableNums = mutableListOf(3, 1, 4, 1, 5, 9)

There are two ways to create a read-only version of a mutable list. The first is to invoke the toList method, which returns a reference of type List:

@Test
fun `toList on mutableList makes a readOnly new list`() {
    val readOnlyNumList: List<Int> = mutableNums.toList() 1
    assertEquals(mutableNums, readOnlyNumList)
    assertNotSame(mutableNums, readOnlyNumList)
}
1

Explicit type shows result is a List<T>

The test shows that the return type from the toList method is List<T>, which represents an immutable list, so methods like add or remove are not available. The rest of the test shows that the method is creating a separate object, however, so while it has the same contents as the original, it doesn’t represent the same objects anymore:

@Test
internal fun `modify mutable list does not change read-only list`() {
    val readOnly: List<Int> = mutableNums.toList()
    assertEquals(mutableNums, readOnly)

    mutableNums.add(2)
    assertThat(readOnly, not(contains(2)))
}

If you want a read-only view of the same contents, assign the mutable list to a reference of type List, as shown in Example 5-11.

Example 5-11. Creating a read-only view of the mutable list
@Test
internal fun `read-only view of a mutable list`() {
    val readOnlySameList: List<Int> = mutableNums 1
    assertEquals(mutableNums, readOnlySameList)
    assertSame(mutableNums, readOnlySameList)

    mutableNums.add(2)
    assertEquals(mutableNums, readOnlySameList)
    assertSame(mutableNums, readOnlySameList)     2
}
1

Assigns mutable to reference of type List

2

Still the same underlying object

This time, the mutable list is assigned to a reference of type List. Not only is the result still the same object, but if the underlying mutable list is modified, the read-only view shows the updated values. You can’t modify the list from the read-only reference, but it is attached to the same object as the original.

As you might expect, the toSet and toMap functions work the same way, as does assigning mutable sets and maps to references of type Set or Map.

5.4 Building a Map from a Collection

Problem

You have a list of keys and want to build a map by associating each key with a generated value.

Solution

Use the associateWith function by supplying a lambda to be executed for each key.

Discussion

Say you have a set of keys and want to map each of them to a generated value. One way to do that is to use the associate function, as in Example 5-12.

Example 5-12. Using associate to generate values
val keys = 'a'..'f'
val map = keys.associate { it to it.toString().repeat(5).capitalize() }
println(map)

Executing this snippet results in the following:

{a=Aaaaa, b=Bbbbb, c=Ccccc, d=Ddddd, e=Eeeee}

The associate function is an inline extension function on Iterable<T> that takes a lambda that transforms T into a Pair<K,V>. In this example, the to function is an infix function that produces a Pair from the left- and right-side arguments.

This works, but in Kotlin 1.3 a new function was added called associateWith that simplifies the code. Example 5-13 shows the previous code reworked with associateWith.

Example 5-13. Using associateWith to generate values
val keys = 'a'..'f'
val map = keys.associateWith { it.toString().repeat(5).capitalize() }
println(map)

The result is the same, but the argument now is a function that produces a String value rather than a Pair<Char, String>.

Both examples produce the same result, but the associateWith function is slightly simpler to write and understand.

5.5 Returning a Default When a Collection Is Empty

Problem

As you are processing a collection, you filter out all the elements but want to return a default response.

Solution

Use the ifEmpty and ifBlank functions to return a default when a collection is empty or a string is blank.

Discussion

Say you have a data class called Product that wraps a name, a price, and a boolean field to indicate whether the product is on sale, as in Example 5-14.

Example 5-14. Data class for a product
data class Product(val name: String,
                   var price: Double,
                   var onSale: Boolean = false)

If you have a list of products and you want the names of the products that are on sale, you could do a simple filtering operation, as follows:

fun namesOfProductsOnSale(products: List<Product>) =
    products.filter { it.onSale }
        .map { it.name }
        .joinToString(separator = ", ")

The idea is to take a list of products, filter them by the boolean onSale property, and map them to just the names, which then are joined into a single string. The problem is that if no products are on sale, the filter will return an empty collection, which will then be converted into an empty string.

If you would rather return a specific string when the result is empty, you can use a function called ifEmpty on both Collection and String. Example 5-15 shows how to use either one.

Example 5-15. Using ifEmpty on Collection and String
fun onSaleProducts_ifEmptyCollection(products: List<Product>) =
    products.filter { it.onSale }
        .map { it.name }
        .ifEmpty { listOf("none") }          1
        .joinToString(separator = ", ")

fun onSaleProducts_ifEmptyString(products: List<Product>) =
        products.filter { it.onSale }
            .map { it.name }
            .joinToString(separator = ", ")
            .ifEmpty { "none" }              2
1

Supplies default on empty collection

2

Supplies default on empty string

In either case, a collection of products that are not on sale will return the string "none" as shown in the tests in Example 5-16.

Example 5-16. Testing products
class IfEmptyOrBlankKtTest {
    private val overthruster = Product("Oscillation Overthruster", 1_000_000.0)
    private val fluxcapacitor = Product("Flux Capacitor", 299_999.95, onSale = true)
    private val tpsReportCoverSheet = Product("TPS Report Cover Sheet", 0.25)

    @Test
    fun productsOnSale() {
        val products = listOf(overthruster, fluxcapacitor, tpsReportCoverSheet)

        assertAll( "On sale products",
            { assertEquals("Flux Capacitor",
                onSaleProducts_ifEmptyCollection(products)) },
            { assertEquals("Flux Capacitor",
                onSaleProducts_ifEmptyString(products)) })
    }

    @Test
    fun productsNotOnSale() {
        val products = listOf(overthruster, tpsReportCoverSheet)

        assertAll( "No products on sale",
            { assertEquals("none", onSaleProducts_ifEmptyCollection(products)) },
            { assertEquals("none", onSaleProducts_ifEmptyString(products)) })
    }
}

Java in version 8 added a class called Optional<T>, which is often used as a return type wrapper when a query may legitimately return a null or empty value. Kotlin supports this as well, but it’s easy enough to return a specific value instead by using the ifEmpty function.

5.6 Restricting a Value to a Given Range

Problem

Given a value, you want to return it if it is contained in a specified range, or return the minimum or maximum of the range if not.

Solution

Use the coerceIn function on ranges, either with a range argument or specified min and max values.

Discussion

There are two overloads of the coerceIn function for ranges: one that takes a closed range as an argument, and one that takes min and max values.

For the first variation, consider an integer range from 3 to 8, inclusive. The test in Example 5-17 shows that coerceIn returns the value if it is contained in the range, or the boundaries if not.

Example 5-17. Coercing a value into a range
@Test
fun `coerceIn given a range`() {
    val range = 3..8

    assertThat(5, `is`(5.coerceIn(range)))
    assertThat(range.start, `is`(1.coerceIn(range)))        1
    assertThat(range.endInclusive, `is`(9.coerceIn(range))) 2
}
1

range.start is 3

2

range.endInclusive is 8

Likewise, if you have the min and max values you want, you don’t have to create a range to use the coerceIn function, as Example 5-18 shows.

Example 5-18. Coercing a value with a min and max
@Test
fun `coerceIn given min and max`() {
    val min = 2
    val max = 6

    assertThat(5, `is`(5.coerceIn(min, max)))
    assertThat(min, `is`(1.coerceIn(min, max)))
    assertThat(max, `is`(9.coerceIn(min, max)))
}

This version returns the value if it is between min and max, and the boundary values if not.

5.7 Processing a Window on a Collection

Problem

Given a collection of values, you want to process them by using a small window that traverses the collection.

Solution

Use the chunked function if you want to divide the collection into equal parts, or the windowed function if you want a block that slides along the collection by a given interval.

Discussion

Given an iterable collection, the chunked function splits it into a list of lists, where each has the given size or smaller. The function can return the list of lists, or you can also supply a transformation to apply to the resulting lists. The signatures of the chunked function are as follows:

fun <T> Iterable<T>.chunked(size: Int): List<List<T>>

fun <T, R> Iterable<T>.chunked(
    size: Int,
    transform: (List<T>) -> R
): List<R>

This all sounds more complicated than it is in practice. For example, consider a simple range of integers from 0 to 10. The test in Example 5-19 breaks it into groups of three consecutive numbers, or computes their sums or averages.

Example 5-19. Breaking a list into sections and processing them
@Test
internal fun chunked() {
    val range = 0..10

    val chunked = range.chunked(3)
    assertThat(chunked, contains(listOf(0, 1, 2), listOf(3, 4, 5),
        listOf(6, 7, 8), listOf(9, 10)))

    assertThat(range.chunked(3) { it.sum() }, `is`(listOf(3, 12, 21, 19)))
    assertThat(range.chunked(3) { it.average() }, `is`(listOf(1.0, 4.0, 7.0, 9.5)))
}

The first call simply returns the List<List<Int>> consisting of [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]. The second and third calls provide a lambda to compute the sums of each list or the average of each list, respectively.

The chunked function is actually a special case of the windowed function. Example 5-20 shows how the implementation of chunked delegates to windowed.

Example 5-20. Implementation of chunked in the standard library
public fun <T> Iterable<T>.chunked(size: Int): List<List<T>> {
    return windowed(size, size, partialWindows = true)
}

The windowed function takes three arguments, two of which are optional:

size

The number of elements in each window

step

The number of elements to move forward on each step (defaults to 1)

partialWindows

A boolean that defaults to false and tells whether to keep the last section if it doesn’t have the required number of elements

The chunked function calls windowed with both the size and step parameters equal to the chunked argument, so it moves the window forward by exactly that size each time. You can use windowed directly, however, to change that.

An example is to compute a moving average. Example 5-21 shows how to use windowed both to behave the same way as chunked and to move the window forward by only one element each time.

Example 5-21. Computing a moving average in each window
@Test
fun windowed() {
    val range = 0..10

    assertThat(range.windowed(3, 3),
        contains(listOf(0, 1, 2), listOf(3, 4, 5), listOf(6, 7, 8)))

    assertThat(range.windowed(3, 3) { it.average() },
        contains(1.0, 4.0, 7.0))

    assertThat(range.windowed(3, 1),
        contains(
            listOf(0, 1, 2), listOf(1, 2, 3), listOf(2, 3, 4),
            listOf(3, 4, 5), listOf(4, 5, 6), listOf(5, 6, 7),
            listOf(6, 7, 8), listOf(7, 8, 9), listOf(8, 9, 10)))

    assertThat(range.windowed(3, 1) { it.average() },
        contains(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0)

The chunked and windowed functions are useful for processing time-series data in stages.

5.8 Destructuring Lists

Problem

You want to use destructuring to access elements of a list.

Solution

Assign the list to a group of at most five elements.

Discussion

Destructuring is the process of extracting values from an object by assigning them to a collection of variables.

Example 5-22 shows how you can assign the first few elements of a list to defined variables in one step.

Example 5-22. Destructuring elements from a list
val list = listOf("a", "b", "c", "d", "e", "f", "g")
val (a, b, c, d, e) = list
println("$a $b $c $d $e")

This code prints the string a b c d e, because the first five elements of the created list are assigned to the variables of the same name. This works because the List class has extension functions defined in the standard library called componentN, where N goes from 1 to 5, as shown in Example 5-23.

Example 5-23. The component1 extension function on List (from the standard library)
/**
 * Returns 1st *element* from the collection.
 */
@kotlin.internal.InlineOnly
public inline operator fun <T> List<T>.component1(): T {
    return get(0)
}

Destructuring relies on the existence of componentN functions. The List class contains implementations for component1, component2, component3, component4, and component5, so the preceding code works.

Data classes automatically add the associated component methods for all of their defined attributes. If you define your own class (and don’t make it a data class), you can manually define any needed component methods as well.

Destructuring is a convenient way to extract multiple elements from an object. At the moment, the List class defines component functions for the first five elements. That may change in later Kotlin versions.

5.9 Sorting by Multiple Properties

Problem

You want to sort a class by one property, and then equal values by a second property, and so on.

Solution

Use the sortedWith and compareBy functions.

Discussion

Say we have a simple data class called Golfer, with a sample collection shown in Example 5-24.

Example 5-24. A data class and some sample data
data class Golfer(val score: Int, val first: String, val last: String)

val golfers = listOf(
    Golfer(70, "Jack", "Nicklaus"),
    Golfer(68, "Tom", "Watson"),
    Golfer(68, "Bubba", "Watson"),
    Golfer(70, "Tiger", "Woods"),
    Golfer(68, "Ty", "Webb")
)

If you would like to sort the golfers by score, then sort equal scores by last name, and finally sort those equal scores and last names by first name, you can use the code in Example 5-25.

Example 5-25. Sorting the golfers by successive properties
val sorted = golfers.sortedWith(
    compareBy({ it.score }, { it.last }, { it.first })
)

sorted.forEach { println(it) }

The result is as follows:

Golfer(score=68, first=Bubba, last=Watson)
Golfer(score=68, first=Tom, last=Watson)
Golfer(score=68, first=Ty, last=Webb)
Golfer(score=70, first=Jack, last=Nicklaus)
Golfer(score=70, first=Tiger, last=Woods)

The three golfers who shot a 68 appear before the two who scored 70. Within the 68s, both Watsons appear ahead of Webb, and in the 70s, Nicklaus is before Woods. For the golfers named Watson who both scored 68s, Bubba appears before Tom.

The full signatures of the sortedWith and compareBy functions are given by Example 5-26.

Example 5-26. The signature of sortedWith in the standard library
fun <T> Iterable<T>.sortedWith(
    comparator: Comparator<in T>
): List<T>

fun <T> compareBy(
    vararg selectors: (T) -> Comparable<*>?
): Comparator<T>

So the sortedWith function takes a Comparator, and the compareBy function produces a Comparator. What’s interesting about compareBy is that you can provide a list of selectors, each of which extracts a Comparable property (note that the property’s class has to implement the Comparable interface), and the function will create a Comparator that sorts by them in turn.

Note

The sortBy and sortWith functions sort their elements in place, and therefore require mutable collections.

Another way to solve the same problem is to build the Comparator by using the thenBy function, which applies a comparison after the previous one. The same collection is sorted in this manner in Example 5-27.

Example 5-27. Chaining comparators together
val comparator = compareBy<Golfer>(Golfer::score)
    .thenBy(Golfer::last)
    .thenBy(Golfer::first)

golfers.sortedWith(comparator)
    .forEach(::println)

The result is the same as in the previous example.

5.10 Defining Your Own Iterator

Problem

You have a class that wraps a collection and you would like to iterate over it easily.

Solution

Define an operator function that returns an iterator, which implements both a next and a hasNext function.

Discussion

The Iterator design pattern has an implementation in Java by defining the Iterator interface. Example 5-28 provides the corresponding definition in Kotlin.

Example 5-28. Iterator interface in kotlin.collections
interface Iterator<out T> {
    operator fun next(): T
    operator fun hasNext(): Boolean
}

In Java, the for-each loop lets you iterate over any class that implements Iterable. In Kotlin, a similar constraint works on the for-in loop. Consider a data class called Player and a class called Team, as given in Example 5-29.

Example 5-29. Player and Team classes
data class Player(val name: String)
class Team(val name: String,
           val players: MutableList<Player> = mutableListOf()) {

    fun addPlayers(vararg people: Player) =
        players.addAll(people)

    // ... other functions as needed ...
}

A Team contains a mutable list of Player instances. If you have a team with several players and you want to iterate over the players, you need to access the players property, as in Example 5-30.

Example 5-30. Iterating over a team of players
val team = Team("Warriors")
team.addPlayers(Player("Curry"), Player("Thompson"),
    Player("Durant"), Player("Green"), Player("Cousins"))

for (player in team.players) { 1
    println(player)
}
1

Accesses players property in loop

This can be (slightly) simplified by defining an operator function called iterator on the Team. Example 5-31 shows how to do this as an extension function, and the resulting simplified loop.

Example 5-31. Iterating over the team directly
operator fun Team.iterator() : Iterator<Player> = players.iterator()

for (player in team) { 1
    println(player)
}
1

Can iterate over team

Either way, the output is as follows:

Player(name=Curry)
Player(name=Thompson)
Player(name=Durant)
Player(name=Green)
Player(name=Cousins)

In reality, the idea is to make the Team class implement the Iterable interface, which includes the abstract operator function iterator. That means the alternative to writing the extension function is to modify Team, as shown in Example 5-32.

Example 5-32. Implementing the Iterable interface
class Team(val name: String,
           val players: MutableList<Player> = mutableListOf()) : Iterable<Player> {

    override fun iterator(): Iterator<Player> =
        players.iterator()

    // ... other functions as needed ...
}

The result is the same, except that now all the extension functions on Iterable are available on Team, so you can write code like that in Example 5-33.

Example 5-33. Using Iterator extension functions on Team
assertEquals("Cousins, Curry, Durant, Green, Thompson",
    team.map { it.name }.joinToString())

The map function here iterates over the players, so it.name represents each player’s name. Other extension functions can be used in the same way.

5.11 Filtering a Collection by Type

Problem

You want to create a new collection of elements of a specified type from an existing group of mixed types.

Solution

Use the extension functions filterIsInstance or filterIsInstanceTo.

Discussion

Collections in Kotlin include an extension function called filter that takes a predicate, which can be used to extract elements satisfying any boolean condition, as in Example 5-34.

Example 5-34. Filtering a collection by type, with erasure
val list = listOf("a", LocalDate.now(), 3, 1, 4, "b")
val strings = list.filter { it is String }

for (s in strings) {
    // s.length  // does not compile; type is erased
}

Although the filtering operation works, the inferred type of the strings variable is List<Any>, so Kotlin does not smart cast the individual elements to type String.

You could add an is check or simply use the filterIsInstance function instead, as in Example 5-35.

Example 5-35. Using reified types
val list = listOf("a", LocalDate.now(), 3, 1, 4, "b")

val all = list.filterIsInstance<Any>()
val strings = list.filterIsInstance<String>()
val ints = list.filterIsInstance<Int>()
val dates = list.filterIsInstance(LocalDate::class.java)

assertThat(all, `is`(list))
assertThat(strings, containsInAnyOrder("a", "b"))
assertThat(ints, containsInAnyOrder(1, 3, 4))
assertThat(dates, contains(LocalDate.now()))

In this case, the filterIsInstance function uses reified types, so the resulting collections are of a known type, and you don’t have to check the type before using its properties. The implementation of the filterIsInstance function in the library is shown here:

public inline fun <reified R> Iterable<*>.filterIsInstance(): List<R> {
    return filterIsInstanceTo(ArrayList<R>())
}

The reified keyword applied to an inline function preserves the type, so the returned type is List<R>.

The implementation calls the function filterIsInstanceTo, which takes a collection argument of a particular type and populates it with elements of that type from the original. That function can also be used directly, as in Example 5-36.

Example 5-36. Using reified types to populate a provided list
val list = listOf("a", LocalDate.now(), 3, 1, 4, "b")

val all = list.filterIsInstanceTo(mutableListOf())
val strings = list.filterIsInstanceTo(mutableListOf<String>())
val ints = list.filterIsInstanceTo(mutableListOf<Int>())
val dates = list.filterIsInstanceTo(mutableListOf<LocalDate>())

assertThat(all, `is`(list))
assertThat(strings, containsInAnyOrder("a", "b"))
assertThat(ints, containsInAnyOrder(1, 3, 4))
assertThat(dates, contains(LocalDate.now()))

The argument to the filterIsInstanceTo function is a MutableCollection<in R>, so by specifying the type of the desired collection, you populate it with the instances of that type.

5.12 Making a Range into a Progression

Problem

You want to iterate over a range, but the range does not contain simple integers, characters, or longs.

Solution

Create a progression of your own.

Discussion

In Kotlin, a range is created when you use the double dot operator, as in 1..5, which instantiates an IntRange. A range is a closed interval, defined by two endpoints that are both included in the range.

The standard library adds an extension function called rangeTo to any generic type T that implements the Comparable interface. Example 5-37 provides its implementation.

Example 5-37. Implementation of the rangeTo function for Comparable types
operator fun <T : Comparable<T>> T.rangeTo(that: T): ClosedRange<T> =
    ComparableRange(this, that)

The class ComparableRange simply extends Comparable, defines start and endInclusive properties of type T, and overrides equals, hashCode, and toString functions appropriately. The return type on rangeTo is ClosedRange, which is a simple interface defined in Example 5-38.

Example 5-38. The ClosedRange interface
interface ClosedRange<T: Comparable<T>> {
    val start: T
    val endInclusive: T
    operator fun contains(value: T): Boolean =
        value >= start && value <= endInclusive
    fun isEmpty(): Boolean = start > endInclusive
}

The operator function contains lets you use the in infix function to check whether a value is contained inside the range.

All this means that you can create a range based on any class that implements Comparable, and the infrastructure to support it is already there. As an example, for java.time.LocalDate, see Example 5-39.

Example 5-39. Using LocalDate in a range
@Test
fun `LocalDate in a range`() {
    val startDate = LocalDate.now()
    val midDate = startDate.plusDays(3)
    val endDate = startDate.plusDays(5)

    val dateRange = startDate..endDate

    assertAll(
        { assertTrue(startDate in dateRange) },
        { assertTrue(midDate in dateRange) },
        { assertTrue(endDate in dateRange) },
        { assertTrue(startDate.minusDays(1) !in dateRange) },
        { assertTrue(endDate.plusDays(1) !in dateRange) }
    )
}

That’s all well and good, but the surprising part comes when you try to iterate over the range:

for (date in dateRange) println(it)         // compiler error!
(startDate..endDate).forEach { /* ... */ }  // compiler error!

The problem is that a range is not a progression. A progression is simply an ordered sequence of values. Custom progressions implement the Iterable interface, just as the existing progressions IntProgression, LongProgression, and CharProgression in the standard library do.

To demonstrate how to create a progression, consider the classes in Example 5-40 and Example 5-41.

Note

The code in this example is based on the 2017 DZone article by Grzegorz Ziemoński entitled, “What Are Kotlin Progressions and Why Should You Care?”

First, here is the LocalDateProgression class, which implements both Iterable<LocalDate> and ClosedRange<LocalDate> interfaces.

Example 5-40. A progression for LocalDate
import java.time.LocalDate

class LocalDateProgression(
    override val start: LocalDate,
    override val endInclusive: LocalDate,
    val step: Long = 1
) : Iterable<LocalDate>, ClosedRange<LocalDate> {

    override fun iterator(): Iterator<LocalDate> =
        LocalDateProgressionIterator(start, endInclusive, step)

    infix fun step(days: Long) = LocalDateProgression(start, endInclusive, days)
}

From the Iterator interface, the only function that must be implemented is iterator. Here it instantiates the class LocalDateProgressionIterator, shown next. The infix step function instantiates the class with the proper increment in days. The ClosedRange interface, as shown in the preceding code, defines the start and endInclusive properties, so they are overridden here in the primary constructor.

Example 5-41. The iterator for the LocalDateProgression class
import java.time.LocalDate

internal class LocalDateProgressionIterator(
    start: LocalDate,
    val endInclusive: LocalDate,
    val step: Long
) : Iterator<LocalDate> {

    private var current = start

    override fun hasNext() = current <= endInclusive

    override fun next(): LocalDate {
        val next = current
        current = current.plusDays(step)
        return next
    }
}

The Iterator interface requires overriding next and hasNext, as shown.

Finally, use an extension function to redefine the rangeTo function to return an instance of the progression:

operator fun LocalDate.rangeTo(other: LocalDate) =
    LocalDateProgression(this, other)

Now LocalDate can be used to create a range that can be iterated over, as shown in the tests in Example 5-42.

Example 5-42. Tests for the LocalDate progression
@Test
fun `use LocalDate as a progression`() {
    val startDate = LocalDate.now()
    val endDate = startDate.plusDays(5)

    val dateRange = startDate..endDate
    dateRange.forEachIndexed { index, localDate ->
        assertEquals(localDate, startDate.plusDays(index.toLong()))
    }

    val dateList = dateRange.map { it.toString() }
    assertEquals(6, dateList.size)
}

@Test
fun `use LocalDate as a progression with a step`() {
    val startDate = LocalDate.now()
    val endDate = startDate.plusDays(5)

    val dateRange = startDate..endDate step 2
    dateRange.forEachIndexed { index, localDate ->
        assertEquals(localDate, startDate.plusDays(index.toLong() * 2))
    }

    val dateList = dateRange.map { it.toString() }
    assertEquals(3, dateList.size)
}

Using the double dot operator creates a range, which in this case supports iteration, which is used by the forEachIndexed function. In this example, creating a progression requires two classes and an extension function, but the pattern is easy enough to replicate for your own classes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.51.145