Explorando registros Java 14

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; // No modifiers allowed for enum constants } 

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!

Source: https://habr.com/ru/post/pt482300/


All Articles