80

I have the following data class

data class PuzzleBoard(val board: IntArray) {
    val dimension by lazy { Math.sqrt(board.size.toDouble()).toInt() }
}

I read that data classes in Kotlin get equals()/hashcode() method for free.

I instantiated two objects.

val board1 = PuzzleBoard(intArrayOf(1,2,3,4,5,6,7,8,0))
val board2 = PuzzleBoard(intArrayOf(1,2,3,4,5,6,7,8,0))

But still, the following statements return false.

board1 == board2
board1.equals(board2)
iknow
  • 8,358
  • 12
  • 41
  • 68
Vaibhav
  • 971
  • 1
  • 7
  • 8
  • 2
    It's never too late to comment, For data class equals contract is generated on the base of paramaters passed in the primary constructor. For example `board1 = PuzzleBoard("Board", 20)` and `board2 = PuzzleBoard("Board", 20)` would return `true` for `board1 == board2`. In you case the equal contract would be generated based on array memory address, that is the reason you are getting false. – Muhammad Babar Apr 05 '21 at 09:45

5 Answers5

111

In Kotlin data classes equality check, arrays, just like other classes, are compared using equals(...), which compares the arrays references, not the content. This behavior is described here:

So, whenever you say

  • arr1 == arr2

  • DataClass(arr1) == DataClass(arr2)

  • ...

you get the arrays compared through equals(), i.e. referentially.

Given that,

val arr1 = intArrayOf(1, 2, 3)
val arr2 = intArrayOf(1, 2, 3)

println(arr1 == arr2) // false is expected here
println(PuzzleBoard(arr1) == PuzzleBoard(arr2)) // false too


To override this and have the arrays compared structurally, you can implement equals(...)+hashCode() in your data class using Arrays.equals(...) and Arrays.hashCode(...):
override fun equals(other: Any?): Boolean{
    if (this === other) return true
    if (other?.javaClass != javaClass) return false

    other as PuzzleBoard

    if (!Arrays.equals(board, other.board)) return false

    return true
}

override fun hashCode(): Int{
    return Arrays.hashCode(board)
}

This code is what IntelliJ IDEA can automatically generate for non-data classes.

Another solution is to use List<Int> instead of IntArray. Lists are compared structurally, so that you won't need to override anything.

hotkey
  • 140,743
  • 39
  • 371
  • 326
  • 3
    For more information on testing value equality of arrays in Kotlin see here: http://stackoverflow.com/q/35272761/1402641 – Marcin Koziński May 30 '16 at 11:34
  • 5
    And if you'd like to avoid writing your own equals method, consider using `List` instead of `IntArray`. It won't be as memory efficient but unless you're creating very large arrays the impact will be nominal. – mfulton26 May 30 '16 at 12:01
  • I don't know much about Kotlin but wouldn't using `if(other !is thisClass) return false` be better? Then you wouldn't need the as thisClass in the next line because the compiler could now smartcast, right? – Zackline Dec 03 '16 at 11:35
  • How can I find and what shortcut (if it differs in IntelliJ) directs me to, the implementation of this method? – Chad Mx Dec 26 '17 at 13:32
  • @ChadMx, in IntelliJ IDEA, the shortcut to generate members is `Alt`+`Insert` by default. Usually, you can find an action's shortcut by pressing `Ctrl`+`Shift`+`A` and typing the action name. – hotkey Dec 26 '17 at 14:19
  • @hotkey Thank you, but I guess I wasn't clear. What I meant was, how do I see the generated code for a data class, if it is possible? – Chad Mx Dec 26 '17 at 19:35
  • 1
    @ChadMx, oh, I really misunderstood you. Since the members are generated not as Kotlin sources but directly as the JVM bytecode, It's only possible to inspect the generated bytecode, [see how](https://stackoverflow.com/questions/35538049/kotlin-bytecode-how-to-analyze-in-intellij-idea). You will find the generated methdos inside the data class. – hotkey Dec 26 '17 at 19:40
  • @hotkey, if I have a data class `PuzzleBoard(val name: String, val board: IntArray)`, how should I override `hashCode()`? – Jack Guo Jun 13 '18 at 22:26
  • I think this implementation is but it requires to check all properties – Michał Ziobro Sep 04 '18 at 10:10
  • @hotkey Do you what is the reason for this behavior of Array's equals method? – Diego Marin Santos Apr 18 '20 at 21:43
  • An overlooked trap here is that if your class contains any doubles or floats, the default `equals` method will not behave as predicted either. – Hakanai Aug 23 '21 at 21:35
  • Where's the data class in your example? – Philip Rego Jul 06 '22 at 03:24
10

Kotlin implementation:

override fun equals(other: Any?): Boolean {
    when (other) {
        is User -> {
            return this.userId == other.userId &&
                    this.userName == other.userName
        }
        else -> return false
    }
}
VIVEK CHOUDHARY
  • 468
  • 5
  • 8
9

For Data classes in Kotlin, hashcode() method will generate and return the same integer if parameters values are same for both objects.

val user = User("Alex", 1)
val secondUser = User("Alex", 1)
val thirdUser = User("Max", 2)

println(user.hashCode().equals(secondUser.hashCode()))
println(user.hashCode().equals(thirdUser.hashCode()))

Running this code will return True and False as when we created secondUser object we have passed same argument as object user, so hashCode() integer generated for both of them will be same.

also if you will check this:

println(user.equals(thirdUser))

It will return false.

As per hashCode() method docs

open fun hashCode(): Int (source)

