Data class en Kotlin : pourquoi on ne peut pas les hériter et comment contourner (analyse sous le capot)
\n\nAvez-vous déjà écrit data class User(val name: String), puis voulu faire data class Admin extends User ? Et obtenu une erreur rouge du compilateur ? Si oui, vous n'êtes pas seul. La question « Extend data class in Kotlin » fait partie des plus populaires sur Stack Overflow (plus de 50 000 vues).
Aujourd'hui, nous ne nous contenterons pas de donner une réponse toute faite. Nous allons regarder sous le capot de Kotlin, comprendre pourquoi les créateurs du langage ont imposé cette limitation « sévère », et vous montrer 3 méthodes légales pour la contourner sans casser l'architecture.
\n\nCet article est rédigé pour le portail éducatif PythonLib — ici, nous aimons creuser en profondeur, pas simplement copier des extraits de code.
\n\nPourquoi une data class ne peut-elle pas être héritée ? Regardons dans le bytecode
\n\nAllons droit au but. Écrivons une simple data class et voyons ce que le compilateur génère :
\n\ndata class User(val name: String, val age: Int)\n\nSous le capot, Kotlin crée :
\n- \n
equals()— compare tous les champs \n hashCode()— un hash basé sur tous les champs \n toString()— une représentation textuelle soignée \n copy()— une fonction de copie avec modification des champs \n componentN()— la déstructuration \n
Imaginez maintenant que vous héritez d'une data class :
\n\nopen data class Base(val x: Int)\ndata class Derived(val x: Int, val y: Int) : Base(x) // Erreur de compilation !\n\nKotlin affiche : « Inheritance of data classes is not allowed ». Pourquoi ? Analysons cela.
\n\nProblème n°1 : copy() est cassée. La fonction copy() est générée uniquement pour les champs de la classe courante. Si vous avez Derived héritée de Base, alors copy() dans Base ne connaît pas le champ y. En appelant base.copy(), vous perdrez les données de l'héritière.
Problème n°2 : equals() et hashCode(). Imaginez : Base(1) et Derived(1, 2). Doivent-ils être égaux ? Si oui, la symétrie est violée (car Derived contient plus de champs). Si non, le contrat equals/hashCode est violé. Kotlin a choisi la voie sûre : interdire l'héritage.
Problème n°3 : componentN(). val (x) = base et val (x, y) = derived — des nombres de composants différents. L'héritage créerait de la confusion.
Conclusion : la data class est conçue pour des conteneurs de données immuables, pas pour des hiérarchies. C'est une décision de conception délibérée du langage.
\n\nMéthode n°1 : La composition plutôt que l'héritage (recommandée)
\n\nLa méthode la plus propre est de ne pas hériter, mais d'inclure une data class dans une autre. Cela s'appelle la 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("Moscou", "Tverskaïa")\n val user = User("Anna", 25, address)\n println(user) // User(name=Anna, age=25, address=Address(city=Moscou, street=Tverskaïa))\n \n // La copie fonctionne parfaitement\n val user2 = user.copy(age = 26)\n println(user2) // User(name=Anna, age=26, address=Address(city=Moscou, street=Tverskaïa))\n}\n\n\nAvantages : tout fonctionne « prêt à l'emploi », pas de surprises avec equals/hashCode, code transparent.
\n\nInconvénients : si vous avez besoin de nombreux niveaux d'imbrication, vous obtenez une « poupée russe ». Mais cela se résout par la normalisation des données.
\n\nMéthode n°2 : Sealed class + data class (pour les hiérarchies strictes)
\n\nSi vous avez vraiment besoin d'une hiérarchie (par exemple, différents types d'utilisateurs), utilisez une sealed class (ou sea