1. Previous: Chapter 7
  2. Next: Chapter 9
Kotlin: An Illustrated Guide • Chapter 8

Collections: Lists and Sets

Chapter cover image

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.

A sheet of paper with a list of books that Libby wants to read. The Kingsford Manor Mystery Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Among the Aliens Books to Read

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, then book4 and book5 would need to be renamed to book3 and book4. 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!

Similarities between Libby's handwritten list and the Kotlin list. The Kingsford Manor Mystery Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Among the Aliens Books to Read val booksToRead = listOf ( "Tea with Agatha" , "Mystery on First Avenue" , "The Ravine of Sorrows" , "Among the Aliens" , "The Kingsford Manor Mystery" , ) Name of List Items First Last First Last

The handwritten list and the Kotlin list have a lot in common.

  1. 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.

  2. 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.

  3. 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 use println() 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.

  1. The type of the collection we’re using.

  2. 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:

  1. The type of the collection is List.

  2. The type of the elements in the collection is String.

The kind of collection is a list, because we used listOf. The type of the elements is String, because the values are all strings. val booksToRead = listOf ( "Tea with Agatha" , "Mystery on First Avenue" , "The Ravine of Sorrows" , "Among the Aliens" , "The Kingsford Manor Mystery" , ) The kind of collection we're using is a List Each of these elements is a 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 of booksToRead is List<String>.

The type of a list of strings in Kotlin. List is the collection type, and String is the element type. List<String> Type of the collection Type of the elements

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 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.1

Even 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 List instance. So, by the time this code is done running, we have two collection variablesbooksToRead and newBooksToRead.

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.

Two sheets of paper - one with the original list, and one with the new list. The Kingsford Manor Mystery Beyond the Expanse Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Among the Aliens New Books to Read The Kingsford Manor Mystery Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Among the Aliens Books to Read

As you remember from Chapter 1, a variable can be declared with either val or var, and this includes variables that hold a collection. Keep in mind, though, that declaring a collection variable with var does not change the fact that the list itself is immutable. In other words, just declaring it with var 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 from val to var, 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.

The original list is trashed, and the new list takes its place. The Kingsford Manor Mystery Beyond the Expanse Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Among the Aliens Books to Read Tea with Agatha Books to Read

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 the add() and remove() 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.

A handwritten list with one removal and one addition. The Kingsford Manor Mystery Beyond the Expanse Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Among the Aliens Books to Read

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?”

Identifying the first item in a handwritten list. The Kingsford Manor Mystery Beyond the Expanse Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Books to Read First Item

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.

