Introduction
In this post I’ll dive deep into list, set and map management in Kotlin programming language.
The Kotlin Standard Library provides a comprehensive set of tools for managing collections.
You can test the code snippets I’ll provide during the article in three ways:
- Google online editor: Kotlin Playground
- Official Kotlin Online Editor
- JetBrains’ IntelliJ IDEA (Ultimate or Community Edition)
The most relevant types of collections are:
- List: collection of items with a specific order (the same elements can occur several times)
- Set: list of unique elements
- Map: set of key-value pairs
Collection
is the root of the hierarchy.
There are two types them in Kotlin:
List
: read-only (cannot be modified after creation)MutableList
: can be modified after creation
lambda and function types
A lambda expression is a function with no name that can immediately be used as an expression. Lambdas allows you:
-
store functions in variables and classes,
-
pass functions as arguments,
-
return functions.
Function types define a specific type of function based on its input parameters and return value. The format is: (T) -> K
. A function with (Int) -> Int
function type must take in a parameter of type Int
and return a value of type Int
. The parameters are listed in parentheses (separated by commas if there are multiple parameters). You could even store a lambda into a variable.
val square: (Int) -> Int = { a: Int -> a * a }
println(square(5)) // 25
Please Note: lambdas usually have a single parameter, so Kotlin allows you to use the special identifier it
as a shorthand.
val square: (Int) -> Int = { it * it }
Using a lambda to set the click listener on an Android app’s view (like a Button
) is convenient shorthand.
SAM conversion
Let’s take the example below: Kotlin applies a Single Abstract Method conversion to convert the lambda into an IntPredicate
object which implements the single abstract method accept()
.
fun interface IntPredicate {
fun accept(i: Int): Boolean
}
fun main() {
// create an instance of a class
val isEven = object : IntPredicate {
override fun accept(i: Int): Boolean {
return i % 2 == 0
}
}
// create an instance using lambda
val isEvenLambda = IntPredicate { it % 2 == 0 }
println(isEven.accept(8)) // true
println(isEvenLambda.accept(8)) // true
}
Higher-order function
Higher-order function means passing a function (a lambda) to another function, or returning a function from another function.
Please Note: When comparing two objects for sorting, the convention is to return:
- a value less than
0
if the first object is less than the second, 0
if they are equal,- a value greater than
0
if the first object is greater than the second
The snippet below will better understood after reading the whole blog post.
val petNames = listOf("Kitty", "Doggo", "Joe", "Eleonore")
// use the default sort function
val sortedPets = petNames.sorted()
// names are in order of increasing length
val lambdaPets = petNames.sortedWith {pet1: String, pet2: String -> pet1.length - pet2.length}
println(sortedPets) // [Doggo, Eleonore, Joe, Kitty]
println(lambdaPets) // [Joe, Kitty, Doggo, Eleonore]
vararg modifier
Before diving deep into List
V.S. MutableList
datatype, let’s see a couple of Kotlin technical aspects.
vararg
modifier allows you to pass a variable number of arguments of the same type into a function or constructor. In that way, you can supply the different vegetables as individual strings instead of a list.
class Cat(vararg val nicknames: String) {
override fun toString(): String {
// var msg = "Cat [ "
// for(nick in nicknames) msg += "$nick "
// msg += "]"
// return msg
return "Cat: ${nicknames.joinToString()}"
}
}
fun main() {
val cat = Cat("Kitty", "Ketty", "Carl")
println(cat.toString())
}
for loop
Let’s also see how the for loop works in Kotlin.
fun main() {
val nums = listOf(-1, -5, 3, 0, 2, 22, 45)
// iterate over items in a list
for (item in nums) print("$item \t")
// console result:
// -1 -5 3 0 2 22 45
// ----------------------------------------
// range of characters in an alphabet
for (item in 'b'..'g') {
print("$item \t")
}
// console result:
// b c d e f g
// ----------------------------------------
// range of numbers
var sum = 0
for (item in 1..5) {
sum += item
println("item = $item ~ sum = $sum")
}
// console result:
/* item = 1 ~ sum = 1
item = 2 ~ sum = 3
item = 3 ~ sum = 6
item = 4 ~ sum = 10
item = 5 ~ sum = 15
*/
// ----------------------------------------
// range of numbers going backward
for (item in 5 downTo 1)
print("$item \t")
// console result:
// 5 4 3 2 1
// ----------------------------------------
// range of numbers with a different step size
for (item in 10..20 step 2) print("$item \t")
// console result:
// 10 12 14 16 18 20
}
All the snippets below assumes the code runs in a
main()
function.
List datatype
Collection interface is a generic collection of elements, whose methods support read-only access to the collection. List interface inherits from Collection and is defined as a generic ordered collection of elements.
You always must specify the type of elements that the list can contain. For instance, List<Int>
holds a list of integers. A list can also holds programmer-defined classes.
To fill a list with elements, use the listOf() method.
val nums: List<Int> = listOf(1, 2, 3)
If the variable type can be inferred based on the value on the right hand side of the assignment, then you can omit the data type.
val nums = listOf(1, 2, 3)
Access elements
The syntax is similar to access an element is similar to other programing language. Place the index between square brackets or use the get() method.
Please Note: Remember that indexes starts at 0
.
val nums = listOf(1, 2, 3)
val el_1 = nums[0]
val el_2 = nums.get(0)
// el_1 is equal to el_2, both equal 1
Kotlin provides some flavors of get
method like getOrNull() to avoid ArrayIndexOutOfBoundsException
issue.
val nums: List<Int> = listOf(1, 2, 3)
val el = nums.getOrNull(9) // will be null
Or getOrElse(), which returns the value for the given key
, if the value is present and not null
.
Otherwise, returns the result of the defaultValue
function (passed as second parameter).
val el = nums.getOrElse(9, {0}) // returns 0
val el = nums.getOrElse(9, {
println("No element")
})
// prints on the console: "No element"
// returns: kotlin.Unit
The contains() method returns true
if the given element is found in the collection.
Else, return false
.
If you need to check if multiple values are in a collection, consider using containsAll() method.
var el = 3 // element to find
val nums: List<Int> = listOf(1, 2, 3)
// varriables marker with 'var' can be reassigned
var isInNums = nums.contains(el) // returns true
println("$el is inside the list? $isInNums")
el += 5
isInNums = nums.contains(el) // return false
println("$el is inside the list? $isInNums")
The output will be:
3 is inside the list? true
8 is inside the list? false
Kotlin also provides first() and last() methods, which both triggers a NoSuchElementException
if the array is empty.
Manipulate Elements
There are some methods to make operations on list’s elements:
- reduce(): accumulates value starting with the first element and applying
operation
from left to right to current accumulator value and each element.
val nums: List<Int> = listOf(1, 2, 3, 4)
nums.reduce { acc, el -> acc + el} // 10
nums.reduce { acc, el -> acc - el} // -8
nums.reduce { acc, el -> acc * el} // 24
Manipulate List
It’s not possible to change a read-only list. However, there are some operations that return a brand-new list with applied changes. The main methods are:
- reversed() returns a list with elements in reversed order
- shuffled() returns a list with the elements of this list randomly shuffled
- slice() returns a list that contains elements at specified indices
- sorted() returns a list of all elements sorted according to their natural sort order
var nums: List<Int> = listOf(1, 2, 3, 4)
val revNums = nums.reversed() // [4, 3, 2, ]
val shNums = nums.shuffled() // [3, 2, 1, 4]
val sortedNums = shNums.sorted() // [1, 2, 3, 4]
nums: List<Int> = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
var slNums = nums.slice(1..4) // [2, 3, 4, 5]
slNums = nums.slice(2..6) // [3, 4, 5, 6, 7]
slNums = nums.slice(3..10)
// will cause java.lang.IndexOutOfBoundsException
MutableList datatype
MutableCollection interface is a generic collection of elements that supports adding and removing elements. MutableList interface inherits from MutableCollection and is defined as a generic ordered collection of elements that supports adding and removing elements.
You always must specify the type of elements that the list can contain.
For instance, List<Int>
holds a list of integers. A list can also hold programmer-defined classes.
To fill a list with elements, use the mutableListOf() method.
val nums: MutableList<Int> = mutableListOf(1, 2, 3)
If the variable type can be inferred based on the value on the right hand side of the assignment, then you can omit the data type.
// error: Not enough information to infer type variable T
val nums = mutableListOf()
// this works
val nums = mutableListOf(0)
Access elements
You can reference to List’s Access elements slice, since it is done in the same way.
Remember to change the datatype of the list variable into MutableList
.
Add or remove elements
The main functions to edit the mutable list are:
- add() adds the specified element to the end of this list (returns
true
) If passing a single parameter, it is the element to add. If passing two parameter, the 1st is the index and the 2nd is the element (could trigger anIndexOutOfBoundsException
) - addAll() adds all the elements of the given collection to the end of this list
- clear() removes all elements from this collection
- remove() removes a single instance of the specified element from this collection (if present)
It returns
true
if it manage to remove the element,false
otherwise - removeAll() removes all of this collection’s elements that are also contained in the specified collection
- removeAt() removes an element at the specified index from the list
removeIf()
removes all elements from the list that satisfies the given predicate
val nums = mutableListOf(-1, -44, 33, 0, 2, 3)
nums.add(6)
// nums = [-1, -44, 33, 0, 2, 3, 6]
nums.addAll(listOf(5, 2, 6)))
// nums = [-1, -44, 33, 0, 2, 3, 6, 5, 2, 6]
nums.remove(6)
// nums = [-1, -44, 33, 0, 2, 3, 5, 2, 6]
nums.removeAll(listOf(5, 2, 6))
// nums = [-1, -44, 33, 0, 3]
nums.removeIf {
it > 2
}
// nums = [-1, -44, 0]
nums.clear()
// nums = []
Please Note: You can use val
for a mutable list because the num
variable contains a reference to the list, and that reference doesn’t change even if the contents of the list do.
Set and MutableSet datatypes
A Set is a generic unordered collection of elements that doesn’t support duplicate elements. Methods in this interface support only read-only access to the set.
One of the core functions is toSet(), which can be applied to any Iterable (the super class of all the Collection
classes). The returned set preserves the element iteration order of the original array.
val nums = listOf(0, 8, 0, -5, -5, -2, 4, 8, 9, 7, 9, -4, 4)
val sortedNums = nums.sorted()
val mySet = sortedNums.toSet()
println(nums)
println(nums.sorted().toSet())
println(nums.toSet())
The output will be:
[0, 8, 0, -5, -5, -2, 4, 8, 9, 7, 9, -4, 4]
[-5, -4, -2, 0, 4, 7, 8, 9]
[0, 8, -5, -2, 4, 9, 7, -4]
Please Note: order is not significant for a set.
val set1 = setOf(1, 2, 3)
val set2 = setOf(2, 3, 1)
val areEqual = (set1 == set2) // true
print("Sets are equal? ${areEqual}")
As with mathematical sets, in Kotlin you can perform operations like the intersection (∩) or the union (∪) of two sets.
val set1 = setOf(1, 2, 3, 8)
val set2 = setOf(3, 6, 7, 8)
val interSet = set1.intersect(set2)
val unionSet = set1.union(set2)
println("intersection: ${interSet}") // intersection: [3, 8]
println("union: ${unionSet}") // union: [1, 2, 3, 8, 6, 7]
Those function can be called by any Iterable
, but always return a Set
variable.
val list1 = listOf(2, 1, 2, 1, 3, 8, 10)
val mutable2 = mutableSetOf(8, 6, 3, 6, 7, 8)
val interSet = list1.intersect(mutable2)
val unionSet = list1.union(mutable2)
// intersection: [3, 8]
// union: [2, 1, 3, 8, 10, 6, 7]
Map and MutableMap datatypes
Map is a collection that holds pairs of objects (keys and values) and supports retrieving the value corresponding to each key. Map keys are unique. The map holds only one value for each key.
Methods in this interface support only read-only access to the map; read-write access is supported through the MutableMap interface.
In both cases, you can specify the types of both the keys and the values even if they can be easily inferred.
val petWeight = mapOf("Mouse" to 2, "Cat" to 15)
// specify the types
val petWeight = mapOf<String, Int>("Mouse" to 2, "Cat" to 15)
To add more entries to a MutableMap
, use the put function, passing the key and the value.
To remove an entry, use the remove function, passing the key. If you also specify the value as 2nd parameter, the entry will only be removed if the value placed at the given key matches the passed in value.
val petWeight = mutableMapOf("Mouse" to 2, "Cat" to 15)
petWeight.remove("Mouse", 4) // does not remove the element
petWeight.remove("Mouse") // removes the element
petWeight.remove("Mouse", 2) // also removes the element
Please Not: keys are unique, but the values can have duplicates.
Collection Transformation
forEach function uses the special identifier it
, instead of specifying a variable for the current item (as it happens in the for loop). The forEach
performs the given action on each element.
val petWeight = mapOf("Mouse" to 2, "Cat" to 15, "Elephant" to 2000)
petWeight.forEach {
// perform an action
println("${it.key} weight is ${it.value}kg")
}
map function returns a List
containing the results of applying the given transform function to each element in the original array.
Let’s say you want to evaluate the weight of your pets on the Moon:
val petWeight = mapOf("Mouse" to 2, "Cat" to 15, "Elephant" to 2000)
// evaluate pet's weight on the Moon
val moonWeight = petWeight.map {
(it.value / 9.81) * 1.622
}
println(moonWeight)
// [0.3306829765545362, 2.4801223241590216, 330.6829765545362]
Collection Filtering
Filtering conditions are defined by predicates: lambda functions that take a collection element and return a boolean value. They return true
if the given element matches the predicate, otherwise return false
.
filter function returns a List
containing only elements matching the given predicate. Its return type is List<T>
most of the time, except for Map
: it this case it returns another Map
.
val petWeight = mapOf("Hamster" to 1, "Cat" to 15, "Hawk" to 80)
// filter pets whose key respect a given condition
val haPets = petWeight.filter {
it.key.startsWith("Ha")
}
// filter pets whose value respect a given condition
val hoPets = petWeight.filter {
it.value >= 15
}
println(haPets) // {Hamster=1, Hawk=80}
println(hoPets) // {Cat=15, Hawk=80}