Pontos Principais
- O que justifica a criação de um novo tipo em Java;
- O que é um Record; - Classe java.lang.Record;
- Tirando proveito do novo tipo record;
- Como ficam as coisas nos bastidores;
Records no Java 14
É possível observar nas últimas versões do Java, que esforços com o intuito de reduzir a verbosidade da linguagem têm sido constantes. No Java 14, que deve ser lançado em março deste ano, não será diferente. Liderada por Brian Goetz, a JEP 359, que será liberada como feature preview provê uma sintaxe compacta para declaração de classes imutáveis em Java.
Projetos Java, comumente contém uma infinidade de classes que representam modelos do domínio e que servem para fazer persistência em base de dados. A estrutura desse tipo de classe é sempre composta por uma lista de atributos, construtores, métodos de acesso, métodos equals, hashCode e toString. Enfim, código repetitivo, de baixo valor e que está muito propenso a erro em refatorações.
Você pode pensar... Mas as IDEs geram esse amontoado de código, eu não me preocupo com eles. Sim, elas geram. Mas você tem que concordar com dois pontos: o primeiro deles é que quando é feito um refactoring, é muito comum esquecermos de regerar; e o segundo, é que estamos falando de um amontoado de código para ler e entender e mesmo sendo gerado automaticamente, em caso de bugs ou comportamentos inesperados, representam mais linhas para analisar.
O que é um Record
Um record é um tipo de classe que serve para modelar classe de dados, Outras linguagens como Kotlin, Scala e C# possuem tipos similares e serviram de base para Brian Goetz na concepção do novo tipo .
Ao declarar um tipo como record, o desenvolvedor expressa a intenção que o novo tipo serve para representar dados. A sintaxe de declaração de um record é muito simples, se compararmos com uma declaração habitual de uma classe, na qual normalmente é necessário sobrescrever os métodos equals e hashCode, métodos de acesso e construtores.
Um record é declarado apenas usando a palavra chave record, o nome do novo tipo e seus componentes. Sendo que os componentes podem ser entendidos como todos os atributos da classe e devem ser declarados com o seu respectivo nome e tipo.
Como já mencionado, records são uma preview feature language, isso significa que embora esteja totalmente implementada, ainda não é padrão na JDK e é necessário habilitar o compilador para podermos usá-la: javac --enable-preview --release. Um outro ponto importante, é que por ser uma preview feature, existe a possibilidade de ser atualizada ou até mesmo removida nas próximas versões do Java.
Classe java.lang.Record
A classe Record será introduzida no java 14 e está definida no pacote java.lang. A seguir temos uma ilustração da mesma.
package java.lang;
@jdk.internal.PreviewFeature(feature=jdk.internal.PreviewFeature.Feature.RECORDS,
essentialAPI=true)
public abstract class Record {
protected Record() {}
@Override
public abstract boolean equals(Object obj);
@Override
public abstract int hashCode();
@Override
public abstract String toString();
}
Record é uma classe abstrata e todas as classes que estenderem Record, devem possuir obrigatoriamente os seguintes membros:
- um atributo final e private para cada componente declarado, com o mesmo nome e mesmo tipo;
- um construtor público, cuja lista de argumentos é idêntica - ordem, nomes e tipos - a lista declarada como componente no Record. A implementação inicializa cada atributo do Record;
- um método de acesso público com o mesmo nome declarado e retorna o mesmo tipo para cada componente.
- um método hashCode
implementado usando a combinação do valor de código hash de todos os componentes;
- um método toString
que retorna o nome da classe, o nome e o valor de cada componente.
- um método equals que obedece um contrato especial, onde:
R copy = new R(r.c1(), r.c2(), ..., r.cn());
isso significa que r.equals(copy)
será verdadeiro;
Usando um record
Para usar um record, é necessário declará-lo explicitamente e deixar o compilador criar o arquivo .class. Na declaração, também é necessário especificar seus componentes, com os seus respectivos tipos. Os componentes serão transformados em atributos da classe pelo compilador.
A título de exemplo, vamos declarar um record chamado PhoneNumber
, com dois componentes: prefix
e number
, os dois do tipo String.
public record PhoneNumber(String prefix, String number) {}
O .class de PhoneNumber é o seguinte:
public final class PhoneNumber extends java.lang.Record {
private final java.lang.String prefix;
private final java.lang.String number;
public PhoneNumber(java.lang.String prefix, java.lang.String number) { /* compiled code */ }
public java.lang.String toString() { /* compiled code */ }
public final int hashCode() { /* compiled code */ }
public final boolean equals(java.lang.Object o) { /* compiled code */ }
public java.lang.String prefix() { /* compiled code */ }
public java.lang.String number() { /* compiled code */ }
}
Note que no .class gerado, não é possível visualizar os métodos criados, pois eles usam invokedynamic para invocar dinamicamente o método apropriado que contém a implementação implícita. Se tentarmos abrir o bytecode, conseguiremos enxergar algo como o trecho a seguir:
public toString()Ljava/lang/String;
L0
LINENUMBER 1 L0
ALOAD 0
INVOKEDYNAMIC toString(LPhoneNumber;)Ljava/lang/String; [
// handle kind 0x6 : INVOKESTATIC
java/lang/runtime/ObjectMethods.bootstrap(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
// arguments:
PhoneNumber.class,
"prefix;number",
// handle kind 0x1 : GETFIELD
PhoneNumber.prefix(Ljava/lang/String;),
// handle kind 0x1 : GETFIELD
PhoneNumber.number(Ljava/lang/String;)
]
ARETURN
L1
LOCALVARIABLE this LPhoneNumber; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
Abstraindo a parte de invocação dinâmica, vamos analisar o que foi gerado. Primeiramente foi criada uma classe final
e que estende Record. Final, que indica não ser possível a criação de subclasses a partir dela. E estende de Record, que é a classe base de todos os records e que possui os métodos abstratos que o compilador implementou.
Nessa classe temos prefix e number como atributos final. O construtor criado também possui como argumentos os dois componentes e temos dois métodos de acesso com os mesmos nomes. O construtor gerado é equivalente a:
public PhoneNumber(java.lang.String prefix, java.lang.String number) {
this.prefix = prefix;
this.number = number;
}
Por este motivo, instanciar um tipo record, é exatamente igual a instanciar qualquer outra classe Java.
PhoneNumber phoneNumber = new PhoneNumber("048", "36466128");
Para acessar um atributo da classe, é um pouco diferente do que estamos habituados, já que os métodos de acesso não possuem o prefixo get.
phoneNumber.number();
O método equals também funciona um pouco diferente, já que ele obedece um contrato especial, já discutido anteriormente. Note:
PhoneNumber phoneNumber1 = new PhoneNumber("048", "36466128");
PhoneNumber phoneNumber2 = new PhoneNumber("048", "36466128");
System.out.println(phoneNumber1.equals(phoneNumber2));
Esse trecho de código deve exibir na tela o valor true. Ficou confuso? Isso acontece porque no contrato especificado em Record, Dois Records são iguais, se eles forem do mesmo tipo e os valores de seus atributos forem os mesmos. Como os Records são imutáveis, não teremos problemas com a mudança de estado durante a execução do programa.
E para finalizar, temos implementado um método toString que inclui uma representação em String de todos os componentes com seus respectivos nomes e valores. Ao executar o método System.out.println(phoneNumber) deve ser exibido algo similar a: PhoneNumber[prefix=048, number=36466128].
Adição de novos membros em um Record
É importante salientar que Records ainda são classes. Elas podem conter anotações, métodos podem ser adicionados e variáveis estáticas podem ser criadas. Mas são classes restritas: novos atributos e construtores não são permitidos. Também não é possível estender uma classe do tipo Record.
No record PhoneNumber, declarado anteriormente, foi adicionado um novo método e uma variável estática. Veja:
public record PhoneNumber(String prefix, String number){
private static final String DIVISOR = "-";
public String fullPhoneNumber(){
return prefix + DIVISOR + number;
}
}
Outro ponto importante, é que a classe Record não pode ser diretamente extendida. Ocorre erro de compilação se existir a tentativa.
Considerações finais
Records introduzem na linguagem Java uma forma muito mais simples e concisa para declararmos classes de dados, sem a necessidade de escrevermos um amontoado de código verboso. Adaptar-se a este novo tipo é só questão de tempo. E que venha o Java 14.
Sobre o autor
Rosicléia Frasson é cientista da computação e apaixonada por desenvolvimento de softwares. Já acompanhou o desenvolvimento de vários produtos durante a sua carreira e sabe o quanto é importante estar atualizado para enfrentar os novos desafios.