Lists
A
Listis 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
Listuses square brackets when displaying itself. -
[2]
forloops work well withLists:for(i in ints)meansireceives each value inints. You don’t declareval ior give its type; Kotlin knows from the context thatiis aforloop identifier. -
[3] Square brackets index into a
List. AListkeeps 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 value99. Thus an index of4produces the value11.
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]
list1refers to a mutable object, which can therefore be modified in place. The compiler translates+=to theplusAssign()call. It doesn’t matter iflist1is avalor avarbecause nothing is ever reassigned tolist1after creation—it always refers to the same mutable list. Make it avarand IntelliJ points out that it never changes and suggests that it be aval. -
[2] This tries to create a new
Listby combininglist2and'B', but it can’t reassign that newListtolist2becauselist2is aval. Without the ability to perform that reassignment, the+=cannot compile. -
[3] Creates
newListwithout modifying the existing immutableListreferred to bylist3. -
[4] Because
list3is avar, the compiler assignsnewListback intolist3. The previous contents oflist3are then forgotten, and it appears thatlist3has been mutated. Actually, the oldlist3has been discarded and replaced by the newly-creatednewList, giving the illusion thatlist3is 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.