Difference in behavior of inherited methods between data and regular classes

sealed class Parent {
    override fun toString(): String {
        return "Hello"
    }
}

data class DataChild(val doesNotMatter: String? = null): Parent()

class ClassChild: Parent()

fun main() {
    val dataChild = DataChild()
    val classChild = ClassChild()
    
    println("DataChild.toSting(): ${dataChild.toString()}")
    println("ClassChild.toSting(): ${classChild.toString()}")
}

Output:

DataChild.toSting(): DataChild(doesNotMatter=null)
ClassChild.toSting(): Hello

This is unexpected, and a potential source of error. When a class, data or otherwise, doesn’t override a superclass method, I’d expect the superclass method to be called. That’s how inheritance works.

1 Like

The compiler automatically derives the following members from all properties declared in the primary constructor:

  • equals() / hashCode() pair;
  • toString() of the form "User(name=John, age=42)" ;
  • componentN() functions corresponding to the properties in their order of declaration;
  • copy() function (see below).

https://kotlinlang.org/docs/reference/data-classes.html

Data classes automatically override the toString method of the parent class. Same way the override equals or hashCode. There are a few exceptions for the use of the autogenerated methods:

If there are explicit implementations of equals() , hashCode() or toString() in the data class body or final implementations in a superclass, then these functions are not generated, and the existing implementations are used

1 Like

Data classes automatically override the toString method of the parent class.

Consider having many data classes all inheriting from a single parent that need to be turned into JSON strings. Given the current design, you’d have to implement toString for each one of them. What a giant pain in the neck!

If there are explicit implementations of equals() , hashCode() or toString() in the data class

I think this should be extended to check the superclasses too, i.e., if someone in the class hierarchy has a custom implementation for one of the aforementioned methods, use that.

1 Like

Not sure this is possible in case of parent classes that are part of a different .kt file or different jar. In any case this would be a breaking change. It could still be changed but it would probably take quite some time.

Right now you could mark the toString function as final. That way your data classes all use that implementation, but you can no longer create custom versions.

2 Likes

final works, thanks :slight_smile: @Wasabi375 can the docs be updated to mention this “trick”?

It’s in the part I quoted above.

1 Like

You should make the classes serializable and make an extension function like toJson().
Also your question probably arises because you are used to that in Java.

While you could find a workaround to do the same in Kotlin, probably that not the right way to go.

1 Like

If you don’t want the automatically-provided toString() implementation, do you really need a data class?

2 Likes

Completely agree with you @socialguy. It’s an unexpected behavior, source of error: just spent 5 hours to try to figure out what happened in a word processing stuff while I was still having the auto generated toString although overrided.

The JSON case is also a great demo of this counter intuitive behavior.

final override toString(...

very very counter intuitive

Frankly speaking, I don’t see how this is a counter intuitive behavior. Whole OOP is based on the idea that subtypes can override the behavior of super classes. We marked our subclass with data, which basically means: “Please, make it equal, print, etc. according to its props”, but the superclass somehow has a precedence and can “override” the behavior we asked for the subtype. How is this more intuitive than the current behavior?

1 Like

What is counter intuitive is the fact that on general subtyping, you override toString of super. Here you must block the synthesizing of toString childs… declaring your parent one as final.
If you doesn’t have any idea about this synthesizing you got no evidence from the pre-compiler that your overriding will not have effect.

What changes in behaviour are you concretely suggesting?

Note that Any has its own implementation of toString and equals so data classes are always overriding concrete methods.

2 Likes

Sorry, I don’t follow you. Marking a class with data literally means: “generate toString for this class”. If someone doesn’t want or doesn’t know the function will be synthesized, then why they use data in the first place?

For me the only counter-intuitive behavior here is that it is allowed to base a data class on a supertype which finalized its toString and other methods. I think this should generate a compile error or at least a warning - similarly as when we try to override a final method. So what is a counter-intuitive feature for you, for me is simply a bug.

I just checked the Lombok’s @Data. It behaves exactly the same as Kotlin’s data, but it generates a compile error when the supertype finalized its method. I like this behavior more.

Maybe the surprising effect lies on the fact you’re in the context of sealed class in which you expect what you define for the sealed class will be propagated to children.

When you build a class with some definitions, you don’t expect that children implement definitions on their own that implicitly override your parent definition, even a data class is a sugar class synthesising some definitions.

I agree with you on that point. You point out something important in the issue.
Why use data class at first place?

My habit is to use data class for simple usage like little data collection without (or rarely) any other definitions than the primary constructor (like one use tuple or pair in C++) ; and to use class when I envisage larger needs.

So when I build a sealed class mainly to create a kind of advanced enum class needs to collect associated data, I use very simple classes to do it, so data class and I “forget” at the same time their synthesising behaviors, because being “in” a sealed class.

Bug or not, what is sure is that we can have a little warning saying we are trying to define something that will be hidden by lower definitions.

Mmm, a warning (compiler or more likely IDE) sounds like a good idea. Though I think it’d have to be on the data keyword on the subclass, as the superclass itself isn’t doing anything dodgy.

(And no more than a warning, as the subclass isn’t doing anything wrong as such — and you could reasonably want a data class for say the equals() and hashcode() implementation even if you knew you wouldn’t get the toString() implementation as well.)

2 Likes