So far, we’ve only worked with variables as individual values. Writing Kotlin becomes so much more interesting once we start putting variables together in a way that we can work on them as a collection. To learn about collections, let’s visit Libby, a bright young lady who’s always got a book nearby!
Who Loves to Read Books?
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
book3
from the middle of the list, thenbook4
andbook5
would need to be renamed tobook3
andbook4
. 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 variable holding the list 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. You can also useprintln()
with a collection variable.println(booksToRead)
When you do this, you’ll see its elements in order, like this:
[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 collection is a
List
.- The type of the elements in the collection is
String
.Once you know these two things, you 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 ofbooksToRead
isList<String>
.Let’s rewrite Listing 8.2, this time explicitly including the type information for the
booksToRead
collection 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 an instance of a generic. We will cover generics in detail in a future chapter. For now, just 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 you’ve already called
listOf()
to create a list, that list can’t be changed. You can’t add anything to it, and you can’t remove anything from it. The fancy word for “change” in programming is mutate, so a list that doesn’t allow you to add or remove elements is called an immutable list.Even though you can’t add or remove elements from a regular Kotlin
List
, you 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, like this: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,
booksToRead + "Beyond the Expanse"
is an expression that evaluates to a newList
instance. So, by the time this code is done running, we have two collection variables -booksToRead
andnewBooksToRead
.This is kind of 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
val
orvar
, and this includes variables that hold a collection. Keep in mind, though, that declaring a collection variable withvar
does not change the fact that the list itself is immutable. In other words, just declaring it withvar
does not make it so that you can add or remove elements. However,var
does let you assign another immutable list to it.So, by changing the
booksToRead
variable fromval
tovar
, the new list can be assigned to the existing variable name, like this: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 trashing the old paper list, 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. As you can guess, you can remove an element from a list in a similar way, but instead of the plus operator, 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 into a single line, like this:
booksToRead = booksToRead + "Beyond the Expanse" - "Among the Aliens"
Now, when Libby does
println(booksToRead)
, she sees 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 they have a type of
MutableList
.When using a mutable list, you can use the
add()
andremove()
functions to add or remove elements, like this: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")
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 of1
, the third has an index of2
, 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)
like this: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 Agatha
Kotlin 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 Expanse
Of 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 in Kotlin! We can use the
forEach()
function. Here’s how it looks.booksToRead.forEach { element -> println(element) }
When Kotlin is running 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, like this:That’s why programming languages call this a loop - because, for each element in the collection, it cycles back through that code. It’s also generally called iterating, and each time the code is run, it’s called 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 ittitle
instead. Alternatively, since this lambda has only a single parameter, you can use the implicit ‘it’ parameter 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 Expanse
Collection 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 you create a new collection from an existing one, you 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 used for sorting.
Before doing that conversion on all of the titles, let’s start with just one of them.
String
objects have aremovePrefix()
function, which you can use to remove words from the beginning of the string. Here’s how you can use it:val sortableTitle = "The Kingsford Manor Mystery".removePrefix("The ") println(sortableTitle) // Kingsford Manor Mystery
Perfect! Now all she needs is to apply this
removePrefix()
function to each element in the list!“Maybe I can use forEach(), 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
= 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 collection operation that runs the lambda and includes the result of that lambda as an element in a new collection.In Kotlin, that collection operation is called
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 an immutable
List
instead of aMutableList
). LikeforEach()
, 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.When you print each element of the list, you 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.
Tea with Agatha Mystery on First Avenue Ravine of Sorrows Kingsford Manor Mystery Beyond the Expanse
Let’s take a closer look at the
map()
function:
- 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 sort them?”
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 Agatha
In order to make things easier to read, each collection operation can go on its own line, like this:
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 is excited! Now she’s got 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 called
filter()
.Just like an air filter that blocks unwanted dust and allergens from getting through to your air conditioner system, a Kotlin list filter blocks elements that you 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: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 themap()
function, however, the lambda forfilter()
must return aBoolean
. If it returnstrue
for an element, then that element is passed into the new collection (i.e.,booksForNolan
in 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 the operation chain is not mutating a single list. In fact, each of these operations creates a new list. The list that’s created by the final operation,
filter()
, is the list that is assigned to the variablebooksForNolan
. 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, you could see how this could improve the performance of your code - that is, 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 might be helpful to you.
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 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 lists are helpful for ensuring that its elements are in a particular order, sets are helpful for ensuring that each element in it 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
= setOf( "The Malt Shop Caper", "Who is Mrs. W?", "At Midnight or Later", ) When you add an element to a set that already has that value, the set will remain unchanged.
val booksBySlim: MutableSet
= 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 particular 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:
- Lists have elements in a guaranteed order, and can contain duplicates.
- Sets have elements in no particular order, and are guaranteed not to contain duplicates.
Also, you can convert a list into a set, or the other way around. Simply use
toSet()
ortoList()
. 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 elements
Summary
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 you 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, you 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! See you then!