Difference between map and function returning when in Kotlin

100 views Asked by At

Is there any difference between using map and using function if everything is known at compile time? Maximal number of items will never be higher than 200, and most of the time it would be below 10.

Example:

val mappings = mapOf(
    "PL" to "Poland",
    "EN" to "England",
    "DE" to "Germany",
    "US" to "United States of America",
)

fun mappingsFunc(code: String): String? {
    return when (code) {
        "PL" -> "Poland"
        "EN" -> "England"
        "DE" -> "Germany"
        "US" -> "United States of America"
        else -> null
    }
}

fun main() {
    println(mappings["PL"]!!)
    println(mappingsFunc("US")!!)
}

Playground link.

2

There are 2 answers

0
Fureeish On BEST ANSWER

Practically, there is no difference, especially since you are using immutable Maps (as opposed to MutableMap). I used Amazon's corretto-18 to compile your code and inspected the bytecode. Those are the conclusions I got from doing so:

println(mappings["PL"]!!) compiles to the following bytecode:

LINENUMBER 19 L0
    GETSTATIC MainKt.mappings : Ljava/util/Map;
    LDC "PL"
    INVOKEINTERFACE java/util/Map.get (Ljava/lang/Object;)Ljava/lang/Object; (itf)
    DUP
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNull (Ljava/lang/Object;)V
    ASTORE 0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 0
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V

So it:

  1. Accesses the global mappings object.
  2. Invokes .get() on it.
  3. Checks if the result is not null.
  4. Accesses the global System.out object.
  5. Invokes .println() on it by passing the String gotten from the map.

In case of println(mappingsFunc("US")!!), we have:

LINENUMBER 20 L1
    LDC "US"
    INVOKESTATIC MainKt.mappingsFunc (Ljava/lang/String;)Ljava/lang/String;
    DUP
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNull (Ljava/lang/Object;)V
    ASTORE 0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 0
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V

which differs only in the first two operations. Instead of accessing any object and calling its method, it simply invokes the mappingsFunc() method. Let us inspect what it does exactly:

public final static mappingsFunc(Ljava/lang/String;)Ljava/lang/String;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
    // annotable parameter count: 1 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "code"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 9 L1
    ALOAD 0
    ASTORE 1
    ALOAD 1
    INVOKEVIRTUAL java/lang/String.hashCode ()I
    LOOKUPSWITCH
      2177: L2
      2217: L3
      2556: L4
      2718: L5
      default: L6
   L2
    LINENUMBER 12 L2
   FRAME APPEND [java/lang/String]
    ALOAD 1
    LDC "DE"
    INVOKEVIRTUAL java/lang/String.equals (Ljava/lang/Object;)Z
    IFEQ L6
    GOTO L7
   L3
    LINENUMBER 11 L3
   FRAME SAME
    ALOAD 1
    LDC "EN"
    INVOKEVIRTUAL java/lang/String.equals (Ljava/lang/Object;)Z
    IFEQ L6
    GOTO L8
   L4
    LINENUMBER 10 L4
   FRAME SAME
    ALOAD 1
    LDC "PL"
    INVOKEVIRTUAL java/lang/String.equals (Ljava/lang/Object;)Z
    IFEQ L6
    GOTO L9
   L5
    LINENUMBER 13 L5
   FRAME SAME
    ALOAD 1
    LDC "US"
    INVOKEVIRTUAL java/lang/String.equals (Ljava/lang/Object;)Z
    IFEQ L6
    GOTO L10
   L9
    LINENUMBER 10 L9
   FRAME SAME
    LDC "Poland"
    GOTO L11
   L8
    LINENUMBER 11 L8
   FRAME SAME
    LDC "England"
    GOTO L11
   L7
    LINENUMBER 12 L7
   FRAME SAME
    LDC "Germany"
    GOTO L11
   L10
    LINENUMBER 13 L10
   FRAME SAME
    LDC "United States of America"
    GOTO L11
   L6
    LINENUMBER 14 L6
   FRAME SAME
    LDC "Unknown"
   L11
    LINENUMBER 9 L11
   FRAME SAME1 java/lang/String
    ARETURN
   L12
    LOCALVARIABLE code Ljava/lang/String; L0 L12 0
    MAXSTACK = 2
    MAXLOCALS = 2

Lots of code, so let us inspect the only relevant part that we should pay attention to:

L1
    LINENUMBER 9 L1
    ALOAD 0
    ASTORE 1
    ALOAD 1
    INVOKEVIRTUAL java/lang/String.hashCode ()I
    LOOKUPSWITCH
      2177: L2
      2217: L3
      2556: L4
      2718: L5
      default: L6
   L2
    LINENUMBER 12 L2
   FRAME APPEND [java/lang/String]
    ALOAD 1
    LDC "DE"
    INVOKEVIRTUAL java/lang/String.equals (Ljava/lang/Object;)Z
    IFEQ L6
    GOTO L7

Before analyzing it further, it's worth to note that branches L3, L4 and L5 are basically identical to L2 - they just differ in a given String literal.

The above bytecode invokes String::hashCode() and then String::equals to pick the correct branch.

Which is... basically the same thing as what Map::get() does.

In conclusion, those constructs are almost equivalent, with a single exception that the memory consumption and initial creation of the mapping object takes some time and memory. A negligible amount, though.

In terms of recommendation, it depends on the scope of the usage. If the functions is local and used in a very limited scope, I would probably stick with the when expression. However, if those options are loaded from some external config (and potentially need some precomputation), you ough to use the Map approach.

0
Nikita Chegodaev On

Yes, there is a difference. When you are using a map the JVM will instantiate an additional object (mappings), it will take some additional memory. And operator function map.get will provide the result based on key object hash code. So for user there is no difference but not for the JVM.