Data class en Kotlin: por qué no se pueden heredar y cómo evitarlo (análisis bajo el capó)
\n\n¿Alguna vez has escrito data class User(val name: String) y luego has querido hacer data class Admin extends User? ¿Y recibiste un error rojo del compilador? Si es así, no estás solo. La pregunta «Extend data class in Kotlin» está entre las más populares en Stack Overflow (más de 50,000 visitas).
Hoy no solo daremos una respuesta preparada. Exploraremos bajo el capó de Kotlin, analizaremos por qué los creadores del lenguaje impusieron esta limitación «drástica» y mostraremos 3 formas legales de evitarla sin romper la arquitectura.
\n\nEl artículo está escrito para el portal educativo PythonLib — aquí nos encanta profundizar, no solo copiar fragmentos de código.
\n\n¿Por qué no se puede heredar un data class? Echando un vistazo al bytecode
\n\nVamos al grano. Escribamos un data class simple y veamos qué genera el compilador:
\n\ndata class User(val name: String, val age: Int)\n\nBajo el capó, Kotlin crea:
\n- \n
equals()— compara todos los campos \n hashCode()— hash basado en todos los campos \n toString()— representación de cadena legible \n copy()— función de copia con modificación de campos \n componentN()— desestructuración \n
Ahora imagina que heredas un data class:
\n\nopen data class Base(val x: Int)\ndata class Derived(val x: Int, val y: Int) : Base(x) // ¡Error de compilación!\n\nKotlin muestra: «Inheritance of data classes is not allowed». ¿Por qué? Analicemos.
\n\nProblema n.º 1: copy() se rompe. La función copy() se genera solo para los campos de la clase actual. Si tienes Derived heredado de Base, entonces copy() en Base no conoce el campo y. Al llamar a base.copy(), perderías los datos del heredero.
Problema n.º 2: equals() y hashCode(). Imagina: Base(1) y Derived(1, 2). ¿Deberían ser iguales? Si es así, se rompe la simetría (ya que Derived contiene más campos). Si no, se rompe el contrato de equals/hashCode. Kotlin eligió el camino seguro: prohibir la herencia.
Problema n.º 3: componentN(). val (x) = base y val (x, y) = derived — diferentes cantidades de componentes. Con la herencia surgiría confusión.
Conclusión: data class está creado para contenedores de datos inmutables, no para jerarquías. Es una decisión consciente del diseño del lenguaje.
\n\nForma n.º 1: Composición en lugar de herencia (recomendada)
\n\nLa forma más limpia es no heredar, sino incluir un data class dentro de otro. Esto se llama composición.
\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 // composición\n)\n\nfun main() {\n val address = Address("Moscú", "Tverskaya")\n val user = User("Ana", 25, address)\n println(user) // User(name=Ana, age=25, address=Address(city=Moscú, street=Tverskaya))\n \n // La copia funciona perfectamente\n val user2 = user.copy(age = 26)\n println(user2) // User(name=Ana, age=26, address=Address(city=Moscú, street=Tverskaya))\n}\n\n\nVentajas: todo funciona «de fábrica», sin sorpresas con equals/hashCode, código transparente.
\n\nDesventajas: si necesitas muchos niveles de anidamiento, obtienes una «muñeca rusa». Pero esto se resuelve con normalización de datos.
\n\nForma n.º 2: Sealed class + data class (para jerarquías estrictas)
\n\nSi realmente necesitas una jerarquía (por ejemplo, diferentes tipos de usuarios), usa sealed class (o sea