We developers usually write software that uses collections on a daily basis, code that deals with things such as fetching a list of products from the database, writing a set of data to a file like CSV among other things are quite common. So let’s take a look on how Kotlin Collections work and how do they compare to Java Collections API.
Read-Only and Mutable collections
As you can see in this figure, Kotlin has a mutable counterpart which inherits and adds functions such as fun add(element: E)
and fun remove(element: E)
for every read-only interface. ArrayList, HashSet, LinkedHashSet, LinkedHashMap and HashMap classes are treated as implementations of MutableList, MutableSet and MutableMap respectively and they are very similar to the ones you find in Java Collections API, in fact, these are the same but marked as typealias
by Kotlin, as seen in the TypeAliases.kt runtime library file:
@SinceKotlin("1.1") public actual typealias ArrayList<E> = java.util.ArrayList<E>
@SinceKotlin("1.1") public actual typealias LinkedHashMap<K, V> = java.util.LinkedHashMap<K, V>
@SinceKotlin("1.1") public actual typealias HashMap<K, V> = java.util.HashMap<K, V>
@SinceKotlin("1.1") public actual typealias LinkedHashSet<E> = java.util.LinkedHashSet<E>
@SinceKotlin("1.1") public actual typealias HashSet<E> = java.util.HashSet<E>
Collections-creation functions
Type | Function | Mutability |
---|---|---|
List | listOf() |
Read-Only |
MutableList | arrayListOf() |
Mutable |
Set | setOf() |
Read-Only |
MutableSet | hashSetOf() |
Mutable |
Map | mapOf() |
Read-Only |
MutableMap | hashMapOf() |
Mutable |
These are the top-level functions used to create collections in Kotlin, example:
val list = listOf(1, 2, 3)
val mutableArrayList = arrayListOf(1, 2, 3)
val map = mapOf(1 to "one", 2 to "two") //key is an Int valued by a String
val mutableHashMapOf = hashMapOf(1 to "one", 2 to "two")
val set = setOf(1, 2, 3)
val mutableHashSetOf = hashSetOf(1, 2, 3)
If we were to create an immutable List in Java, we would have to wrap our collection in another one like this:
List<String> originalMutableList = new ArrayList<>();
List<String> unmodifiableList = Collections.unmodifiableList(originalMutableList);
// Collections.unmodifiableMap(...);
// Collections.unmodifiableSet(...);
// and so on...
We still would have methods such as add
, remove
and clear
tough, as it's part of the List interface and there's no such distinction between read-only and mutable lists in standard Java.
Filter, map, flatMap and zip
Filter
Filtering lists in Kotlin is simple and looks very similar to the way we work with Java 8 Streams, Kotlin's Iterable interface has a method named filter that accepts a predicate in order to process the collection.
val countries = listOf("Brazil", "Argentina", "Germany")
val filteredCountries = countries.filter { it.length > 6 }
//'it' means the instance itself, in that case a String representing a country name
println(filteredCountries) // prints [Argentina, Germany]
Map and flatMap
Pretty much like filtering, Map and flatMap operations remain similar to the ones found in Java 8 when working with Streams.
Map:
val countries = listOf("Brazil", "Argentina", "Germany")
val countriesToLowerCase = countries.map(String::toLowerCase) //same as countries.map { it.toLowerCase() }
println(countriesToLowerCase) //prints [brazil, argentina, germany]
flatMap:
val countries = listOf("Brazil", "Argentina", "Germany")
val listOfCharacters = countries.flatMap { it.toList() } // converts to a List of chars
println(listOfCharacters) // prints [B, r, a, z, i, l, A, r, g, e, n, t, i, n, a, G, e, r, m, a, n, y]
zip:
val countries = listOf("Brazil", "Argentina", "Germany")
val cities = listOf("Aracaju", "Ushuaia", "Berlin")
val zippedPairs = cities.zip(countries) //creates a new list containing an ordered pair of the elements of these two lists
println(zippedPairs) // prints [(Aracaju, Brazil), (Ushuaia, Argentina), (Berlin, Germany)]
Please note that unlike Java 8 Streams, Kotlin Lists are eagerly evaluated, which means there's no need to use terminal methods such as collect(), forEach(), etc... to run intermediate operations.
Sequences
Sequences are like lazily evaluated collections, which means that when we invoke functions such as filter and map to it, these will add intermediate steps to be executed when requested, that is, when a terminal function is called. Examples of terminal functions are toList()
, first()
, sumBy()
and max()
. Sequences are a good choice when the size of the collection is not known in advance, or when the collection is too large to be processed or kept in memory at once.
Despite the similarities, I'd favor Sequences over Streams as it offers a more idiomatic, homogeneous and cleaner code when working with Kotlin.
Another important thing to keep in mind is that at the time of writing this article Kotlin targets JDK 1.6+, which means that if you are using Java 6 working with Sequences might be your only choice.
val cities = listOf("Aracaju", "Ushuaia", "Berlin")
val sequence = cities.asSequence().filter { it.startsWith("A") }.map { it.toUpperCase() }
println(sequence.first()) //prints ARACAJU
Kotlin is a pragmatic language created to solve real world problems, it aims to provide popular features that have proven to be successful. With collections, for instance, we have built-in well segregated components for both mutable and immutable concepts.
Hope you enjoyed, thanks a lot! (this is my first article by the way :D).
Top comments (0)