Data class em Kotlin: por que não podem ser herdadas e como contornar (análise por baixo dos panos)
\n\nVocê já escreveu data class User(val name: String) e depois quis fazer data class Admin extends User? E recebeu um erro vermelho do compilador? Se sim, você não está sozinho. A pergunta «Extend data class in Kotlin» está entre as mais populares no Stack Overflow (mais de 50.000 visualizações).
Hoje não vamos apenas dar uma resposta pronta. Vamos olhar por baixo dos panos do Kotlin, analisar por que os criadores da linguagem impuseram essa limitação «severa» e mostrar 3 maneiras legais de contorná-la sem quebrar a arquitetura.
\n\nO artigo foi escrito para o portal educacional PythonLib — aqui gostamos de cavar fundo, e não apenas copiar snippets.
\n\nPor que data class não pode ser herdada? Olhando o bytecode
\n\nVamos direto ao ponto. Vamos escrever uma data class simples e ver o que o compilador gera:
\n\ndata class User(val name: String, val age: Int)\n\nPor baixo dos panos, o Kotlin cria:
\n- \n
equals()— compara todos os campos \n hashCode()— hash baseado em todos os campos \n toString()— representação string bonita \n copy()— função de cópia com alteração de campos \n componentN()— desestruturação \n
Agora imagine que você herda uma data class:
\n\nopen data class Base(val x: Int)\ndata class Derived(val x: Int, val y: Int) : Base(x) // Erro de compilação!\n\nO Kotlin exibe: «Inheritance of data classes is not allowed». Por quê? Vamos entender.
\n\nProblema nº1: copy() quebra. A função copy() é gerada apenas para os campos da classe atual. Se você tem Derived herdando de Base, o copy() em Base não sabe sobre o campo y. Ao chamar base.copy(), você perde os dados do herdeiro.
Problema nº2: equals() e hashCode(). Imagine: Base(1) e Derived(1, 2). Eles devem ser iguais? Se sim — a simetria é quebrada (já que Derived contém mais campos). Se não — o contrato equals/hashCode é quebrado. O Kotlin escolheu o caminho seguro — proibir a herança.
Problema nº3: componentN(). val (x) = base e val (x, y) = derived — diferentes quantidades de componentes. Com herança, haveria confusão.
Conclusão: data class foi criada para contêineres de dados imutáveis, não para hierarquias. Esta é uma decisão consciente de design da linguagem.
\n\nManeira nº1: Composição em vez de herança (recomendada)
\n\nA maneira mais limpa — não herdar, mas incluir uma data class dentro de outra. Isso se chama composição.
\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 // composição\n)\n\nfun main() {\n val address = Address("Moscou", "Tverskaya")\n val user = User("Ana", 25, address)\n println(user) // User(name=Ana, age=25, address=Address(city=Moscou, street=Tverskaya))\n \n // Cópia funciona perfeitamente\n val user2 = user.copy(age = 26)\n println(user2) // User(name=Ana, age=26, address=Address(city=Moscou, street=Tverskaya))\n}\n\n\nVantagens: tudo funciona «de fábrica», sem surpresas com equals/hashCode, código transparente.
\n\nDesvantagens: se você precisar de muitos níveis de aninhamento, vira uma «matrioska». Mas isso se resolve com normalização de dados.
\n\nManeira nº2: Sealed class + data class (para hierarquias estritas)
\n\nSe você realmente precisa de uma hierarquia (por exemplo, diferentes tipos de usuários), use sealed class (ou sea