I have a simple data class which stores x and y coordinates of a position. My use case is that a single object of this class will be created and updated, and I need to maintain a set of unique coordinates.
I've simplified my use case in the following code where adding the pos
object directly to the set vs passing the copy of the object result in different behavior (please see the comment in the code).
My initial hunch was that it could be because Java/Kotlin is passing the object by reference and the Set.add
compares on reference. However, that doesn't seem to be true, if I set the pos.x
or pos.y
to any other value then the set.contains
method returns false.
Question:
If the comparison is by reference then why does it fail when setting to a value other than what is given in the below code? If comparison is by hash code then why does the setByCopy
not return true in the original case?
data class Pos(var x: Int = 0, var y: Int = 0)
fun main() {
val pos = Pos(0, 0)
val set = mutableSetOf<Pos>()
val setByCopy = mutableSetOf<Pos>()
pos.x = -9
pos.y = -6
set.add(pos)
setByCopy.add(pos.copy())
println(pos.hashCode())
pos.x = -8
pos.y = -37
println(set.contains(pos)) // true, but expected false.
println(setByCopy.contains(pos)) // false
}
CodePudding user response:
As usual, modifying an element that's already in a set produces undefined behavior. This is not explicitly documented in Kotlin, but carries over from Java, where it's documented:
Great care must be exercised if mutable objects are used as set elements. The behavior of a set is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is an element in the set.
This means that anything can happen: it can work or not work randomly.
CodePudding user response:
You're creating two objects, pos
and then a separate one we'll call pos2
. When you invoke copy()
on an instance of a data class, you get a completely separate instance with its properties initialised to the same data.
Then you add each instance to a separate Set
. Even though set
contains pos
and setByCopy
contains pos2
, if you call setByCopy.contains(pos)
then that will return true because of how equality works for sets and data classes:
Returns true if this set contains the specified element. More formally, returns true if and only if this set contains an element e such that
(o==null ? e==null : o.equals(e))
.
That o.equals(e)
bit is important - a data class automatically generates an equals()
implementation based on its data, i.e. the properties in the constructor. So Pos(0, 0) == Pos(0, 0)
is true even though they're different instances, because they contain the same data.
This is why setByCopy.contains(pos)
is true - not because it contains that object, but because it contains an object that is equal to it.
When you update pos
with different numbers, now pos
and pos2
have different values for their data properties - they're no longer equal, so setByCopy.contains(pos)
returns false.
set.contains(pos)
still evaulates to true because that set contains the pos
object. When you updated that object, the reference in the set is pointing to that same object, so of course it's equal to itself! If you wanted to create a distinct, separate instance that doesn't change when you update pos
, then that's what copy()
is for