Lists

A List is a container—something that holds other objects.

Containers are also called collections. Lists are part of the standard Kotlin package so they’re available without any imports. In the following example, we create a List populated with Ints by calling the function listOf() with initialization values:

// Lists/Lists.kt import atomictest.eq fun main(args: Array<String>) { // Lists hold other objects: val ints = listOf(99, 3, 5, 7, 11, 13) ints eq "[99, 3, 5, 7, 11, 13]" // [1] // Select each element in the List: var result = "" for (i in ints) // [2] result += "$i " result eq "99 3 5 7 11 13 " // "[]" is "Indexing": ints[4] eq 11 // [3] }

Forgetting that indexing starts at zero is responsible for the so-called off-by-one error. If you try to use an index beyond the last element in the List, Kotlin will throw an exception to inform you that something has gone wrong. The exception will display an error message telling you it’s an IndexOutOfBoundsException so you can figure out what the problem is:

// Lists/OutOfBounds.kt import atomictest.* fun main(args: Array<String>) { val ints = listOf(1, 2, 3) capture { ints[3] } eq "ArrayIndexOutOfBoundsException: 3" }

In a language like Kotlin we often don’t select elements one at a time, but instead iterate through a whole container using in—an approach that eliminates off-by-one errors.

List can hold all different types. Here, we create a List of Doubles and a List of Strings:

// Lists/ListUsefulFunction.kt import atomictest.eq fun main(args: Array<String>) { val doubles = listOf(1.1, 2.2, 3.3, 4.4) doubles.min() eq 1.1 doubles.max() eq 4.4 val strings = listOf("Twas", "Brillig", "And", "Slithy", "Toves") strings eq listOf("Twas", "Brillig", "And", "Slithy", "Toves") strings.sorted() eq listOf("And", "Brillig", "Slithy", "Toves", "Twas") strings.reversed() eq listOf("Toves", "Slithy", "And", "Brillig", "Twas") strings.first() eq "Twas" strings.takeLast(2) eq listOf("Slithy", "Toves") }

This shows some of List’s operations. Note the name “sorted” instead of “sort.” When you call sorted() it produces a new List containing the same elements as the old, in sorted order—but it leaves the original List alone. Calling it “sort” implies that the original List is changed directly (a.k.a. sorted in place). Throughout Kotlin, you see this tendency of “leaving the original object alone and producing a new object.” reversed() produces a new List as well, ordered end to beginning.

Parameterized Types

We consider it a good idea to let Kotlin infer types whenever possible. It tends to make the code cleaner and easier to read. Sometimes, however, Kotlin can’t figure out what type to use (if so, it complains) and we must help. In other cases, you have some reason that you want to be explicit about the type, for readability’s sake. For example, here’s how we explicitly tell Kotlin the type contained in a List:

// Lists/ParameterizedTypes.kt import atomictest.eq fun main(args: Array<String>) { // Type is inferred: val numbers = listOf(1, 2, 3) val strings = listOf("one", "two", "three") // Exactly the same, but explicitly typed: val numbers2: List<Int> = listOf(1, 2, 3) val strings2: List<String> = listOf("one", "two", "three") numbers eq numbers2 strings eq strings2 }

The initialization values tell the Kotlin compiler that numbers contains a List of Ints, while strings contains a List of Strings, so it infers those types.

numbers2 and strings2 are explicitly-typed versions of numbers and strings. On the left sides we add colons and the type declarations List<Int> and List<String>. The angle brackets are new here; they denote a type parameter, allowing us to say, “the container holds objects of the type ‘parameter’.” You typically pronounce List<Int> as “list of Int.”

Type parameters are useful for elements other than containers, but you often see them with container-like objects. In this book we normally use List as a basic container.

Return values can also have type parameters:

// Lists/ParameterizedReturn.kt import atomictest.eq // Return type is inferred: fun inferred(c1: Char, c2: Char) = listOf(c1, c2) // Explicit return type: fun explicit(c1: Char, c2: Char): List<Char> = listOf(c1, c2) fun main(args: Array<String>) { inferred('a', 'b') eq "[a, b]" explicit('y', 'z') eq "[y, z]" }

Kotlin infers the return type for inferred(). The definition of explicit() specifies the function return type. You can’t just say it returns a List; Kotlin will complain, so you must give the type parameter as well. When you specify the return type of a function, Kotlin enforces your intention.

Read-only and Mutable Lists

If you don’t explicitly say you want a mutable List, you won’t get one. listOf() produces a read-only list without mutating functions.

If you’re creating a list gradually (you don’t have all the elements at creation time), call mutableListOf(). This produces a MutableList which can be modified:

// Lists/MutableList.kt import atomictest.eq fun main(args: Array<String>) { val list = mutableListOf<Int>() list.add(1) list.addAll(listOf(2, 3)) list += 4 list += listOf(5, 6) list eq listOf(1, 2, 3, 4, 5, 6) }

You can add elements to a MutableList using add() and addAll(), or the shortcut += which adds a single element or another collection. Because list is given no initial elements, we must tell the compiler what type it is with the <Int> specification.

A MutableList can be treated as a List, in which case it cannot be changed. You can’t, however, treat a read-only List as a MutableList:

// Lists/MutListIsList.kt import atomictest.eq fun getList(): List<Int> { return mutableListOf(1, 2, 3) } fun main(args: Array<String>) { // getList() produces a read-only List: val list = getList() // list += 3 // Error list eq listOf(1, 2, 3) }

Note that list lacks mutation functions despite being originally created using mutableListOf() inside getList(). As getList() returns the result, the type becomes a List<Int>. The original object is still a MutableList, but it is viewed through the lens of a List.

A List is read-only—you can read its contents but not write to it. If the underlying implementation is a MutableList and you retain a mutable reference to that implementation, you can still modify it via that mutable reference, and any read-only references will see those changes. This is another example of the problem of aliasing, which was introduced in Constraining Visibility:

// Lists/MultipleListRefs.kt import atomictest.eq fun main(args: Array<String>) { val first = mutableListOf(1) val second: List<Int> = first second eq listOf(1) first += 2 // second sees the change: second eq listOf(1, 2) }

first is an immutable reference (val) to the mutable object produced by mutableListOf(1). second is aliased to first. second is a reference to a read-only view of that same object; read-only because List<Int> does not include any modification functions. Note that, without the List<Int> type declaration, Kotlin would infer that second was also a reference to a mutable object.

We’re able to add one more element (2) to the object through the first reference, because first is a reference to a mutable list. Note that second observes these changes—it cannot change the list itself, even though the list does change.

Previous          Next

©2018 Mindview LLC. All Rights Reserved.