So far, we’ve only worked with variables as individual values. Writing Kotlin becomes so much more interesting once we start putting values together in such a way that we can work with them as a collection.
To learn about collections, let’s visit Libby, a bright young lady who’s always got a book nearby!
Let’s put things together!
Libby is a voracious reader. She’s always on the lookout for a great novel, so whenever someone tells her about a good book, she jots down the title on a list that she keeps on a sheet of paper. Here are the titles that are currently on her list.
Libby has been learning to write Kotlin code in her spare time, so she also wanted to write this same list in Kotlin, and print all of the titles to the screen.
Here’s what she wrote.
val book1 = "Tea with Agatha" val book2 = "Mystery on First Avenue" val book3 = "The Ravine of Sorrows" val book4 = "Among the Aliens" val book5 = "The Kingsford Manor Mystery" println(book1) println(book2) println(book3) println(book4) println(book5)“Hmm…”, she thought. “Every time I add a new book, I have to create a new variable. And keeping the numbers in order could be hard—if I remove
book3from the middle of the list, thenbook4andbook5would need to be renamed tobook3andbook4. If only there were a better way to keep track of my list of book titles…”Introduction to Lists
Thankfully, there’s a much better way! Libby can create a collection. Let’s start with one of the most common kinds of collections in Kotlin—a list. Creating a list is easy—just call
listOf()with the values that you want, separating them with commas. Let’s update Libby’s code so that it’s using a list.val booksToRead = listOf( "Tea with Agatha", "Mystery on First Avenue", "The Ravine of Sorrows", "Among the Aliens", "The Kingsford Manor Mystery", )This code looks pretty similar to Libby’s handwritten list. In fact, let’s compare the two!
The handwritten list and the Kotlin list have a lot in common.
First, they both have a name. In Kotlin, the name of the list variable is kind of like the name of the list on paper.
Next, both lists have items in them—in this case, the titles of the books. In Kotlin, the items in a list are called elements.
Finally, both lists have the titles in a particular order.
In the past, we’ve used the
println()function to print out the contents of a variable to the screen. We can also useprintln()with a collection variable.println(booksToRead)When we do this, we’ll see its elements in order.
[Tea with Agatha, Mystery on First Avenue, The Ravine of Sorrows, Among the Aliens, The Kingsford Manor Mystery]Collections and Types
When working with collections in Kotlin, we have two different types to consider.
The type of the collection we’re using.
The type of the elements in the collection.
These two things together determine the overall type of the collection variable. In the case of Listing 8.2:
The type of the collection is
List.The type of the elements in the collection is
String.Once we know these two things, we can write the type of a collection variable easily. Just put the type of the collection first, then write the type of the elements second, between a left angle bracket
<and a right angle bracket>.So the type ofbooksToReadisList<String>.Let’s rewrite Listing 8.2, this time explicitly including the type information for the
booksToReadcollection variable.val booksToRead: List<String> = listOf( "Tea with Agatha", "Mystery on First Avenue", "The Ravine of Sorrows", "Among the Aliens", "The Kingsford Manor Mystery", )This kind of a type is known as a generic. We will cover generics in detail in Chapter 18. For now, it’s just important to know how to write the type for a list, in case you need to use it as a function’s parameter type or return type.
Adding and Removing an Element
Libby just heard about another great book from her friend, Rebecca! She’s ready to add this new title, Beyond the Expanse, to her list. How can she do this?
Of course, she could just add one more argument to the end of
listOf(). But what about adding the title after the list has already been created?In Kotlin, once we’ve already called
listOf()to create a list, that list can’t be changed. We can’t add anything to it, and we can’t remove anything from it. The fancy word for “change” in programming is mutate, so a list that doesn’t allow us to add or remove elements is called an immutable list.1Even though we can’t add or remove elements from a regular Kotlin
List, we can create a new list by putting the original list together with a new element. To do this, use the plus operator. That is, use+to connect the original list with the new item, and assign it to a new variable.val booksToRead = listOf( "Tea with Agatha", "Mystery on First Avenue", "The Ravine of Sorrows", "Among the Aliens", "The Kingsford Manor Mystery", ) val newBooksToRead = booksToRead + "Beyond the Expanse"In this code, the highlighted code is an expression that evaluates to a new
Listinstance. So, by the time this code is done running, we have two collection variables—booksToReadandnewBooksToRead.This is kind of like writing the new list of titles on a second sheet of paper. That way, Libby actually ends up with two lists—the original list and the new list.
As you remember from Chapter 1, a variable can be declared with either
valorvar, and this includes variables that hold a collection. Keep in mind, though, that declaring a collection variable withvardoes not change the fact that the list itself is immutable. In other words, just declaring it withvardoes not make it so that you can add or remove elements. However,vardoes let you assign another immutable list to it.So by changing the
booksToReadvariable fromvaltovar, the new list can be assigned to the existing variable.var booksToRead = listOf( "Tea with Agatha", "Mystery on First Avenue", "The Ravine of Sorrows", "Among the Aliens", "The Kingsford Manor Mystery", ) booksToRead = booksToRead + "Beyond the Expanse"This is kind of like throwing the old paper list in the trash, and simply giving the new list the same name as the old one.
Libby’s list now has six titles in it. Just when she thought she was done updating the list, she heard from Rebecca again. “You know, I read Among the Aliens last week. It really wasn’t very good,” she said. “You shouldn’t bother reading that one.”
Libby would like to scratch that one off her list. The way we remove an element is similar to the way we added an element above, but instead of using the plus operator, we use the minus operator.
var booksToRead = listOf( "Tea with Agatha", "Mystery on First Avenue", "The Ravine of Sorrows", "Among the Aliens", "The Kingsford Manor Mystery", ) booksToRead = booksToRead + "Beyond the Expanse" booksToRead = booksToRead - "Among the Aliens"In fact, those last two lines can be consolidated, like this.
booksToRead = booksToRead + "Beyond the Expanse" - "Among the Aliens"With this change, if Libby writes
println(booksToRead), she would see this on the screen.[Tea with Agatha, Mystery on First Avenue, The Ravine of Sorrows, The Kingsford Manor Mystery, Beyond the Expanse]“Excellent!” she thought, “My reading list is all up to date!”
List and MutableList
So far, we’ve been using a regular Kotlin
List, which doesn’t allow changes to it, as we saw above. Instead, we had to create a new list by using a plus or minus operator.However, Kotlin also provides another kind of list—one that does allow you to change it. Since these lists do allow changes, they’re called mutable lists, and their type is
MutableList.val booksToRead: MutableList<String> = mutableListOf( "Tea with Agatha", "Mystery on First Avenue", "The Ravine of Sorrows", "Among the Aliens", "The Kingsford Manor Mystery", ) booksToRead.add("Beyond the Expanse") booksToRead.remove("Among the Aliens")A mutable list can be created with a function named
mutableListOf(). When using a mutable list, we can use theadd()andremove()functions to add or remove elements.Using a mutable list is kind of like Libby writing down her paper list with a pencil instead of a pen.
She can go in and erase a title, or add another, without using another sheet of paper.
Libby is ready to start reading those books! But in order know which book to start with, she needs to know how to get a single title out of the list.
Getting an Element from a List
“All right, which title is first on my list?” wondered Libby. She glanced down at her handwritten page. It was easy for her to see which one was first. “Tea with Agatha,” she noted. “Now how do I get the first title from the list in Kotlin?”
As mentioned earlier, the elements of a list are in a particular order, and that order is very important for getting an individual element out of the list.
Here’s how it works. Each element in the list is given a number, called an index, based on where it is in the list. The first element has an index of 0, the second has an index of 1, the third has an index of 2, and so on.
It’s easy to get an element out of a list once you know its index. Just call the
get()member function, passing the index as the argument. For example, Libby can get the first element out of the list by callingget(0).val booksToRead = listOf( "Tea with Agatha", "Mystery on First Avenue", "The Ravine of Sorrows", "The Kingsford Manor Mystery", "Beyond the Expanse" ) val firstBook = booksToRead.get(0) println(firstBook) // Tea with Agatha“Great!” said Libby. “Now I can easily get a single title out of the list of books!”
Rather than calling the
get()function directly, you can use the indexed access operator instead, which is written with an opening bracket[and a closing bracket], with the index in the middle. The code in the following listing does the exact same thing as the code above.val firstBook = booksToRead[0] println(firstBook) // Tea with AgathaKotlin developers use the indexed access operator much more than they use the
get()function, so we’ll be using it from now on.Now, getting an individual item out of the list can be helpful, but collections become especially helpful when we want to do something with each item in the list. Let’s see how to do that next!
Loops and Iterations
“Now, I’d like to print out the list of books to the screen,” said Libby to herself. “I’ll use
println(booksToRead)for this!” Upon running that code, here’s what she saw.[Tea with Agatha, Mystery on First Avenue, The Ravine of Sorrows, The Kingsford Manor Mystery, Beyond the Expanse]“It’s nice that I can print out the list so easily, but I’d really like to see the list vertically, like my handwritten list.” Here’s what she has in mind.
Tea with Agatha Mystery on First Avenue The Ravine of Sorrows The Kingsford Manor Mystery Beyond the ExpanseOf course, to achieve this, she could call
println()on each element one by one, like this.println(booksToRead[0]) println(booksToRead[1]) println(booksToRead[2]) println(booksToRead[3]) println(booksToRead[4])However, writing code like this is quite tedious. Plus, it would be easy to make a mistake by printing the elements out of order, or by accidentally printing the same element more than once. In fact, this looks a whole lot like the code back in Listing 8.1!
Instead of writing out the same code for each element in the list, what if Kotlin could just go through every element, one by one, and call
println()on each?Thankfully, this is very easy to do! We can use the
forEach()function. Here’s how it looks.booksToRead.forEach { element -> println(element) }When Kotlin is executing this code, it runs down through
println(element)for the first element, then comes back up and runs down it again for the second element, then comes back up and runs down it again for the third element, and so on.By going through this line of code over and over again, it’s as if it’s looping in circles.
That’s why programming languages call this a *loop—because, for each element in the collection, it cycles back through that code. This process is also called iterating, and each time the code in the loop runs, it’s a single iteration.
Let’s look a little closer at
forEach(), to understand why we had to structure the code the way we did.
forEach()is a member function that exists on collection variables. It’s a higher-order function that accepts a lambda. That lambda is the code that you want Kotlin to run “for each” element in the collection.Here we named the parameter
element, but you could have named ittitleinstead. Alternatively, since this lambda has only a single parameter, we can use the implicititparameter instead, which makes it nice and concise. In fact, we can put it all on one line.booksToRead.forEach { println(it) }The result in either case is exactly what Libby wanted—the book titles are printed out vertically, just like on her paper notepad!
Tea with Agatha Mystery on First Avenue The Ravine of Sorrows The Kingsford Manor Mystery Beyond the ExpanseCollection Operations
Libby is ready to share her list of books with other people who are interested in what she’ll be reading, starting with her friend Nolan. However, when she makes a copy of the list for him, she wants to make changes to some of the titles.
“I’d really like to remove the word ‘The’ from the beginning of each title,” thought Libby. “That way, I’ll be able to sort it alphabetically, and the titles that begin with ‘The’ won’t clump together.”
Mapping Collections: Converting Elements
Sometimes when we create a new collection from an existing one, we also want to convert one or more of the elements in some way. In Libby’s case, she wants to remove the word “The” when it appears at the beginning of a title, so that they could be sorted alphabetically.
Before doing that conversion on all of the titles, let’s start with just one of them.
Stringobjects have aremovePrefix()function, which returns a new string that omits characters from the beginning of the original string. Here’s how we can use it.val title = "The Kingsford Manor Mystery" val sortableTitle = title.removePrefix("The ") println(sortableTitle) // Kingsford Manor MysteryWith this code, the original
titlestring still begins with “The ”, but the newsortableTitlestring omits it.Perfect! Now she just needs to apply this
removePrefix() function to _each element in the list! “~Maybe I can useforEach(), since I know it operates on each item in the list”, thought Libby. She rolled up her sleeves, and cranked out the following code.val sortableTitles: MutableList<String> = mutableListOf() booksToRead.forEach { title -> sortableTitles.add(title.removePrefix("The ")) } sortableTitles.forEach { println(it) }“Well, that works,” thought Libby. “But it’s a little complicated, and it’s a lot of code to write…”
The reason this is complicated is that Libby wanted to create a new collection, but
forEach()doesn’t do that. It simply runs the lambda on an existing collection, and then returnsUnit. What she really needs is a function that runs the lambda for each element and includes the result of that lambda as an element in a new collection.In Kotlin, that function is named
map(). Here’s how Libby can use it to remove the word “The” from the beginning of titles in the new collection.val sortableTitles = booksToRead.map { title -> title.removePrefix("The ") }This code does the same thing as the previous listing, except that the result is a regular
Listinstead of aMutableList.Like
forEach(), themap()function calls the lambda once with each element in the list. However, unlikeforEach(),map()will use the result of the lambda on each iteration to build out a new list.Tea with Agatha Mystery on First Avenue Ravine of Sorrows Kingsford Manor Mystery Beyond the ExpanseWhen we print each element of the list, we can see that both The Ravine of Sorrows and The Kingsford Manor Mystery have been updated so that the word “The” is not at the beginning.
Let’s take a closer look at the
map()function.Here are the key takeaways.
Similar to
forEach(), themap()function is a higher-order function that takes a lambda.That lambda will run once for each element in the list.
The result of the lambda will be an element in the new collection.
The
map()function returns that new collection.Functions like
forEach()andmap()are called collection operations, because they’re functions that perform some operation on (that is, they do something with) a collection.“Perfect!” said Libby, “Now that the titles have been changed like I want, maybe I can finally sort them alphabetically!”
Sorting Collections
The
forEach()andmap()functions are only two of many collection operations in Kotlin. Another one that can be quite helpful is calledsorted().Since the
map()function returns a collection, Libby can just callsorted()immediately after the call tomap(), like this.val sortedTitles = booksToRead.map { title -> title.removePrefix("The ") }.sorted()When she printed out the elements of
sortedTitles, she saw the output she was hoping for!Beyond the Expanse Kingsford Manor Mystery Mystery on First Avenue Ravine of Sorrows Tea with AgathaIn order to make things easier to read, each collection operation can go on its own line.
val sortedTitles = booksToRead .map { title -> title.removePrefix("The ") } .sorted()This code is identical to the previous listing except for the formatting. In other words, all of the letters and punctuation are exactly the same and in the same order—it’s only the space between them that’s different.
Writing the collection operations vertically like this can be helpful because it makes it easy to scan down the lines to see what collection operations are involved and what order they’re in. For example, first the titles are mapped, and then the titles are sorted. For that reason, Kotlin developers often format their code this way.
Filtering Collections: Including and Omitting Elements
Libby was excited to have a list of books—sorted alphabetically—to share with Nolan.
“I can’t wait to see your list of books,” said Nolan. “Just remember—I only read mystery novels!”
“Only mysteries…?” repeated Libby. “Okay,” she thought to herself, “the final thing I need to do is remove the titles that are not mysteries.” She pulled out a sheet from her notepad, and wrote a customized list just for Nolan, omitting any title that is not a mystery.
“How can I do this in Kotlin?” she wondered.
As you probably guessed, Kotlin includes a collection operation that makes this easy, and unsurprisingly, it’s named
filter(). Just like an air filter that blocks unwanted dust and allergens from getting through to an air conditioner system, a Kotlin list filter blocks elements that we don’t want to get through to a new list!Let’s use the
filter()function to filter down the list of books to just those that have “Mystery” in the title. To achieve this, we’ll use a function namedcontains(), which returns aBooleanindicating whether any part of the string includes its argument.By using the
contains()function inside thefilter()lambda, we can filter the list of books down to only those that include the word “Mystery.”val booksForNolan = booksToRead .map { title -> title.removePrefix("The ") } .sorted() .filter { title -> title.contains("Mystery") }The
filter()function is similar to themap()function above—it takes a lambda as an argument, and that lambda will be invoked once for each title in the original list.Unlike the
map()function, however, the lambda forfilter()must return aBoolean. If it returnstruefor an element, then that element is passed into the new collection (i.e.,booksForNolanin this case). If it returnsfalse, then it’s omitted from the new collection.Here’s a breakdown of how to use the
filter()function.Printing each element of the list, here’s what Libby saw.
Kingsford Manor Mystery Mystery on First Avenue“Great,” she said. “The list is exactly like I wanted it. It includes only mysteries, and it’s sorted properly!”
Collection Operation Chains
Let’s look at that code again.
val booksForNolan = booksToRead .map { title -> title.removePrefix("The ") } .sorted() .filter { title -> title.contains("Mystery") }In Kotlin, it’s common to put multiple collection operations together like this, one after another. When we do this, it’s called chaining the collection operations—each operation is like one link in the chain. In this code listing, the
map(),sorted(), andfilter()calls are chained together.Keep in mind that each operation in the chain creates a new list rather than mutating the original list. The list that’s created by the final operation—in this case,
filter()—is the list that is assigned to thebooksForNolanvariable.The *intermediate lists—that is, the lists that are created by the collection operations inside the chain—are used by the next operation in the chain, but are not assigned to any variable. It’s still important to keep these intermediate lists in mind, though.
This next illustration shows the list that’s involved at each step in the chain.
Whenever you’ve got a collection operation chain like this, it’s helpful to consider how many elements are in each intermediate list. For example, the code in Listing 8.21 has the
filter()call at the end of the chain. But what if it went at the beginning of the chain instead, like this?val booksForNolan = booksToRead .filter { title -> title.contains("Mystery") } .map { title -> title.removePrefix("The ") } .sorted()By doing this, the intermediate list that
filter()produces would only have two elements, in which case themap()function would only need to invoke its lambda twice instead of five times, andsorted()would only have two items to sort instead of five. In this example, the final list is the same either way, but Listing 8.22 is likely to be more efficient than Listing 8.21.Here’s an illustration showing the list involved at each step when placing the
filter()call at the top. Notice that the intermediate lists have fewer elements than they did in the previous illustration.On a small list like this, it’s not a big deal, but on a list that has hundreds or thousands of elements, it’s easy to see how this could improve the performance of your code—in other words, it would run much faster!
Other Collection Operations
Kotlin has many other collection operations that are easy to use! Just to give you an idea, here are a few others that can come in handy.
drop(3)- The new list omits the first 3 elements from the original list.
take(5)- The new list uses only the first 5 elements from the original list.
distinct()- The new list will omit duplicate elements, so that each element is included only once.
reversed()- The new list will have the same elements as the original, but their order will be backwards.You can see a more complete list of them in the official Kotlin API documentation.
Introduction to Sets
Before we wrap up this chapter, it’s worth noting that lists aren’t the only kind of collection in Kotlin.
Lists are probably the most frequently used, but another helpful collection type is called a set. Whereas a list ensures that its elements stay in a specific order, a set ensures that each of its elements is always unique.
For example, Nolan’s favorite mystery author, Slim Chancery, has written three books, and Nolan is proud to say he’s got the whole set.
Creating a set in Kotlin is just as easy as creating a list. Simply use
setOf()ormutableSetOf()instead oflistOf()ormutableListOf().val booksBySlim: Set<String> = setOf( "The Malt Shop Caper", "Who is Mrs. W?", "At Midnight or Later", )If we add an element to a set that already has that value, the set will remain unchanged.
val booksBySlim: MutableSet<String> = mutableSetOf( "The Malt Shop Caper", "Who is Mrs. W?", "At Midnight or Later", ) booksBySlim.add("The Malt Shop Caper") println(booksBySlim) // [The Malt Shop Caper, Who is Mrs. W?, At Midnight or Later]Note that a set does not guarantee the order of its elements when you print them out or use a collection operation on it. It’s possible that the elements will be in the same order that you added them, but don’t depend on it!
Because sets don’t have any guaranteed order to their elements, their elements do not have indexes. For that reason, sets do not even include a
get()function.The key takeaway is that:
Listobjects have elements in a guaranteed order, and can contain duplicates.
Setobjects have elements in no particular order, and are guaranteed not to contain duplicates.Also, it’s possible to convert a list into a set, or the other way around. Simply use
toSet()ortoList(), as shown below. Just remember that if you convert a list to a set, you’ll lose duplicate elements, and the order could possibly be different!val bookList = listOf( "The Malt Shop Caper", "At Midnight or Later", "The Malt Shop Caper", ) val bookSet = bookList.toSet() // bookSet has two elements val anotherBookList = bookSet.toList() // anotherBookList also has two elementsSummary
Thanks to Kotlin’s collection types, Libby can keep her reading lists organized and share them with her friends. By using lists, she can keep the books in order, and with sets, she can avoid duplicates. Now she’s ready to crack open the first book and get immersed in the story!
Up until this chapter, we’ve only worked with individual variables. By using collections like lists and sets, we’re able to do things with entire groups of values, which opens a whole new world of possibilities! In this chapter, you learned about collections, including:
- How to create a list.
- How to create lists by adding or removing an element from another list.
- The difference between immutable and mutable collections.
- How to get a single element out of a list.
- Collection operations, like
filter(),map(), andsorted().- The difference between a list and a set.
We discovered how easy it is to get an element from a list by its index. However, sometimes we want an easy way to get an element by some other information about it. For example, instead of getting a book by its positional index, we might want to get a book by its ISBN (that long number above the barcode on the back). In the next chapter, we’ll learn about another way to group elements that will make it easy to do this!
Technically, a List is not guaranteed to be immutable. Depending on the particular instance of the list, it might be possible to cast it to a MutableList and then make changes to it. In Chapters 12 and later, we’ll learn about subtypes, supertypes, and casting. But for now, we’ll regard a List as immutable, because it can’t be mutated in its current form. ↩︎