0

I have a MutableMap that its keys are objects from a DataClass (User dataclass), and the values are arrays from other Dataclass (Dog dataclass). If i have a variable with a User object, and i put it in the MutableMap and i test if the map contains the User, it says that is true. But after putting the user in the MutableMap if i change one of the attributes of the User object using the variable that holds the User object, the Map says that it doesnt contains the user object.

This is an example

data class User(
    var name: String,
    var project: String,
)

data class Dog(
    var kind: String
)


fun main(args: Array<String>) {
    var mapUserDogs: MutableMap<User, MutableList<Dog>> = mutableMapOf()
    var userSelected = User("name2", "P2")

    mapUserDogs.put(
        User("name1", "P1"),
        mutableListOf(Dog("R1"), Dog("R2"))
    )

    mapUserDogs.put(
        userSelected,
        mutableListOf(Dog("R21"), Dog("R31"))
    )

    println(userSelected)
    println(mapUserDogs.keys.toString())
    println(mapUserDogs.contains(userSelected))
    println(mapUserDogs.values.toString())
    println("\n")

    userSelected.name = "Name3"

    println(userSelected)
    println(mapUserDogs.keys.toString())
    println(mapUserDogs.contains(userSelected))
    println(mapUserDogs.values.toString())
}

The prints statements show this:

User(name=name2, project=P2)
[User(name=name1, project=P1), User(name=name2, project=P2)]
true
[[Dog(kind=R1), Dog(kind=R2)], [Dog(kind=R21), Dog(kind=R31)]]


User(name=Name3, project=P2)
[User(name=name1, project=P1), User(name=Name3, project=P2)]
false
[[Dog(kind=R1), Dog(kind=R2)], [Dog(kind=R21), Dog(kind=R31)]]

Process finished with exit code 0

But it doesn't make sense. Why the map says that it doesn't contains the user object if its clear that it still holds the reference to it after being modified?

User(name=Name3, project=P2)
[User(name=name1, project=P1), User(name=Name3, project=P2)]

The user in the keys collection was also changed when i modified the userSelected variable, so now the object has it attribute name as "Name3" in both the variable and in the Map keys, but it still says that it doesnt contains it.

What can i do so that i can change the attributes in the userSelected object and the Map still return true when using the "contains" method?. And doing the same process in reverse shows the same. If i get from the map the user and i modify it, the userVariable is also modified but if i later test if the map contains the userVariable, it says false.

3 Answers3

2

What can i do so that i can change the attributes in the userSelected object and the Map still return true when using the "contains" method?

There is nothing you can do that preserves both your ability to look up the entry in the map and your ability to modify the key.

Make your data class immutable (val instead of var, etc.), and when you need to change a mapping, remove the old key and put in the new key. That's really the only useful thing you can do.

Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
1

To add to Louis Wasserman's correct answer:

This is simply the way that maps work in Kotlin: their contract requires that keys don't change significantly once stored. The docs for java.util.Map* spell this out:

Note: great care must be exercised if mutable objects are used as map keys. The behavior of a map is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is a key in the map.

The safest approach is to use only immutable objects as keys. (Note that not just the object itself, but any objects it references, and so on, must all be immutable for it to be completely safe.) You can get away with mutable keys as long as, once the key is stored in the map, you're careful never to change anything that would affect the results of calling equals() on it. (This may be appropriate if the object needs some initial set-up that can't all be done in its constructor, or to avoid having both mutable and immutable variants of a class.) But it's not easy to guarantee, and leaves potential problems for future maintenance, so full immutability is preferable.

The effects of mutating keys can be obvious or subtle. As OP noticed, mappings may appear to vanish, and maybe later reappear. But depending on the exact map implementation, it may cause further problems such as errors when fetching/adding/removing unrelated mappings, memory leaks, or even infinite loops. (“The behaviour… is not specified” means that anything can happen!)

What can i do so that i can change the attributes in the userSelected object and the Map still return true when using the "contains" method?

What you're trying to do there is to change the mapping. If you store a map from key K1 to value V, and you mutate the key to hold K2, then you're effectively saying “K1 no longer maps to V; instead, K2 now maps to V.”

So the correct way to do that is to remove the old mapping, and then add the new one. If the key is immutable, that's what you have to do — but even if the key is mutable, you must remove the old mapping before changing it, and then add a new mapping after changing it, so that it never changes while it's stored in the map.


(* The Kotlin library docs don't address this, unfortunately — IMHO this is one of many areas in which they're lacking, as compared to the exemplary Java docs…)

gidds
  • 16,558
  • 2
  • 19
  • 26
  • Wow, such great answer!. I didn't think in the consequences of using mutable objects as keys. Thanks very much!. When i readed Louis answers i just thought of making a custom contains function to test if a dataclass objects is in the map keys, but now i'm scared of a possible bug of keys dissapear or something else. I'm just going to change the implementation. Thanks mate! – Daniel Florez Cortes Jan 07 '23 at 16:00
  • And hey @gidds, i wanted to ask you about the "Values". In my example the "values" of the Map are arrays of dataclass objects. The same problems will apear if i change attributes of the objects in the values arrays? Or that is safe to do? – Daniel Florez Cortes Jan 07 '23 at 16:07
  • There's no problem with mutable objects as values in a map, because the map doesn't need to do any processing on them — in particular, it doesn't need to call `equals()` on them, so that can change with no consequences. – gidds Jan 07 '23 at 21:43
  • It might help to think how you could implement a simple map yourself — e.g. with two parallel arrays, one of keys, the other holding the corresponding values. To get the value for a key, you'd have to scan through the array of keys, calling `equals()` on each one until you find the right one, and you can then get its value from the corresponding item in the other array. (Of course, in practice most maps are implemented in far more complex and efficient ways than that, but it's still a useful exercise.) – gidds Jan 07 '23 at 21:44
-2

That happens because data classes in Kotlin are compared by value, unlike regular classes which are compared by reference. When you use a data class as a key, the map gets searched for a User with the same string values for the name and project fields, not for the object itself in memory.

For example:

data class User(
    var name: String,
    var project: String,
)
val user1 = User("Daniel", "Something Cool")
val user2 = User("Daniel", "Something Cool")
println(user1 == user2) // true

works because, even though they are different objects (and thus different references), they have the same name and project values. However, if I were to do this:

user1.name = "Christian"
println(user1 == user2) // false

the answer would be false because they don't share the same value for all of their fields. If I made User a standard class:

class User(
    var name: String,
    var project: String,
)
val user1 = User("Daniel", "Something Cool")
val user2 = User("Daniel", "Something Cool")
println(user1 == user2) // false

the answer would be false because they are different references, even though they share the same values. For your code to work the way you want, make User a regular class instead of a data class. That's the key difference between regular classes and data classes: a class is passed by reference, a data class is passed by value. Data classes are nothing more than collections of values with (optionally) some methods attached to them, classes are individual objects.

  • 1
    This is incorrect. **All** classes are compared by their `equals()` and `hashcode()` functions, which may be defined in any way you like, regardless of whether the class is a data class. It may or may not compare by reference or value or some subset of the properties in the class. And **all** non-inline classes are **always** passed by reference, regardless of whether they are data classes. Data classes are a subset of regular classes, with an added implicit `copy()` function and implicit overrides of `equals` and `hashcode`. They are treated exactly the same as any other class. – Tenfour04 Jan 06 '23 at 19:19
  • 3
    In short, `data class` is nothing more than a syntax shortcut for defining a specific kind of "regular" class. The class itself doesn't have any special status or different treatment whatsoever. – Tenfour04 Jan 06 '23 at 19:27