Android Fall Sale

Get up to 55% off all Android books!

Kotlin Collections: Getting Started

In this tutorial, you’ll learn how to work with Kotlin Collections. You’ll transform data, filter it out, and use different types of collections in Kotlin!

5/5 3 Ratings

Version

  • Kotlin 1.3, Other, Other

Most people think programming is hard and involves dealing with a lot of complex mechanisms. But, when you think about it, most of what programmers do every day is work with data.

Much like the Sith, data rarely comes alone! This is why it’s crucial to work with groups, or collections, of data in our everyday lives.

In this tutorial, you’ll learn how to do that with Kotlin collections. Throughout the tutorial, you’ll:

  • See which Kotlin collection types exist.
  • Compare Kotlin collections types and what you should use them for.
  • Learn how to use different types of Kotlin collections.
  • Explore Kotlin collections operators.
  • Transform data in collections, using various operators.
  • Group and filter data according to different conditions.
Note: If you’re new to Kotlin, it’s highly recommended that you start with Beginning Android Development with Kotlin. This tutorial will focus only on Kotlin, but you can apply everything you learn to Android development.

Getting Started

Download the starter project using the Download materials button at the top or bottom of the tutorial. Open Android Studio 3.4.2 or later and choose Open an existing Android Studio project. Then select the project you just extracted.

Note: To avoid downloading and installing IntelliJ or Android Studio, you can use the Kotlin Playground tool for playing around with the language. All the code can be executed in the playground.

If you open the starter project, you’ll see a lot of small, empty files, separated in their respective packages. Moreover, each file will represent one section of the tutorial and Kotlin collections.

Project structure

You’ll only use one file at a time to learn about collections. So by the end, you should cover every topic within a separate file. However, if you’ve chosen to follow the tutorial in the Kotlin Playground, you should see something like the following image when you open the website:

Kotlin Playground

On the left-hand side, you have the code editor, in which you’ll add all the snippets of code. And on the right-hand side, you have buttons to run the code, change the version of Kotlin and share the code.

But that’s enough talk about the projects, time to start learning!

What are Collections?

If you’ve got some experience with programming, you should know there’s a small number of data types in every programming language. This is true in Kotlin as well.

Furthermore, types in object-oriented programming languages are often divided into primitive and object types. Those available in most languages are Boolean, Int, Char, String and Object/Any types, along with several different number types.

But you won’t always work with only those types. In addition to creating your own types, you often need to work with a large number of objects which are of the same type.

For example, if you’re writing an e-reader application, you’ll display and manage many books. In this case, you have to use a group of objects of the same type. In common terms, this is a collection because you can collect those objects and add or remove them when needed.

But collections aren’t only for storing data. Many programming languages provide different types of collections so you can use them in different scenarios.

Kotlin collections also support this. It provides four different types.

Note: You can open Collections.kt from the Kotlin standard library and take a look at all the Kotlin collections definitions. Most are implemented as interfaces, with concrete implementations varying between each Kotlin collection type.

Collection Types

You can differentiate collections based on a few questions:

  1. Is it dynamic or static in memory?
  2. Does it allow duplicate elements?
  3. How fast is it when it comes to data lookup, insertion or removal?
  4. Can you change the internal contents?

You’ll find the answers to all these questions by exploring the following four collection types:

  1. Array
  2. List
  3. Set
  4. Map

It’s best to start learning about Kotlin collections by exploring arrays since it’s a concept which exists in nearly all programming languages.

Arrays in Kotlin

Array is the simplest Kotlin collection. Here are some points to keep in mind about this collection:

  • It stores objects of the same type together and has a fixed size.
  • It’s a static collection.
  • Each value is indexed by the position in the array.
  • The first value is actually the value with the index zero.
  • The last value is at the index and is equal to the array’s size minus one.

For example, say your computer has thirty blocks of memory free and you create an array of four Int values. The array will take four blocks of the free memory and create one big block of four values, which it groups together.

This means that you can’t add or remove values because the program has allocated that big block to always take four memory slots. But you can always take those four blocks of memory and change what you store in them. Because of this, arrays are immutable when it comes to size, but mutable when it comes to their elements.

