Sempre me espanta o quanto o Kotlin é capaz de oferecer em relação ao Java “puro”, e as classes de dados não são exceção. Nesta postagem, exploraremos como as classes de dados de Kotlin eliminam todo o clichê dos POJOs antigos, o poder dos iguais , hashcode e copie métodos e aprenda a fácil desestruturação com os auxiliares componentN gerados. Por fim, veremos uma pequena pegadinha ao misturar herança com classes de dados.

Avante!

O que é Kotlin?

Como uma atualização rápida, Kotlin é uma linguagem moderna, com tipagem estática, que pode ser compilada para uso na JVM. Muitas vezes, é usado em qualquer lugar que você alcance para Java, incluindo aplicativos Android e servidores de back-end (usando Java Spring ou o próprio Ktor ).

Como as classes de dados do Kotlin se comparam aos antigos hábitos Java?

Se você configurou um POJO em Java antes, provavelmente já lidou com algum código padrão: getters e setters, um bom toString para depuração, algumas substituições para é igual a e hashCode se você quiser comparabilidade… ensaboar, enxaguar, repetir, certo?

Bem, Kotlin não gostou de toda essa cerimônia. Eles criaram um tipo especial de classe para lidar com:

  • Uma função igual a gerada com base nos parâmetros do seu construtor (em vez de referências de memória não tão úteis)
  • Um valor toString () agradável e legível com base nesses parâmetros do construtor
  • Uma função de copiar para clonar instâncias à vontade, sem passar entre os construtores por conta própria
  • Capacidade de estrutura usando parênteses ()

Estas são algumas vitórias muito grandes sobre os padrões POJO de antigamente. O Kotlin não apenas lidará com todos os getters e setters para você (já que os parâmetros do construtor estão disponíveis publicamente por padrão), mas também oferece comparabilidade gratuitamente!

Vamos aprender como podemos fazer uma declaração de classe enorme como esta:

 classe UniversityStudentBreakfasts { private int numEggs; public UniversityStudentBreakfasts (int numEggs) { this.numEggs=numEggs; } public int getNumEggs () { return numEggs; } public void setNumEggs (int numEggs) { this.numEggs=numEggs; } @Sobrepor public boolean equals (Object o) { if (this==o) return true; if (o==null || getClass ()!=o.getClass ()) return false; Café da manhã do aluno da Universidade=(Café da manhã do aluno da Universidade) o; return numEggs==breakfast.numEggs; } @Sobrepor public String toString () { return"UniversityStudentBreakfasts ("+ "numEggs='"+ numEggs +'\''+ ')'; } //não me fale sobre a capacidade de cópia...
}

… e transforme-o em uma linha simples 😄

 classe de dados UniversityStudentBreakfasts ( val numEggs: Int,
)

Usando o trait interno igual a

Vamos começar com um enorme valor agregado em relação às classes padrão: uma função de igualdade integrada com base em nossos parâmetros de construtor.

Resumindo, o Kotlin irá gerar uma função igual bacana (mais uma função hashCode complementar) que avalia seus parâmetros de construtor para comparar instâncias de sua classe:

 classe de dados UniversityStudentBreakfasts ( val numEggs: Int,
)
val student1Diet=UniversityStudentBreakfasts (numEggs=2)
val student2Diet=UniversityStudentBreakfasts (numEggs=2)
student1Diet==student2Diet//true
student1Diet.hashCode ()==student2Diet.hashCode ()//também verdadeiro

⚠ Observação : nos bastidores, isso chama a função igual para todos os parâmetros do construtor ao comparar. Infelizmente, isso significa que problemas de referência de memória podem surgir novamente quando suas classes de dados contêm listas ou referências a outras classes.

Usando o método toString

Sim, as classes de dados fornecem um bom auxiliar toString para depuração mais simples. Em vez de obter uma referência de memória aleatória para nossa classe UniversityStudentBreakfasts acima, obtemos um bom mapeamento de chaves de construtor para valores:

 println (student1Diet)
//-> UniversityStudentBreakfasts (numEggs=2)

Usando o traço copiar

A característica copy do

Kotlin aborda uma armadilha comum das classes tradicionais: queremos pegar uma classe existente e construir uma nova que seja apenas ligeiramente diferente. Tradicionalmente, existem duas maneiras de abordar isso. A primeira é canalizar manualmente tudo de um construtor para outro:

 val couponApplied=ShoppingCart (coupon="cupom", ovos=original.eggs, pão=original.bread, jam=original.jam...)

… mas isso é muito desagradável de fazer, especialmente se tivermos referências aninhadas para nos preocuparmos com a duplicação. A segunda opção é simplesmente admitir a derrota e abrir tudo para mutação usando apply {...} :

 val couponApplied=original.apply {coupon="cupom"}

… mas você pode não gostar dessa abordagem se sua equipe estiver trabalhando com técnicas de programação funcional. Se ao menos pudéssemos ter uma sintaxe semelhante a apply que não modifique o valor original…

A boa notícia? Se você estiver usando uma classe de dados, copiar permite que você faça exatamente isso!

 classe de dados ShoppingCart ( val coupon: String,//apenas um"val"regular funcionará ovos val: Int, pão val: Int, ...
)
val original=checkoutLane.ringUpCustomer ()
val couponApplied=original.copy (cupom="cupom")

Você também notará que copiar é apenas uma chamada de função regular sem uma opção para um lambda. Esta é a beleza do compilador Kotlin-ele gera todos os argumentos para você com base nos parâmetros do construtor 💪.

Desmistificando componenteN em Kotlin

