Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin: Intrinsics.areEqual infinite loop (stack overflow)

java.lang.StackOverflowError
    at kotlin.jvm.internal.Intrinsics.areEqual(Intrinsics.java:164)
    at plugin.interaction.inter.teleports.Category.equals(Category.kt)
    at kotlin.jvm.internal.Intrinsics.areEqual(Intrinsics.java:164)
    at plugin.interaction.inter.teleports.Destination.equals(Destination.kt)

Happens from a .equals comparison between two non-relationship data classes.

Major bug.

data class Category(val name: String, val destinations: MutableList<Destination>)

data class Destination(val category: Category, val name: String)
like image 629
Jire Avatar asked Jan 31 '23 00:01

Jire


1 Answers

Data classes in Kotlin are just syntactic sugar for Java POJOs.

The main culprit in your example is this cycle:

val destinations: MutableList<Destination> in Category &
val category: Category in Destination

You must remove this cycle by moving either of the two variables out of the primary data class constructor.

However, there is also a much bigger sideeffect: data class Category(..) is mutable, which will cause for it (and any other data class using categories in it's primary constructor!) to be unsafe to use as keys in any hash-based collection. For more information, see: Are mutable hashmap keys a dangerous practice?

Given that data classes are meant for pure data, I recommend removing val category: Category in data class Destination(..), and change type of val destinations: MutableList<Destination> in data class Category(..) to read-only List<Destination>. In order to break immutable state after said changes, you will have to either perform unsafe casts from Kotlin or create an instance of the class from Java.

If you however absolutely require a backreference to categories in destinations (and aren't using your classes in hashmaps/-sets/etc.), you could either make Destination a regular class and implement equals/hashCode yourself, or move the category out of the primary constructor. This is a bit tricky, but can be done with a secondary constructor:

data class Destination private constructor(val name: String) {
    private lateinit var _category: Category
    val category get() = _category
    constructor(category: Category, name: String) : this(name) {
        _category = category
    }
}
like image 172
F. George Avatar answered Feb 02 '23 20:02

F. George