Creating An Array

Open either the Arrays.kt file or the Kotlin Playground, depending on the work environment you chose, and add the following code:

fun main() {  
  val arrayOfNumbers = arrayOf(2, 3, 5, 6, 10)
  println(arrayOfNumbers)
}

To create an array of any type, all you have to do is call arrayOf(..values). The compiler will know which type of an array it is based on the values you pass in to this method.

In the snippet above, you’ve allocated an array of five numbers, creating an Array of type Int. If you try printing it out by running the snippet of code, you should receive something like [Ljava.lang.Integer;@2b193f2d. Not quite what you expected, right? :]

Since an Array is an object, the output above represents its class name and hash code. To print out the values of an array you have to iterate over them. Change the code above to the following:

fun main() {  
  val arrayOfNumbers = arrayOf(2, 3, 5, 6, 10)
  arrayOfNumbers.forEach { number -> println(number) }
}

Run the code snippet. You should now see the values printed out in the order you created them. Here you used forEach() to iterate through the elements of the array.

Another way you can iterate through the values is by using the Kotlin for loop. Change the forEach(). Change the code above to the following:

fun main() {  
  val arrayOfNumbers = arrayOf(2, 3, 5, 6, 10)
  for (number in arrayOfNumbers) {
    println(number)
  }
}

Run the code and you should see the same output.

There are different ways of creating an array in Kotlin:

  • Create an empty array by calling emptyArray() and specifying the type of the array.
  • Create an array with a specific size, and the default values for all the elements by calling Array(size) { defaultValue }.

For example, if you wanted to create an array with five spots, where each element is an empty String, you’d write the following:

fun main() {  
  val someOtherArray = Array(5) { "" }
  println(someOtherArray)
}

Creating an array is one thing. But you should be able to use its values, access specific indices and change the contents since array contents are mutable.

Working With Arrays

As previously mentioned, arrays rely on indices. They’re positions in the array memory structure, starting with the zeroth index, for the first item.

Note: This is because computers work with bit registers, and the smallest possible positive number is a zero when all the bits are also zero.

With that in mind, here’s how you’d access the first item in an array:

fun main() {  
  val arrayOfNumbers = arrayOf(2, 3, 5, 6, 10)
  val firstValue = arrayOfNumbers[0] // Resolves to 2
  println(firstValue)
}

You can get items from an array, and almost all the Kotlin collections, by using the square-brackets syntax. Within the brackets, you mention which index you are looking for. In the example above you’d be looking for the zeroth index.

If you try to access a value with an index which is larger than the last index in a collection, or a negative index, you’ll receive an ArrayIndexOutOfBoundsException.

To change a value at a certain index, you can use the same syntax, combined with the assignment operator:

fun main() {  
  val arrayOfNumbers = arrayOf(2, 3, 5, 6, 10)
  val firstValue = arrayOfNumbers[0] // Resolves to 2
  arrayOfNumbers[0] = 100 // the first element of the array is now `100`
  val newFirstValue  = arrayOfNumbers[0]
  println(newFirstValue) 
}

Using the access operator, you can easily change what you store in arrays and access the values.

Note: Re-assignment only works for mutable Kotlin collections. You’ll learn about immutable collections in the next section.

Finally, there’s a plethora of useful functions and properties on arrays, such as array.size to know the size of an array, or array.lastIndex, to know the last index. You’ll cover a lot throughout this tutorial, but the sheer number of functions is baffling! :]

Note: There’s an additional language feature close to arrays called the varags or variable arguments parameter. Be sure to check it out using the official documentation.

Arrays are the basis of all the Kotlin collections because most collections use arrays internally, to store their data. You’ll check it out the next collection.

Lists in Kotlin

Lists are a dynamic version of arrays. In terms of memory, dynamic means that you can allocate each element in any free memory slot. Because of this, you can have very large lists of data since you can scatter elements around in memory.

List collections can be both mutable and immutable in terms of their size and contents.

Lists are useful in programming. Since you’ll use lists in your everyday coding it’s important to understand them.

Creating a List

Once again, depending on your work environment, open the Lists.kt file or clear the Kotlin Playground code and add the following code:

fun main() {
  val list = listOf(2, 3, 5, 6, 7)
  println(list)
}

If you run the snippet above, you should see the numbers printed out. This is because all true collections override toString(). Additionally, you can use the square brackets syntax to access the values.

Try to do the following:

fun main() {
  val list = listOf(2, 3, 5, 6, 7)
  list[2] = 100
}

You’ll receive an error. This is because there is a distinct difference between a List and MutableList construct in Kotlin collections.

You can change the size and contents of Kotlin collections which start with the Mutable.

Mutable Lists

To be able to add, remove and insert elements at certain indices, you have to create a MutableList using mutableListOf(). Take the following snippet for example:

fun main() {
  val list = mutableListOf(2, 3, 5, 6, 7)
  list[2] = 100 // works now
  println(list[2]) // 100
  list.add(index = 3, element = 500)
  println(list[3]) // 500
  list.remove(7) 
  println(list) // [2, 3, 100, 500, 6]
  list.removeAt(0)
  println(list) // [3, 100, 500, 6]
}

Because the list is now mutable, you can change items at indices, add items at specific indices and remove items at provided indices.

But what if you don’t want to have duplicate elements? You’d use Sets to eliminate duplicates and preserve elements uniqueness.

Sets in Kotlin

A set is a collection of elements where each of the elements is unique and there are no duplicates. They’re useful when you need to filter out duplicates. For example, you might use a set when storing ids or users.

Sets are a more advanced version of lists, with internal filtering of data. Like lists, they can be both mutable and immutable.

Creating Sets

To create a set, use setOf() or mutableSetOf(). If you run the snippet, you’ll see only three items will print, even though there are four items in the set initializer function. This is because there are two items with the same id and name:

data class Worker(
    val id: Int,
    val name: String
)

fun main() {
  val workers = setOf(
      Worker(id = 5, name = "Filip"),
      Worker(id = 3, name = "Mike"),
      Worker(id = 5, name = "Filip"),
      Worker(id = 4, name = "Filip")
  )
  // hashcode is used to remove duplicates
  println(workers)
}

Sets use an object’s hashCode() internally to filter out duplicates. If the set is mutable and you try to add another element to it, the new object will replace the old one. You can override hashCode() and manually determine how to differentiate the elements.

Working With Sets

Sets are a bit different than lists when it comes to accessing data. You wouldn’t look up items in a set using indices since the index of an item is actually it’s hash code. If you know the hash code, then you should already have the value.

As such, you’ll use sets to store data inside and keep a clear, unique, collection of elements. You can still iterate over them. Check the following code:

data class Worker(
    val id: Int,
    val name: String
)

fun main() {
  val workers = mutableSetOf(
      Worker(id = 5, name = "Filip"),
      Worker(id = 3, name = "Mike"),
      Worker(id = 5, name = "Filip"),
      Worker(id = 4, name = "Filip")
  )
  println(workers) // [Worker(id=5, name=Filip), Worker(id=3, name=Mike), Worker(id=4, name=Filip)]
  val removedWorker = Worker(id = 5, name = "Filip")
  workers.remove(removedWorker)
  println(workers) // [Worker(id=3, name=Mike), Worker(id=4, name=Filip)]
}

Since Sets do not have indices, they’re considered an unordered collection. Sets don’t care about what order you insert things in, they only care to store unique object instances. Sets are also used at the core of Maps collections.

Maps in Kotlin

Maps store pairs of objects where each value has a distinctive key. Each pair can be of any type you want and contains two objects.

Maps are collections of key/value pairs. As such, maps are extremely useful when you’re trying to tie one value to a key like an id or a String identifier.

And like other collections, maps can be both mutable and immutable.

Creating a Map

You can probably guess how to create a map by now, given that you initialize all the collections the same way. :]

fun main() {
  val httpHeaders = mapOf(
      "Authorization" to "your-api-key",
      "ContentType" to "application/json",
      "UserLocale" to "US")
}

By using mapOf() you can create a map. Each map has two generic type parameters: key and value. In the initializer, you use to() to create pairs of values with ease. Given that the pairs are of two strings, you’re creating a Map of types String, String.