Com classes de dados, cada propriedade é acessível como um componente usando funções de extensão como componente1, componente2, etc., onde o número corresponde à posição de um argumento no construtor. Você provavelmente poderia usar um exemplo para este:

 classe de dados MyFridge ( val doesPastaLookSketchy: Boolean, val numEggsLeft: Int, val chiliOfTheWeek: String,
)
val refrigerador=MyFridge ( doesPastaLookSketchy=true, numEggsLeft=0, chiliOfTheWeek="Feijão preto"
)
geladeira.component1 ()//verdadeiro
geladeira.component2 ()//0
frigorífico.component3 ()//"Feijão preto"

Você pode estar pensando, “OK, mas por que diabos eu buscaria um valor chamando component57 () ?” Boa pergunta! Você provavelmente não chamará esses ajudantes diretamente assim. No entanto, eles são muito úteis para o Kotlin nos bastidores para realizar a desestruturação.

Destruição com classes de dados Kotlin

Digamos que temos um par de coordenadas em um mapa. Poderíamos usar a classe Pair para representar este tipo como um Par de inteiros:

 coordenadas val=Par  (255, 255)

Então, como pegamos os valores xey daqui? Bem, podemos usar as funções de componente que vimos antes:

 val x=coordinates.component1 ()
val y=coordinates.component2 ()

Ou podemos apenas desestruturar usando parens () em nossas declarações de variáveis:

 val (x, y)=coordenadas

Legal! Agora podemos deixar Kotlin chamar essas funções de componentes feias para nós.

Podemos usar esse mesmo princípio para nossas próprias classes de dados. Por exemplo, se quisermos que nossas coordenadas tenham uma terceira dimensão z, podemos fazer uma boa classe de Coordenadas , assim:

 coordenadas da classe de dados ( val x: Int, val y: Int, val z: Int,
)

E, em seguida, desestruture conforme consideramos adequado 👍.

 val (x, y, z)=Coordenadas (255, 255, 255)

⚠ Nota : Isso pode ficar complicado quando a ordem dos argumentos não está implícita. Sim, está bem claro que x vem antes de y (que vem antes de z ) em nosso exemplo de Coordenadas . Mas se um engenheiro mover distraidamente o valor z para o topo do construtor, ele poderia quebrar as instruções de desestruturação em toda a base de código!

Uma pegadinha importante para herança

À medida que você começa a se familiarizar com as classes de dados, pode começar a usá-las como um objeto de tipo seguro para todas as ocasiões.

Mas não tão rápido! Os problemas começam a surgir quando você começa a se orientar a objetos. Para expandir nosso exemplo de Geladeira anterior, digamos que você queira uma classe de dados especial com campos extras para representar seu próprio caos na cozinha:

 classe de dados Fridge ( val doesPastaLookSketchy: Boolean, val numEggsLeft: Int,
)
classe de dados YourFridge ( val servingsOfChickenNoodleLeft: Int,
): Frigorífico()

Em outras palavras, você deseja pegar carona na primeira classe de dados e manter as características de igualdade e cópia intactas. Mas se você tentar isso em um parquinho, você obterá uma exceção desagradável:

 Nenhum valor passado para o parâmetro'doesPastaLookSketchy'
Nenhum valor passado para o parâmetro'numEggsLeft'

Hm, parece que precisaremos duplicar nosso construtor Fridge para permitir que todos os nossos valores passem. Vamos fazer isso:

 classe de dados Fridge ( open val doesPastaLookSketchy: Boolean, val aberto numEggsLeft: Int,
)
classe de dados YourFridge ( override val doesPastaLookSketchy: Boolean, substituir val numEggsLeft: Int, val servingsOfChickenNoodleLeft: Int,
): Geladeira (doesPastaLookSketchy, numEggsLeft)

… o que nos deixa com uma exceção muito diferente 😬

 A função'component1'gerada para a classe de dados conflita com o membro do supertipo'Fridge'
A função'component2'gerada para a classe de dados conflita com o membro do supertipo'Fridge'
Este tipo é final, portanto não pode ser herdado de

Agora parece que há um problema com o uso de override nesses parâmetros do construtor. Isso se resume a uma limitação do compilador Kotlin: para que os auxiliares componentN () apontem para o valor correto, as classes de dados precisam ser mantidas “finais”.

Portanto, depois de definir esses parâmetros, eles não podem ser anulados (ou mesmo estendidos).

Felizmente, você pode retirar nossa herança, desde que o pai não seja uma classe de dados. Uma classe abstrata provavelmente resolveria o problema para nós:

 classe abstrata Fridge ( open val doesPastaLookSketchy: Boolean, val aberto numEggsLeft: Int,
)
classe de dados YourFridge ( override val doesPastaLookSketchy: Boolean, substituir val numEggsLeft: Int, val servingsOfChickenNoodleLeft: Int,
): Geladeira (doesPastaLookSketchy, numEggsLeft)

Sim, ainda precisamos duplicar os parâmetros que queremos usando override , mas isso nos dá alguma segurança de tipo para parâmetros compartilhados entre nossas classes de dados, tudo ao mesmo tempo mantendo a igualdade, cópia e hashing características em funcionamento.

Conclusão

Como você pode ver, as classes de dados oferecem alguns benefícios interessantes com quase nenhuma sobrecarga do desenvolvedor. É por isso que eu recomendo usar data quase em qualquer lugar em que você use uma classe regular para esses benefícios de comparabilidade adicionais. Então, vá em frente e reescreva alguns POJOs!

A postagem Usando classes de dados Kotlin para eliminar boilerplates Java POJO apareceu primeiro no LogRocket Blog .