Returns a hash code value for the object. The general contract of hashCode is:

Whenever it is invoked on the same object more than once, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified.

If two objects are equal according to the equals() method, then calling the hashCode method on each of the two objects must produce the same integer result.

For more details see this discussion here

Community
  • 1
  • 1
TapanHP
  • 5,969
  • 6
  • 37
  • 66
9

In Kotlin, equals() behaves differently between List and Array, as you can see from code below:

val list1 = listOf(1, 2, 3)
val list2 = listOf(1, 2, 3)

val array1 = arrayOf(1, 2, 3)
val array2 = arrayOf(1, 2, 3)

//Side note: using a==b is the same as a.equals(b)

val areListsEqual = list1 == list2// true
val areArraysEqual = array1 == array2// false

List.equals() checks whether the two lists have the same size and contain the same elements in the same order.

Array.equals() simply does an instance reference check. Since we created two arrays, they point to different objects in memory, thus not considered equal.

Since Kotlin 1.1, to achieve the same behavior as with List, you can use Array.contentEquals().

Source: Array.contentEquals() docs ; List.equals() docs

PHPirate
  • 7,023
  • 7
  • 48
  • 84
Mauro Banze
  • 1,898
  • 15
  • 11
0

The board field in the PuzzleBoard class is an IntArray, when compiled it is turned into a primitive integer array. Individual array elements are never compared when checking the equality of primitive integer arrays. So calling equals on int array returns false as they are pointing to different objects. Eventually, this results in getting false in the equals() method, even though array elements are the same.

Byte code check

Looking at the decompiled java byte code, the Kotlin compiler generates some functions of data classes for us. This includes,

  1. copy() function
  2. toString() function - takes form ClassName(var1=val1, var2=val2, ...)
  3. hashCode() function
  4. equals() function

Hash code is generated by adding the hash code of individual variables and multiplying by 31. The reason for multiplying is that it can be replaced with the bitwise operator and according to experimental results, 31 and other numbers like 33, 37, 39, 41, etc. gave fever clashes when multiplied.

Take a look at decompiled java byte code of the Kotlin class PuzzleBoard which reveals the secrets of data classes.

@Metadata(
   mv = {1, 7, 1},
   k = 1,
   d1 = {"\u0000(\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u0015\n\u0002\b\u0004\n\u0002\u0010\b\n\u0002\b\u0007\n\u0002\u0010\u000b\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0000\b\u0086\b\u0018\u00002\u00020\u0001B\r\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0002\u0010\u0004J\t\u0010\r\u001a\u00020\u0003HÆ\u0003J\u0013\u0010\u000e\u001a\u00020\u00002\b\b\u0002\u0010\u0002\u001a\u00020\u0003HÆ\u0001J\u0013\u0010\u000f\u001a\u00020\u00102\b\u0010\u0011\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\u0012\u001a\u00020\bHÖ\u0001J\t\u0010\u0013\u001a\u00020\u0014HÖ\u0001R\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006R\u001b\u0010\u0007\u001a\u00020\b8FX\u0086\u0084\u0002¢\u0006\f\n\u0004\b\u000b\u0010\f\u001a\u0004\b\t\u0010\n¨\u0006\u0015"},
   d2 = {"Lcom/aureusapps/androidpagingbasics/data/PuzzleBoard;", "", "board", "", "([I)V", "getBoard", "()[I", "dimension", "", "getDimension", "()I", "dimension$delegate", "Lkotlin/Lazy;", "component1", "copy", "equals", "", "other", "hashCode", "toString", "", "androidpagingbasics_debug"}
)
public final class PuzzleBoard {
   @NotNull
   private final Lazy dimension$delegate;
   @NotNull
   private final int[] board;

   public final int getDimension() {
      Lazy var1 = this.dimension$delegate;
      Object var3 = null;
      return ((Number)var1.getValue()).intValue();
   }

   @NotNull
   public final int[] getBoard() {
      return this.board;
   }

   public PuzzleBoard(@NotNull int[] board) {
      Intrinsics.checkNotNullParameter(board, "board");
      super();
      this.board = board;
      this.dimension$delegate = LazyKt.lazy((Function0)(new Function0() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke() {
            return this.invoke();
         }

         public final int invoke() {
            return (int)Math.sqrt((double)PuzzleBoard.this.getBoard().length);
         }
      }));
   }

   @NotNull
   public final int[] component1() {
      return this.board;
   }

   @NotNull
   public final PuzzleBoard copy(@NotNull int[] board) {
      Intrinsics.checkNotNullParameter(board, "board");
      return new PuzzleBoard(board);
   }

   // $FF: synthetic method
   public static PuzzleBoard copy$default(PuzzleBoard var0, int[] var1, int var2, Object var3) {
      if ((var2 & 1) != 0) {
         var1 = var0.board;
      }

      return var0.copy(var1);
   }

   @NotNull
   public String toString() {
      return "PuzzleBoard(board=" + Arrays.toString(this.board) + ")";
   }

   public int hashCode() {
      int[] var10000 = this.board;
      return var10000 != null ? Arrays.hashCode(var10000) : 0;
   }

   public boolean equals(@Nullable Object var1) {
      if (this != var1) {
         if (var1 instanceof PuzzleBoard) {
            PuzzleBoard var2 = (PuzzleBoard)var1;
            if (Intrinsics.areEqual(this.board, var2.board)) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }
}
UdaraWanasinghe
  • 2,622
  • 2
  • 21
  • 27