As you can see, you’re creating an HTTP headers map for the authorization key, the content type format and the user’s locale. If you send this to a backend server, it can look at each of the keys and read the values paired with them.

And because maps use sets internally, by having key set all the keys are unique and there are no duplicates.

Working With a Map

Like with sets and lists, maps are immutable by default. To create a mutable map, you have to use mutableMapOf():

fun main() {
  val httpHeaders = mutableMapOf(
      "Authorization" to "your-api-key",
      "ContentType" to "application/json",
      "UserLocale" to "US")
}

Now, you can change its values by their keys:

...
httpHeaders["Authorization"] = "something else"
println(httpHeaders["Authorization"]) // something else

Because the indices in a map are actually the keys, whichever type of key you use, you have to pass in the square brackets. Moreover, if you want to add values, you don’t add() them, per se. Instead, you have to call put(), like below:

...
httpHeaders.put("Hello", "World")
println(httpHeaders) // {Authorization=something else, ContentType=application/json, UserLocale=US, Hello=World}

or

...
httpHeaders["Hello"] = "World"
println(httpHeaders) // {Authorization=something else, ContentType=application/json, UserLocale=US, Hello=World}

This is the same as using the assignment operator with the appropriate key.

Iterating through maps is a bit different from the rest of Kotlin collections. Since maps have two objects for each element, you have to iterate over pairs, instead of single values. You can do this in the following way:

httpHeaders.forEach { key, value -> println("Value for key $key is $value") }

You iterate through the pairs. In each iteration, you receive both the key and the value for that key, since it’s often necessary to know and consume both the key and the value.

Nice work! Kotlin collections provide much more functionality for data consuming and transforming.

Concrete Collection Types

Kotlin collections have to be interoperable with Java. Because of this, they revolve around the usage of the same concrete types when it comes to each collection type. For example, maps can be HashMap or a LinkedHashMap.

Concrete types are a huge topic and one tutorial won’t do them justice. It’s better to check out this link, which briefly explains the various types. Alternatively, check out the official documentation for Java collection types, starting with the List.

Collection Operators

The best feature Kotlin collections offers is the ability to transform between Kotlin collection types using collection operators. You can use them to:

  • Transform from a list to a set.
  • Turn a list of strings to a list of integers.
  • Group the data according to certain conditions.

You’ll learn several usages of the operators including:

  • filtering elements.
  • looking up data.
  • grouping data.
  • transforming the elements.
  • validating the element.s

Open Operations.kt and add the following code:

data class Product(
    val id: Int,
    val name: String,
    val price: Double
)

class Receipt(
    val id: Int,
    val seller: Worker,
    val products: List<Product>,
    val isPaid: Boolean = false
)

class Store(
    val receipts: List<Receipt>,
    val workers:List<Worker>
)

data class Worker(
    val id: Int,
    val name: String
)

fun beer() = Product(id = 2, name = "Beer, light, 0.5l", price = 7.5)
fun coffee() = Product(id = 3, name = "Ground coffee 1kg", price = 5.0)
fun bread() = Product(id = 1, name = "Gluten-free bread, 1kg", price = 5.0)

fun main() {
    val firstWorker = Worker(id = 1, name = "Filip")
    val secondWorker = Worker(id = 2, name = "Chris")

    val store = Store(
        // 1
        receipts = listOf(
            Receipt(
                //2
                id = 1,
                seller = firstWorker,
                products = listOf(bread(), bread(), bread(), coffee(), beer()),
                isPaid = true
            ),

            Receipt(
                id = 2,
                seller = secondWorker,
                products = listOf(coffee(), coffee(), beer(), beer(), beer(), beer(), beer()),
                isPaid = false
            ),

            Receipt(
                id = 3,
                seller = secondWorker,
                products = listOf(beer(), beer(), bread()),
                isPaid = false
            )
        ),
        // 3
        workers = listOf(firstWorker, secondWorker)
    )
}

Here’s a play-by-play of what’s happening above:

  1. First, you create a store with a list of receipts from purchases.
  2. Each receipt has a list of products, whether it’s paid or not, an id and the worker which handled the receipt.
  3. You then add a list of workers to the store, because someone has to sell the products.

