Data class in Kotlin: why they cannot be inherited and how to work around it (under the hood analysis)
\n\nHave you ever written data class User(val name: String), and then wanted to do data class Admin extends User? And got a red error from the compiler? If yes — you are not alone. The question «Extend data class in Kotlin» is among the top most popular on Stack Overflow (over 50,000 views).
Today we will not just give a ready answer. We will look under the hood of Kotlin, analyze why the language creators went for such a "harsh" limitation, and show 3 legal ways to bypass it without breaking the architecture.
\n\nThe article is written for the educational portal PythonLib — here we love to dig deep, not just copy snippets.
\n\nWhy can't a data class be inherited? Let's look at the bytecode
\n\nLet's get straight to the point. Let's write a simple data class and see what the compiler generates:
\n\ndata class User(val name: String, val age: Int)\n\nUnder the hood, Kotlin creates:
\n- \n
equals()— compares all fields \n hashCode()— hash based on all fields \n toString()— nice string representation \n copy()— copy function with field modification \n componentN()— destructuring \n
Now imagine you inherit a data class:
\n\nopen data class Base(val x: Int)\ndata class Derived(val x: Int, val y: Int) : Base(x) // Compilation error!\n\nKotlin outputs: «Inheritance of data classes is not allowed». Why? Let's figure it out.
\n\nProblem #1: copy() breaks. The copy() function is generated only for the fields of the current class. If you have Derived inherited from Base, then copy() in Base does not know about the y field. Calling base.copy() will lose the inheritor's data.
Problem #2: equals() and hashCode(). Imagine: Base(1) and Derived(1, 2). Should they be equal? If yes — symmetry is violated (since Derived contains more fields). If no — the equals/hashCode contract is violated. Kotlin chose the safe path — to prohibit inheritance.
Problem #3: componentN(). val (x) = base and val (x, y) = derived — different numbers of components. Inheritance would cause confusion.
Bottom line: data class is created for immutable data containers, not for hierarchies. This is a conscious language design decision.
\n\nMethod #1: Composition instead of inheritance (recommended)
\n\nThe cleanest way is not to inherit, but to include one data class inside another. This is called composition.
\n\ndata class Address(val city: String, val street: String)\n\ndata class User(\n val name: String,\n val age: Int,\n val address: Address // composition\n)\n\nfun main() {\n val address = Address("Moscow", "Tverskaya")\n val user = User("Anna", 25, address)\n println(user) // User(name=Anna, age=25, address=Address(city=Moscow, street=Tverskaya))\n \n // Copying works perfectly\n val user2 = user.copy(age = 26)\n println(user2) // User(name=Anna, age=26, address=Address(city=Moscow, street=Tverskaya))\n}\n\n\nPros: everything works "out of the box", no surprises with equals/hashCode, code is transparent.
\n\nCons: if you need many levels of nesting, you get a "matryoshka". But this is solved by data normalization.
\n\nMethod #2: Sealed class + data class (for strict hierarchies)
\n\nIf you really need a hierarchy (e.g., different user types), use sealed class (or sea