Lists

A List is a container, which is an object that holds other objects.

Containers are also called collections. When we need a basic container for the examples in this book, we normally use a List.

Lists are part of the standard Kotlin package so they don’t require an import.

The following example creates a List populated with Ints by calling the standard library function listOf() with initialization values:

// Lists/Lists.kt
import atomictest.eq

fun main() {
  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"

  // "Indexing" into the List:
  ints[4] eq 11                     // [3]
}
  • [1] A List uses square brackets when displaying itself.
  • [2] for loops work well with Lists: for(i in ints) means i receives each value in ints. You don’t declare val i or give its type; Kotlin knows from the context that i is a for loop identifier.
  • [3] Square brackets index into a List. A List keeps its elements in initialization order, and you select them individually by number. Like most programming languages, Kotlin starts indexing at element zero, which in this case produces the value 99. Thus an index of 4 produces the value 11.

Forgetting that indexing starts at zero produces the so-called off-by-one error. In a language like Kotlin we often don’t select elements one at a time, but instead iterate through an entire container using in. This eliminates off-by-one errors.

If you use an index beyond the last element in a List, Kotlin throws an ArrayIndexOutOfBoundsException:

// Lists/OutOfBounds.kt
import atomictest.*

fun main() {
  val ints = listOf(1, 2, 3)
  capture {
    ints[3]
  } contains
    listOf("ArrayIndexOutOfBoundsException")
}

A List can hold all different types. Here’s a List of Doubles and a List of Strings:

// Lists/ListUsefulFunction.kt
import atomictest.eq

fun main() {
  val doubles =
    listOf(1.1, 2.2, 3.3, 4.4)
  doubles.sum() eq 11.0

  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() also produces a new List.

Parameterized Types

We consider it good practice to use type inference—it tends to make the code cleaner and easier to read. Sometimes, however, Kotlin complains that it can’t figure out what type to use, and in other cases explicitness makes the code more understandable. Here’s how we tell Kotlin the type contained by a List:

// Lists/ParameterizedTypes.kt
import atomictest.eq

fun main() {
  // 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
}

Kotlin uses the initialization values to infer that numbers contains a List of Ints, while strings contains a List of Strings.

numbers2 and strings2 are explicitly-typed versions of numbers and strings, created by adding the type declarations List<Int> and List<String>. You haven’t seen angle brackets before—they denote a type parameter, allowing you to say, “this container holds ‘parameter’ objects.” We pronounce List<Int> as “List of Int.”

Type parameters are useful for components other than containers, but you often see them with container-like objects.

Return values can also have type parameters:

// Lists/ParameterizedReturn.kt
package lists
import atomictest.eq

// Return type is inferred:
fun inferred(p: Char, q: Char) =
  listOf(p, q)

// Explicit return type:
fun explicit(p: Char, q: Char): List<Char> =
  listOf(p, q)

fun main() {
  inferred('a', 'b') eq "[a, b]"
  explicit('y', 'z') eq "[y, z]"
}

Kotlin infers the return type for inferred(), while 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 that has no mutating functions.

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

// Lists/MutableList.kt
import atomictest.eq

fun main() {
  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)
}

Because list has no initial elements, we must tell Kotlin what type it is by providing the <Int> specification in the call to mutableListOf(). You can add elements to a MutableList using add() and addAll(), or the operator += which adds either a single element or another collection.

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
package lists
import atomictest.eq

fun makeList(): List<Int> =
  mutableListOf(1, 2, 3)

fun main() {
  // makeList() produces a read-only List:
  val list = makeList()
  // list.add(3) // Unresolved reference: add
  list eq listOf(1, 2, 3)
}

list lacks mutation functions despite being originally created using mutableListOf() inside makeList(). Notice that the result type of makeList() is 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 aliasing, introduced in Constraining Visibility:

// Lists/MultipleListRefs.kt
import atomictest.eq

fun main() {
  val first = mutableListOf(1)
  val second: List<Int> = first
  second eq listOf(1)
  first.add(2)
  // second sees the change:
  second eq listOf(1, 2)
}

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

We’re able to add an element (2) to the object because first is a reference to a mutable List. Note that second observes these changes—it cannot change the List although the List changes via first.

The += Puzzle

The += operator can give the appearance that an immutable List is actually mutable:

// Lists/ApparentlyMutableList.kt
import atomictest.eq

fun main() {
  var list = listOf('X') // Immutable
  list += 'Y' // Appears to be mutable
  list eq "[X, Y]"
}

listOf() produces an immutable List, but list += 'Y' seems to be modifying that List. Does += somehow violate immutability?

This only happens because list is a var. Here’s a more detailed example that shows the different combinations of mutable/immutable Lists with val/var:

// Lists/PlusAssignPuzzle.kt
import atomictest.eq

fun main() {
    // Mutable List assigned to a 'val'/'var':
    val list1 = mutableListOf('A') // or 'var'
    list1 += 'A' // Is the same as:
    list1.plusAssign('A')               // [1]

    // Immutable List assigned to a 'val':
    val list2 = listOf('B')
    // list2 += 'B' // Is the same as:
    // list2 = list2 + 'B'              // [2]

    // Immutable List assigned to a 'var':
    var list3 = listOf('C')
    list3 += 'C' // Is the same as:
    val newList = list3 + 'C'           // [3]
    list3 = newList                     // [4]

    list1 eq "[A, A, A]"
    list2 eq "[B]"
    list3 eq "[C, C, C]"
}
  • [1] list1 refers to a mutable object, which can therefore be modified in place. The compiler translates += to the plusAssign() call. It doesn’t matter if list1 is a val or a var because nothing is ever reassigned to list1 after creation—it always refers to the same mutable list. Make it a var and IntelliJ points out that it never changes and suggests that it be a val.
  • [2] This tries to create a new List by combining list2 and 'B', but it can’t reassign that new List to list2 because list2 is a val. Without the ability to perform that reassignment, the += cannot compile.
  • [3] Creates newList without modifying the existing immutable List referred to by list3.
  • [4] Because list3 is a var, the compiler assigns newList back into list3. The previous contents of list3 are then forgotten, and it appears that list3 has been mutated. Actually, the old list3 has been discarded and replaced by the newly-created newList, giving the illusion that list3 is mutable.

This behavior of += happens with other collections, as well. The resulting confusion is another reason to choose val over var for your identifiers.

Exercises and solutions can be found at www.AtomicKotlin.com.