Search code examples
androidkotlinequalsequality

Strange value comparison issue in Kotlin, "===" returns true but "==" returns false


I have encountered a very strange value comparison issue in Kotlin that I cannot explain, the following code prints false

data class Foo (
    val a: Byte
)
fun main() {
    val NUM: Byte = 1
    var m: Foo? = Foo(NUM)
    println(m?.a == NUM)
}

But if I change the last line to

println(m?.a === NUM)

or

println(m!!.a == NUM)

it prints true, I'm so confused, anyone could help to explain? Thanks.


Solution

  • The issue only appears in version 1.5.20, while 1.5.10 is not affected.

    This seems to be an issue in the newer kotlin compiler version.

    With some bytecode, we can explain the problem (data class was called Blah, func was called blah).

    This is the bytecode, compiled with 1.5.10, that returns True for println(m?.a == NUM) - everything seems to be fine. We're doing a primitive not equals of the two numbers, which returns False (correct since 1 != 1 is False).

    Compiled from "WtfTest.kt"
    public final class de.sfxr.WtfTest {
      public de.sfxr.WtfTest();
        Code:
           0: aload_0
           1: invokespecial #8                  // Method java/lang/Object."<init>":()V
           4: return
    
      public final void blah();
        Code:
           0: iconst_1
           1: istore_1
           2: new           #14                 // class de/sfxr/Blah
           5: dup
           6: iload_1
           7: invokespecial #17                 // Method de/sfxr/Blah."<init>":(B)V
          10: astore_2
          11: aload_2
          12: astore_3
          13: aload_3
          14: invokevirtual #21                 // Method de/sfxr/Blah.getA:()B
          17: iload_1
          18: istore_3
          19: iload_3
          // PRIMITIVE NOT EQUALS => False
          20: if_icmpne     27                  
          23: iconst_1
          24: goto          28
          27: iconst_0
          28: istore_3
          29: iconst_0
          30: istore        4
          32: getstatic     #27                 // Field java/lang/System.out:Ljava/io/PrintStream;
          35: iload_3
          36: invokevirtual #33                 // Method java/io/PrintStream.println:(Z)V
          39: return
    }
    

    However in Version 1.5.20, the bytecode instructs for an object comparison using the JVMs Intrinsics.areEqual on a boxed Integer with content 1 and a boxed Byte with content 1, which will return False, since it uses equals on Byte. This is the cause of this issue. The compiler devs surely wanted a True at this point.

    But why does this evaluate to false? Here's a snippet of the Byte.equals's description "The result is true if and only if the argument is not null and is a Byte object that contains the same byte value as this object."

    ...and the bytecode for explaination:

    Compiled from "WtfTest.kt"
    public final class de.sfxr.WtfTest {
      public de.sfxr.WtfTest();
        Code:
           0: aload_0
           1: invokespecial #8                  // Method java/lang/Object."<init>":()V
           4: return
    
      public final void blah();
        Code:
           0: iconst_1
           1: istore_1
           2: new           #14                 // class de/sfxr/Blah
           5: dup
           6: iload_1
           7: invokespecial #17                 // Method de/sfxr/Blah."<init>":(B)V
          10: astore_2
          11: aload_2
          12: astore_3
          13: aload_3
          14: invokevirtual #21                 // Method de/sfxr/Blah.getA:()B
          17: invokestatic  #27                 // Method java/lang/Byte.valueOf:(B)Ljava/lang/Byte;
          20: iload_1
          21: invokestatic  #32                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
          // OBJECT COMPARISON VIA JVM ON BOXED BYTE(1) AND BOXED INT(1) => False
          24: invokestatic  #38                 // Method kotlin/jvm/internal/Intrinsics.areEqual:(Ljava/lang/Object;Ljava/lang/Object;)Z    
          27: istore_3
          28: iconst_0
          29: istore        4
          31: getstatic     #44                 // Field java/lang/System.out:Ljava/io/PrintStream;
          34: iload_3
          35: invokevirtual #50                 // Method java/io/PrintStream.println:(Z)V
          38: return
    }
    

    UPDATE

    The guys from jetbrains commented the issued ticket https://youtrack.jetbrains.com/issue/KT-47717 with "That's definitely a bug." and priority major.