Data interfaces!

Imagine we have a sealed interface called Point that has two fields, x: Double and y:Double, and one sub-interface called MutablePoint. We also have a private data class Impl for both Point and MutablePoint, and we’ve made them data classes, thinking we’re taking advantage of Kotlin’s cool features.

But then this test fails:

fun equalityTest() {
    val immutable = Point.of(5, 6)
    val set = setOf(immutable)
    val mutable = MutablePoint.of(5, 6)
    assertTrue(mutable in set)

And so you realize that the only features that data classes inject into your code, that is having conveniently defined equals, hashCode and toString methods as well as componentN methods, only matter if the class is public and its objects are working with objects of the same class, and they’re useless if they’re private and their core logic is defined in the interfaces they implement, like the example above.

The current solution for this is make the classes non-data, and make them extend a common superclass like AbstractPointImpl where the equals, hashCode are defined in a way that allows the subclasses co-exist together in collections. This is bad because 1) we stopped using data classes and with it we lost the compile-time and runtime optimizations that exist for them and 2) we introduced a whole new class within which we had to resort to ancient modes of programming that is very reminiscent of the Java days, something that Kotlin was trying to put behind especially with data classes.

What I propose is simple: data interfaces! They are just like data classes except for interfaces. They provide equals, hashCode logic based on a handful of fields defined in the parenthesis after the data interface and the interface name. They could also provide predefined toString and componentN behavior but this is optional and not the subject of this post.

Known issue: I realize this won’t work because the JVM does not allow equals and hashCode to be defined in interfaces, because those methods are traditionally a part of Object which all classes implicitly extend, and so interfaces do not have access to them to override them.

Data class is really just a simple syntactic sugar to save boilerplate for the most common cases. If your case is not the common one, feel free to implement the same manually in 5 minutes or so (actually, generate the code by IDE).

Also note comparing objects of different types is tricky. If you have another class implementing Point then it would probably didn’t make sense that Point.of(5, 6) == 3DPoint.of(5, 6, 2) returns true (just an example). You said your Point is sealed, so in that case it actually makes sense, but I think it is a rather rare case.