You can transform the data to create something more meaningful than this huge construct.

Transforming Data

Transforming is the process of taking one type and converting it to another. For example, you can transform a String into an Int by calling string.length on it.

In your store example, you need to get all the products you sold. Add the following code to main():

val receipts = store.receipts // fetch the receipts
val productsLists = receipts.map { it.products } // List<List<Product>>
println(productsLists) 

By using the map operator, you can transform all the receipts collection elements to their respective products. Map iterates over each element of type T and passes it into a provided lambda function. The lambda function returns an element of type R, which in this case, turns each receipt item into a product. What you get back is a List of List of Products, which is fine, but clunky to work with.

You can improve this by using the flatMap operator instead:

val receipts = store.receipts // fetch the receipts
val allProducts = receipts.flatMap { it.products } // List<Product>
println(allProducts)

Flat mapping not only means transforming the elements (a la map) but also flattens them into a single list which is easier to use.

But what if you need all the prices of the products or their sum?

val receipts = store.receipts // fetch the receipts
val allProductsEarnings = receipts.flatMap { it.products }
      .map { it.price }
      .sumByDouble { it }
println(allProductsEarnings)

You can chain Kotlin collection operators and create really clean and readable transformation operations.

Here, you first flattened and mapped the receipts to products. Then you mapped it to the price, which you ultimately summed. You can also use sumByDouble { it.price } in place of the map saving you one precious line of code.

Pretty neat, huh? :]

Filtering and Grouping Data

Each store has to keep a record of all the paid and unpaid receipts. You can do this, using filtering and grouping operators, by adding this code to main():

// filtering by condition
val paidReceipts = receipts.filter { it.isPaid }
println(paidReceipts)

// grouping values by condition
val paidUnpaid = receipts.partition { it.isPaid }
val (paid, unpaid) = paidUnpaid
println(paid)
println(unpaid)

val groupedByWorker = receipts.groupBy { it.seller } // Map<Worker, List<Receipt>>
println(groupedByWorker)

You can filter out one partition of the results by using filter() and providing a lambda to filter by. The lambda takes each element T and uses the expression in the curly braces to validate if it should be collected.

partition(), however, takes the same lambda but returns a pair of elements that match the predicate and elements that don’t. Finally, you can use groupBy() and provide a value matcher in the lambda, which it will use to compare to other elements of a list and group the ones which match the value with hashCode().

Here you used a Worker to group the values and, in the end, you receive a map holding the list of all the receipts for each of the workers.

Validating Data

Sometime you don’t need to filter data but instead need to check if the data conforms to a condition. For example, you might want to check if all the receipts are paid. You can do that as well:

  val areThereNoReceipts = receipts.isEmpty() // also isNotEmpty
  val areAllPaid = receipts.all { it.isPaid }
  val nonePaid = receipts.none { it.isPaid }
  val isAtLeastOnePaid = receipts.any { it.isPaid }

The all(), none() and other functions allow you to return a boolean to see if any, all or none of the elements match the given condition.

Looking Up Data

You also have various ways to look up data:

  val receiptByIndex = receipts[0] // receipt.get(0)
  val firstPaidReceipt = receipts.first { it.isPaid } // will crash if there is none
  val firstPaidReceiptOrNull = receipts.firstOrNull { it.isPaid } // either is paid, or null
  val lastByPredicate = receipts.last { !it.isPaid } // last which is not paid

These functions help you look up data beyond indices, as you can add different logic to the conditions you’re looking for.

Where To Go From Here

Congratulations! You’ve learned the basics of Kotlin collections.

As you can see by the length of this tutorial, Kotlin collections is a huge topic! And even though you learned a lot along the way, there is still much more to cover.

If you’re hungry for more, you may want to check out the official documentation to learn about Sequences.

There are also a few more examples in the projects, so be sure to download them using the Download Materials button, on top or the bottom of the page. Please join the discussion in the comments and the forum and ask any questions you have.

Hope you have a lovely time working with Kotlin collections!

Average Rating

5/5

Add a rating for this content

3 ratings

Contributors

Comments