Na última vez , testamos a
instanceof
aprimorada do operador, que aparecerá na próxima
14ª versão do Java (será lançada em março de 2020). Hoje eu gostaria de investigar em detalhes o segundo recurso sintático, que também aparecerá no Java 14: records.
Os registros têm
seu próprio JEP , mas não mostram muitos detalhes, portanto há muito o que tentar e verificar por si mesmo. Sim, é claro, é possível abrir
a especificação do Java SE, mas me parece que é muito mais interessante começar a escrever o código você mesmo e observar o comportamento do compilador em determinadas situações. Então faça uma gaivota e sente-se confortavelmente. Vamos lá
Diferentemente da última vez, quando tive que criar uma ramificação especial do JDK por
instanceof
para teste, agora tudo isso já está na ramificação principal e está disponível na
compilação inicial do JDK 14 , que eu baixei.
Primeiro, implementamos o exemplo clássico com
Point
e o compilamos:
record Point(float x, float y) { }
> javac --enable-preview --release 14 Point.java Note: Point.java uses preview language features. Note: Recompile with -Xlint:preview for details.
javac
compilou com
Point.class
arquivo
Point.class
. Vamos desmontá-lo e ver o que o compilador gerou para nós lá:
> javap -private Point.class Compiled from "Point.java" final class Point extends java.lang.Record { private final float x; private final float y; public Point(float, float); public java.lang.String toString(); public final int hashCode(); public final boolean equals(java.lang.Object); public float x(); public float y(); }
Sim, o compilador criou o seguinte:
- A classe final herdada de java.lang.Record (semelhante a
enum
, que herda de java.lang.Enum
). - Campos finais privados
x
e y
. - Um construtor público que corresponde à assinatura do próprio registro. Esse construtor é chamado canônico .
- Implementações de
toString()
, hashCode()
e equals()
. Curiosamente, hashCode()
e equals()
são final
e toString()
não é. É improvável que isso afete alguma coisa, já que a aula em si é final
, mas alguém sabe por que eles fizeram isso? (Eu não sou) - Métodos para leitura de campos.
Com o construtor e os métodos de leitura, tudo fica claro, mas eu me pergunto como exatamente
toString()
,
hashCode()
e
equals()
implementados? Vamos ver Para fazer isso, execute o
javap
com o sinalizador
-verbose
:
Pin longo do desmontador > javap -private -verbose Point.class Classfile Point.class Last modified 29 . 2019 .; size 1157 bytes SHA-256 checksum 24fe5489a6a01a7232f45bd7739a961c30d7f6e24400a3e3df2ec026cc94c0eb Compiled from "Point.java" final class Point extends java.lang.Record minor version: 65535 major version: 58 flags: (0x0030) ACC_FINAL, ACC_SUPER this_class: #8 // Point super_class: #2 // java/lang/Record interfaces: 0, fields: 2, methods: 6, attributes: 4 Constant pool: #1 = Methodref #2.#3 // java/lang/Record."<init>":()V #2 = Class #4 // java/lang/Record #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Record #5 = Utf8 <init> #6 = Utf8 ()V #7 = Fieldref #8.#9 // Point.x:F #8 = Class #10 // Point #9 = NameAndType #11:#12 // x:F #10 = Utf8 Point #11 = Utf8 x #12 = Utf8 F #13 = Fieldref #8.#14 // Point.y:F #14 = NameAndType #15:#12 // y:F #15 = Utf8 y #16 = Fieldref #8.#9 // Point.x:F #17 = Fieldref #8.#14 // Point.y:F #18 = InvokeDynamic #0:#19 // #0:toString:(LPoint;)Ljava/lang/String; #19 = NameAndType #20:#21 // toString:(LPoint;)Ljava/lang/String; #20 = Utf8 toString #21 = Utf8 (LPoint;)Ljava/lang/String; #22 = InvokeDynamic #0:#23 // #0:hashCode:(LPoint;)I #23 = NameAndType #24:#25 // hashCode:(LPoint;)I #24 = Utf8 hashCode #25 = Utf8 (LPoint;)I #26 = InvokeDynamic #0:#27 // #0:equals:(LPoint;Ljava/lang/Object;)Z #27 = NameAndType #28:#29 // equals:(LPoint;Ljava/lang/Object;)Z #28 = Utf8 equals #29 = Utf8 (LPoint;Ljava/lang/Object;)Z #30 = Utf8 (FF)V #31 = Utf8 Code #32 = Utf8 LineNumberTable #33 = Utf8 MethodParameters #34 = Utf8 ()Ljava/lang/String; #35 = Utf8 ()I #36 = Utf8 (Ljava/lang/Object;)Z #37 = Utf8 ()F #38 = Utf8 SourceFile #39 = Utf8 Point.java #40 = Utf8 Record #41 = Utf8 BootstrapMethods #42 = MethodHandle 6:#43 // REF_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; #43 = Methodref #44.#45 // 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; #44 = Class #46 // java/lang/runtime/ObjectMethods #45 = NameAndType #47:#48 // 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; #46 = Utf8 java/lang/runtime/ObjectMethods #47 = Utf8 bootstrap #48 = Utf8 (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; #49 = String #50 // x;y #50 = Utf8 x;y #51 = MethodHandle 1:#7 // REF_getField Point.x:F #52 = MethodHandle 1:#13 // REF_getField Point.y:F #53 = Utf8 InnerClasses #54 = Class #55 // java/lang/invoke/MethodHandles$Lookup #55 = Utf8 java/lang/invoke/MethodHandles$Lookup #56 = Class #57 // java/lang/invoke/MethodHandles #57 = Utf8 java/lang/invoke/MethodHandles #58 = Utf8 Lookup { private final float x; descriptor: F flags: (0x0012) ACC_PRIVATE, ACC_FINAL private final float y; descriptor: F flags: (0x0012) ACC_PRIVATE, ACC_FINAL public Point(float, float); descriptor: (FF)V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=3, args_size=3 0: aload_0 1: invokespecial #1 // Method java/lang/Record."<init>":()V 4: aload_0 5: fload_1 6: putfield #7 // Field x:F 9: aload_0 10: fload_2 11: putfield #13 // Field y:F 14: return LineNumberTable: line 1: 0 MethodParameters: Name Flags x y public java.lang.String toString(); descriptor: ()Ljava/lang/String; flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokedynamic #18, 0 // InvokeDynamic #0:toString:(LPoint;)Ljava/lang/String; 6: areturn LineNumberTable: line 1: 0 public final int hashCode(); descriptor: ()I flags: (0x0011) ACC_PUBLIC, ACC_FINAL Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokedynamic #22, 0 // InvokeDynamic #0:hashCode:(LPoint;)I 6: ireturn LineNumberTable: line 1: 0 public final boolean equals(java.lang.Object); descriptor: (Ljava/lang/Object;)Z flags: (0x0011) ACC_PUBLIC, ACC_FINAL Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: invokedynamic #26, 0 // InvokeDynamic #0:equals:(LPoint;Ljava/lang/Object;)Z 7: ireturn LineNumberTable: line 1: 0 public float x(); descriptor: ()F flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #16 // Field x:F 4: freturn LineNumberTable: line 1: 0 public float y(); descriptor: ()F flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #17 // Field y:F 4: freturn LineNumberTable: line 1: 0 } SourceFile: "Point.java" Record: float x; descriptor: F float y; descriptor: F BootstrapMethods: 0: #42 REF_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; Method arguments: #8 Point #49 x;y #51 REF_getField Point.x:F #52 REF_getField Point.y:F InnerClasses: public static final #58= #54 of #56; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
Na implementação de
toString()
,
hashCode()
e
equals()
, vemos
invokedynamic
. Isso significa que a lógica desses métodos será gerada preguiçosamente pela própria máquina virtual. Não sou um grande especialista em tempo de execução, mas acho que isso é feito para melhorar a eficiência. Por exemplo, se no futuro eles criarem um hash mais rápido, nessa abordagem, o código compilado antigo terá todas as vantagens da nova versão. Também reduz o tamanho dos arquivos de classe.
Mas algo que foi muito profundo. Vamos voltar aos nossos registros de
ovelhas . Vamos tentar instanciar um
Point
e ver como os métodos funcionam. A partir de agora, não usarei mais o
javac
e apenas executarei o arquivo java diretamente:
… public class Main { public static void main(String[] args) { var point = new Point(1, 2); System.out.println(point); System.out.println("hashCode = " + point.hashCode()); System.out.println("hashCode2 = " + Objects.hash(point.x(), point.y())); var point2 = new Point(1, 2); System.out.println(point.equals(point2)); } } record Point(float x, float y) { }
> java --enable-preview --source 14 Main.java Note: Main.java uses preview language features. Note: Recompile with -Xlint:preview for details. Point[x=1.0, y=2.0] hashCode = -260046848 hashCode2 = -260045887 true
Assim,
toString()
e
equals()
funcionam como eu esperava (bem, a menos que
toString()
use colchetes, mas eu gostaria de chaves). Mas o
hashCode()
funciona de maneira diferente. Por alguma razão, pensei que seria compatível com o
Objects.hash()
. Mas nada nos impede de criar nossa própria implementação de
hashCode()
. Vamos fazer isso e, ao mesmo tempo, transferiremos o método
main()
para dentro:
… public record Point(float x, float y) { @Override public int hashCode() { return Objects.hash(x, y); } public static void main(String[] args) { System.out.println(new Point(1, 2).hashCode()); } }
> java --enable-preview --source 14 Point.java Note: Point.java uses preview language features. Note: Recompile with -Xlint:preview for details. -260045887
Ok Agora vamos verificar o compilador quanto à durabilidade. Vamos fazer algo incorreto, por exemplo, adicione um campo:
public record Point(float x, float y) { private float z; }
Point.java:2: error: field declaration must be static private float z; ^ (consider replacing field with record component)
Portanto, você pode adicionar apenas campos estáticos.
Gostaria de saber o que acontecerá se você
final
os componentes. Eles serão ainda mais finais?
public record Point(final float x, final float y) { }
Point.java:1: error: record components cannot have modifiers public record Point(final float x, final float y) { ^ Point.java:1: error: record components cannot have modifiers public record Point(final float x, final float y) { ^
Talvez essa seja uma proibição lógica. Para que não haja ilusão de que os componentes se tornem mutáveis se você remover final. Sim, e
enum
tem uma regra semelhante, então nada de novo:
enum A { final X;
E se você substituir o tipo de método de acesso?
public record Point(float x, float y) { public double x() { return x; } }
Point.java:2: error: invalid accessor method in record Point public double x() { ^ (return type of accessor method x() is not compatible with type of record component x)
Isso é absolutamente lógico.
E se você mudar a visibilidade?
public record Point(float x, float y) { private float x() { return x; } }
Point.java:2: error: invalid accessor method in record Point private float x() { ^ (accessor method must be public)
Também é impossível.
A herança de classes é proibida, mesmo do
Object
:
public record Point(float x, float y) extends Object { }
Point.java:1: error: '{' expected public record Point(float x, float y) extends Object { ^
Mas você pode implementar interfaces:
public record Point(float x, float y) implements PointLike { public static void main(String[] args) { PointLike point = new Point(1, 2); System.out.println(point.x()); System.out.println(point.y()); } } public interface PointLike { float x(); float y(); }
> java --enable-preview --source 14 Point.java Note: Point.java uses preview language features. Note: Recompile with -Xlint:preview for details. 1.0 2.0
É interessante brincar um pouco com o construtor canônico. Primeiro, vamos escrever um construtor canônico compacto, ou seja, construtor canônico sem argumentos e inclua a validação dos parâmetros de entrada:
public record Point(float x, float y) { public Point { if (Float.isNaN(x) || Float.isNaN(y)) { throw new IllegalArgumentException("NaN"); } } public static void main(String[] args) { System.out.println(new Point(Float.NaN, 2)); } }
… Exception in thread "main" java.lang.IllegalArgumentException: NaN at Point.<init>(Point.java:4) at Point.main(Point.java:9)
Ganhou. Mas gostaria de saber se funcionará se você escrever o mesmo código, mas através do
return
:
public record Point(float x, float y) { public Point { if (!Float.isNaN(x) && !Float.isNaN(y)) { return; } throw new IllegalArgumentException("NaN"); } }
Point.java:2: error: invalid compact constructor in record Point(float,float) public Point { ^ (compact constructor must not have return statements)
Um detalhe interessante. É improvável que isso me machuque muito na vida, já que não sou fã de escrever
return
, mas todos os desenvolvedores de IDE precisam ter isso em mente.
Vamos tentar um construtor canônico explícito. Gostaria de saber se é possível renomear os parâmetros?
public record Point(float x, float y) { public Point(float _x, float _y) { if (Float.isNaN(_x) || Float.isNaN(_y)) { throw new IllegalArgumentException("NaN"); } this.x = _x; this.y = _y; } }
Point.java:2: error: invalid canonical constructor in record Point public Point(float _x, float _y) { ^ (invalid parameter names in canonical constructor)
Acontece que você não pode renomear. Mas não vejo nada de errado com essa restrição. O código será mais limpo.
E a ordem de inicialização?
public record Point(float x, float y) { public Point { System.out.println(this); } public static void main(String[] args) { System.out.println(new Point(-1, 2)); } }
… Point[x=0.0, y=0.0] Point[x=-1.0, y=2.0]
O primeiro
Point
impresso com zeros, o que significa que os campos foram atribuídos no final do construtor, após
System.out.println(this)
.
Bom Que tal adicionar um construtor não-canônico? Por exemplo, um construtor sem argumentos:
public record Point(float x, float y) { public Point() { } }
Point.java:2: error: constructor is not canonical, so its first statement must invoke another constructor public Point() { ^
Sim, esqueci de escrever
this(0, 0)
. Mas não vamos corrigir e verificar isso.
E os genéricos?
public record Point<A extends Number>(A x, A y) { public static void main(String[] args) { System.out.println(new Point<>(-1, 2)); } }
> java --enable-preview --source 14 Point.java Note: Point.java uses preview language features. Note: Recompile with -Xlint:preview for details. Point[x=-1, y=2]
Nada sobrenatural. Bem, exceto que você precisa lembrar que os parâmetros de tipo devem ser definidos antes da gravação dos parâmetros.
É possível criar um registro sem componentes?
public record None() { public static void main(String[] args) { System.out.println(new None()); } }
> java --enable-preview --source 14 None.java Note: None.java uses preview language features. Note: Recompile with -Xlint:preview for details. None[]
Porque não
Que coisas não tentamos? E as postagens aninhadas?
record Point(int x, int y) { record Nested(int z) { void print() { System.out.println(x); } } }
Point.java:4: error: non-static record component x cannot be referenced from a static context System.out.println(x); ^
Isso significa que as entradas aninhadas são sempre estáticas (como
enum
). Se sim, e se você declarar um registro local? Em teoria, ele não deve capturar um contexto não estático externo:
public class Main { public static void main(String[] args) { record Point(int x, int y) { void print() { System.out.println(Arrays.toString(args)); } } new Point(1, 2).print(); } }
> java --enable-preview --source 14 Main.java Note: Main.java uses preview language features. Note: Recompile with -Xlint:preview for details. []
Hmm, funcionou. Eu acho que isso é um bug. Ou apenas uma falha: esse comportamento é herdado de classes locais comuns que podem capturar variáveis externas efetivamente finais, mas esqueceram de corrigi-las para registros.
Um ponto sensível que me interessa: é possível criar vários registros públicos em um arquivo?
public record Point(float x, float y) { } public record Point2(float x, float y) { }
> javac --enable-preview --release 14 Point.java Point.java:4: error: class Point2 is public, should be declared in a file named Point2.java public record Point2(float x, float y) { ^
Isso é impossível. Será que isso será um problema em projetos reais? Certamente muitos vão querer escrever muitos registros para modelar suas entidades. Em seguida, você precisará decompor todos eles em seus próprios arquivos ou usar os registros anexados.
Por fim, também gostaria de brincar com a reflexão. Como em tempo de execução para descobrir informações sobre os componentes que um registro contém? Para isso, você pode usar o método
Class.getRecordComponents () :
import java.lang.reflect.RecordComponent; public record Point(float x, float y) { public static void main(String[] args) { var point = new Point(1, 2); for (RecordComponent component : point.getClass().getRecordComponents()) { System.out.println(component); } } }
> java --enable-preview --source 14 Point.java Note: Point.java uses preview language features. Note: Recompile with -Xlint:preview for details. float x float y
Também notei que no Java 14 um novo tipo de anotação apareceu especificamente para gravar componentes:
ElementType.RECORD_COMPONENT . E o que acontece se você usar os tipos antigos
FIELD
e
PARAMETER
? Afinal, os componentes parecem não ser campos nem parâmetros:
public record Point( @FieldAnnotation @ComponentAnnotation float x, @ParamAnnotation @ComponentAnnotation float y) { } @Target(ElementType.FIELD) @interface FieldAnnotation { } @Target(ElementType.PARAMETER) @interface ParamAnnotation { } @Target(ElementType.RECORD_COMPONENT) @interface ComponentAnnotation { }
Sim, o código é compilado, então todos os três funcionam. Bem, isso é lógico. Gostaria de saber se eles serão "arrastados" para os campos?
public record Point( @FieldAnnotation @ComponentAnnotation float x, @ParamAnnotation @ComponentAnnotation float y) { public static void main(String[] args) { var point = new Point(1, 2); Field[] fields = point.getClass().getDeclaredFields(); for (Field field : fields) { for (Annotation annotation : field.getAnnotations()) { System.out.println(field + ": " + annotation); } } } } @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @interface FieldAnnotation { } @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @interface ParamAnnotation { } @Target(ElementType.RECORD_COMPONENT) @Retention(RetentionPolicy.RUNTIME) @interface ComponentAnnotation { }
> java --enable-preview --source 14 Point.java Note: Point.java uses preview language features. Note: Recompile with -Xlint:preview for details. private final float Point.x: @FieldAnnotation()
Isso significa que apenas as anotações
FIELD
são "arrastadas", mas não
RECORD_COMPONENT
e
PARAMETER
.
Talvez eu termine com isso, porque o artigo já parece bastante complicado. Seria possível "cavar" por um longo tempo e profundamente, testando todos os tipos de casos de borda diferentes, mas acho que o nível atual de profundidade é mais que suficiente.
Conclusões
Os registros são, sem dúvida, algo interessante e muito esperado pela comunidade, que economizará tempo no futuro e uma quantidade enorme de código padrão. Agora, as gravações estão quase prontas, e resta apenas aguardar a correção de
algumas rugas e o lançamento público do Java 14. É verdade que você ainda precisará aguardar 1-2 lançamentos quando as gravações se tornarem estáveis, mas se desejar, elas podem ser usadas no modo de visualização .
E aqueles que não têm pressa de mudar do Java 8, acho que devemos esperar até setembro de 2021 e imediatamente mudar para o Java 17, onde já haverá
expressões de comutação estáveis,
blocos de texto ,
instância aprimorada de registros e
tipos selados (com alta probabilidade).
PS Se você não quiser perder minhas notícias e artigos sobre Java, recomendo que você assine
meu canal no Telegram .
Tudo com a vinda!