Inside Kotlin Collections. Part 2

by Andrew Khrystian,
Software Engineer Android @Runtastic
Hope you enjoyed the first part of my story about Kotlin collections. In the second part, I’m going to talk about sets and maps. I will discuss the following topics:
- What extends these collections
- What about mutability and immutability
- How to create these collections
Set and MutableSet
1. Structure
They have the same structure as the List and Mutable list I’ve already described before. Now, I’d like to say a few words about immutable sets. Let’s extend the Set interface and look at the Kotlin bytecode.
class ExampleSet : Set<Unit> { override fun equals(other: Any?): Boolean = other is Set<*> && other.isEmpty() override fun hashCode(): Int = 0 override fun toString(): String = "[]" override val size: Int get() = 0 override fun isEmpty(): Boolean = true override fun contains(element: Unit): Boolean = false override fun containsAll(elements: Collection<Unit>): Boolean = elements.isEmpty() override fun iterator(): Iterator<Unit> = null!! }
And what we have in the bytecode:
public final class com/kotlin/android/ExampleSet implements java/util/Set kotlin/jvm/internal/markers/KMappedMarker public removeAll(Ljava/util/Collection;)Z NEW java/lang/UnsupportedOperationException DUP LDC "Operation is not supported for read-only collection" INVOKESPECIAL java/lang/UnsupportedOperationException.<init> (Ljava/lang/String;)V ATHROW MAXSTACK = 3 MAXLOCALS = 2 // access flags 0x1 public clear()V NEW java/lang/UnsupportedOperationException DUP LDC "Operation is not supported for read-only collection" INVOKESPECIAL java/lang/UnsupportedOperationException.<init> (Ljava/lang/String;)V ATHROW MAXSTACK = 3 MAXLOCALS = 1 // access flags 0x1 // signature (Ljava/util/Collection<+Lkotlin/Unit;>;)Z // declaration: boolean addAll(java.util.Collection<? extends kotlin.Unit>) public addAll(Ljava/util/Collection;)Z NEW java/lang/UnsupportedOperationException DUP LDC "Operation is not supported for read-only collection" INVOKESPECIAL java/lang/UnsupportedOperationException.<init> (Ljava/lang/String;)V ATHROW MAXSTACK = 3 MAXLOCALS = 2
So, as you can see, there are also changing methods in the byte code, but they throw “Operation is not supported for read-only collection” (the same as for the immutable lists).
2. Creation
Here, everything is more interesting. To create immutable sets, you need to use these methods:
fun <T> setOf(): Set<T> fun <T> emptySet(): Set<T> fun <T> setOf(vararg elements: T): Set<T>
The first two methods return EmptySet object as a result. The third method calls the function:
return when (size) { 0 -> emptySet() 1 -> setOf(this[0]) else -> toCollection(LinkedHashSet<T>(mapCapacity(size))) } }
If we have one item that returns a Java immutable set using the singleton() method
@JvmVersion public fun <T> setOf(element: T): Set<T> = java.util.Collections.singleton(element)
then we have multiple elements. This function returns a Java LinkedHashSet as a realization. But the return type is kotlin.Set, so we have an immutable set.
Maps and Mutable maps
1. Structure
As above, maps can be “mutable” or “immutable”. There is one big difference between them. Basically, a map is not a collection because it doesn’t extend a collection or an iterable. But a map uses collections inside, and a map interface is the part of kotlin.collections package. A map is actually an interface that has a pair interface inside, entries Set, keys Set and values Collection. An immutable map doesn’t have any “changing methods”, this is already an existing realisation of immutable map.
private object EmptyMap : Map<Any?, Nothing>, Serializable { private const val serialVersionUID: Long = 8246714829545688274 override fun equals(other: Any?): Boolean = other is Map<*,*> && other.isEmpty() override fun hashCode(): Int = 0 override fun toString(): String = "{}" override val size: Int get() = 0 override fun isEmpty(): Boolean = true override fun containsKey(key: Any?): Boolean = false override fun containsValue(value: Nothing): Boolean = false override fun get(key: Any?): Nothing? = null override val entries: Set<Map.Entry<Any?, Nothing>> get() = EmptySet override val keys: Set<Any?> get() = EmptySet override val values: Collection<Nothing> get() = EmptyList private fun readResolve(): Any = EmptyMap }
So, it’s pretty close to a Java map, but there are also some different implementations. Keys and values are not functions; they are override vals with Kotlin getter. And there is no keySet method. Obviously, we can find it in the bytecode,
public final bridge keySet()Ljava/util/Set; L0 LINENUMBER 40 L0 ALOAD 0 INVOKEVIRTUAL android/sample/Generator$EmptyMap.getKeys ()Ljava/util/Set; ARETURN MAXSTACK = 1 MAXLOCALS = 1
As you can see, getKeys was called in keysSet function to return a set of keys, which makes it easy to call this method from your Java code. Furthermore it contains all hidden methods that are responsible for the mutability.
public putAll(Ljava/util/Map;)V NEW java/lang/UnsupportedOperationException DUP LDC "Operation is not supported for read-only collection" INVOKESPECIAL java/lang/UnsupportedOperationException.<init> (Ljava/lang/String;)V ATHROW MAXSTACK = 3 MAXLOCALS = 2 // access flags 0x1 public replace(Ljava/lang/Object;Ljava/lang/Void;)Ljava/lang/Void; NEW java/lang/UnsupportedOperationException DUP LDC "Operation is not supported for read-only collection" INVOKESPECIAL java/lang/UnsupportedOperationException.<init> (Ljava/lang/String;)V ATHROW MAXSTACK = 3 MAXLOCALS = 3 ....
2. Creation
To create immutable maps, you can use these methods:
- emptyMap<K, V>() — returns EmptyMap object
- mapOf<K, V>() — calls emptyMap()
- mapOf<K, V>(pair: Pair<K, V>) — returns java.util. singletonMap()
- mapOf<K, V>(vararg pairs: Pair<K, V>) — returns singletonMap() if the pairs’ size is “1”. If the pairs.size is more than 1, it calls pairs.toMap(LinkedHashMap()). Pairs is actually an array; toMap() function just puts all the array items into the destination (LinkedHashMap).
Creating a mutable map looks a lot simpler:
- mutableMapOf<K, V>() returns empty LinkedHashMap
- mutableMapOf<K, V>(varags pairs: Pair<K, V>) the same as mutableMapOf<K, V>(), but here the logic is to put pairs into the LinkedHashMap
- hashMapOf<K, V>() returns empty HashMap
- hashMapOf<K, V>(varargs pairs: Pair<K, V>) returns HashMap with items
- linkedMapOf<K,V>() the same as mutableMapOf<K, V>() but the return type is LinkedHashMap
- linkedMapOf<K,V>(varags pairs: Pair<K, V>) the same as mutableMapOf<K, V>(varags pairs: Pair<K, V>), but the return type is LinkedHashMap
When I used Java for Android development, I guess about 70% of the maps I used were HashMaps. So, here, if you need HashMap<K, V> in Kotlin, you have to remember that mutableMap() is not HashMap. So, you need to use another method for creating these maps. And, of course, you can still use Java constructors for creating other realizations of the map interface. Remember that they all will be mutable.
In my next article, I’m going to show you all the functional power of Kotlin collections. You’re always welcome to ask me if you have any questions. Enjoy!
***