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!

***

RATE THIS ARTICLE NOW

Runtastic Tech Team We are made up of all the tech departments at Runtastic like iOS, Android, Web, Infrastructure, DataEngineering, etc. We’re eager to tell you how we work and what we have learned along the way. View all posts by Runtastic Tech Team »

Leave a Reply