The first element has index 0, the second has index 1, and so on. 0 1 2 3 4 val booksToRead = listOf ( "Tea with Agatha" , "Mystery on First Avenue" , "The Ravine of Sorrows" , "The Kingsford Manor Mystery" , "Beyond the Expanse" )

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 calling get(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 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! 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.

When using `forEach()` above, Kotlin loops over the `println(element)` statement, giving it each element in turn. "The Ravine of Sorrows" "Tea with Agatha" "The Kingsford Manor Mystery" "Beyond the Expanse" "Mystery on First Avenue"

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.

Breaking down the `forEach()` function. Lambda parameter Code to run for each element booksToRead. forEach { element -> println (element) } Collection variable

Here we named the parameter element, but you could have named it title instead. Alternatively, since this lambda has only a single parameter, we 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 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.

Creating a new list, where titles that began with the word "The" no longer include that word. The Kingsford Manor Mystery Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Beyond the Expanse Books to Read Kingsford Manor Mystery Beyond the Expanse Tea with Agatha Mystery on First Avenue Ravine of Sorrows Sortable Titles

Before doing that conversion on all of the titles, let’s start with just one of them. String objects have a removePrefix() 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 Mystery

With this code, the original title string still begins with “The ”, but the new sortableTitle string omits it.

Perfect! Now she just needs 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<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 returns Unit. 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 List instead of a MutableList.

Like forEach(), the map() function calls the lambda once with each element in the list. However, unlike forEach(), 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 Expanse

When 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.

When using `map()` above, Kotlin loops over the `title.removePrefix("The ")` expression, giving it each element in turn. "The Ravine of Sorrows" "Tea with Agatha" "The Kingsford Manor Mystery" "Beyond the Expanse" "Mystery on First Avenue" "Ravine of Sorrows" "Tea with Agatha" "Kingsford Manor Mystery" "Beyond the Expanse" "Mystery on First Avenue"

Let’s take a closer look at the map() function.

Breakdown of the `map()` function. Lambda parameter Evaluates to a value that becomes an element in the new list. val sortableTitles = booksToRead. map { title -> title. removePrefix ( "The " ) } Original list variable New list variable

Here are the key takeaways.

  • Similar to forEach(), the map() 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() and map() 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() and map() functions are only two of many collection operations in Kotlin. Another one that can be quite helpful is called sorted().

Since the map() function returns a collection, Libby can just call sorted() immediately after the call to map(), 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.

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.

Creating a new list that only includes certain titles from the original list. Kingsford Manor Mystery Tea with Agatha Mystery on First Avenue Ravine of Sorrows Beyond the Expanse Books to Read Kingsford Manor Mystery Mystery on First Avenue Books for Nolan

“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 named contains(), which returns a Boolean indicating whether any part of the string includes its argument.

How to use the `contains` function. val title = "Mystery on First Avenue" title. contains ( "Avenue" ) true title. contains ( "Street" ) false

By using the contains() function inside the filter() 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 the map() 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 for filter() must return a Boolean. If it returns true for an element, then that element is passed into the new collection (i.e., booksForNolan in this case). If it returns false, then it’s omitted from the new collection.

When using `filter()` above, Kotlin loops over the `title.contains("Mystery")` expression, giving it each element in turn. "The Ravine of Sorrows" "Tea with Agatha" "The Kingsford Manor Mystery" "Beyond the Expanse" "Mystery on First Avenue" false false true false true

Here’s a breakdown of how to use the filter() function.

Breakdown of the `filter()` function. Lambda parameter Determines whether this element is included in the new list. Original list variable New list variable val booksForNolan = booksToRead. filter { title -> title. contains ( "Mystery" ) }

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(), and filter() calls are chained together.

A collection operation chain. Each operation is like one link in the chain. val booksForNolan = booksToRead . map { title -> title. removePrefix ( "The " ) } . sorted () . filter { title -> title. contains ( "Mystery" ) } Collection operation chain

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 the booksForNolan variable.

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.

A new list is created at each step of the operation chain. Kingsford Manor Mystery Mystery on First Avenue Beyond the Expanse Kingsford Manor Mystery Mystery on First Avenue Ravine of Sorrows Tea with Agatha Tea with Agatha Mystery on First Avenue Ravine of Sorrows Kingsford Manor Mystery Beyond the Expanse Tea with Agatha Mystery on First Avenue The Ravine of Sorrows The Kingsford Manor Mystery Beyond the Expanse val booksForNolan = booksToRead . map { title -> title. removePrefix ( "The " ) } . sorted () . filter { title -> title. contains ( "Mystery" ) }

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 the map() function would only need to invoke its lambda twice instead of five times, and sorted() 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.

Fewer elements are processed in `map()` and `sorted()` when `filter()` runs first. Kingsford Manor Mystery Mystery on First Avenue Mystery on First Avenue Kingsford Manor Mystery Mystery on First Avenue The Kingsford Manor Mystery Tea with Agatha Mystery on First Avenue The Ravine of Sorrows The Kingsford Manor Mystery Beyond the Expanse val booksForNolan = booksToRead . filter { title -> title. contains ( "Mystery" ) } . map { title -> title. removePrefix ( "The " ) } . sorted ()

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() or mutableSetOf() instead of listOf() or mutableListOf().

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:

  1. List objects have elements in a guaranteed order, and can contain duplicates.

  2. Set objects 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() or toList(), 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 elements

Summary

Enjoying this book?
Order the paperback today!

Kotlin: An Illustrated Guide is now available on Amazon See the book on Amazon

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:

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!


  1. 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. ↩︎

Share this article:

  • Share on Facebook
  • Share on Reddit