Explorando los registros de Java 14

La última vez que probamos la instanceof mejorada del operador, que aparecerá en la próxima versión 14 de Java (se lanzará en marzo de 2020). Hoy me gustaría investigar en detalle la segunda característica sintáctica, que también aparecerá en Java 14: registros.

Los registros tienen su propio JEP , pero no muestra muchos detalles, por lo que hay mucho que probar y verificar por usted mismo. Sí, por supuesto, puede abrir la especificación Java SE, pero me parece que es mucho más interesante comenzar a escribir código usted mismo y observar el comportamiento del compilador en ciertas situaciones. Así que haz una gaviota y siéntate cómodamente. Vamos

A diferencia de la última vez, cuando tuve que construir una rama especial de JDK por instanceof prueba, ahora todo esto ya está en la rama principal y está disponible en la primera versión de JDK 14 , que descargué.

Primero, implementamos el ejemplo clásico con Point y lo 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 compiló Point.class archivo Point.class . Desmontémoslo y veamos qué generó el compilador para nosotros allí:

 > 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(); } 

Sí, el compilador creó lo siguiente:

  • La clase final heredada de java.lang.Record (similar a enum , que hereda de java.lang.Enum ).
  • Campos finales privados x e y .
  • Un constructor público que coincide con la firma del registro en sí. Tal constructor se llama canónico .
  • Implementaciones de toString() , hashCode() y equals() . Curiosamente, hashCode() y equals() son final , y toString() no lo es. Es poco probable que esto afecte algo, ya que la clase en sí es final , pero ¿alguien sabe por qué hicieron esto? (No estoy)
  • Métodos para la lectura de campos.

Con el constructor y los métodos de lectura, todo está claro, pero me pregunto cómo equals() implementan exactamente toString() , hashCode() y equals() . A ver Para hacer esto, ejecute javap con el indicador -verbose :

Pasador de desmontaje largo
 > 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 

En la implementación de toString() , hashCode() y equals() vemos invokedynamic . Esto significa que la lógica de estos métodos será generada por la propia máquina virtual. No soy un gran especialista en tiempo de ejecución, pero creo que esto se hace para una mejor eficiencia. Por ejemplo, si en el futuro se les ocurre un hash más rápido, entonces, en este enfoque, el antiguo código compilado obtendrá todas las ventajas de la nueva versión. También reduce el tamaño de los archivos de clase.

Pero algo fuimos demasiado profundo. Volvamos a nuestros registros de ovejas . Intentemos instanciar un Point y ver cómo funcionan los métodos. De ahora en adelante, ya no javac y simplemente ejecutaré el archivo java directamente:

 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 

Por lo tanto, toString() y equals() funcionan como esperaba (bueno, a menos que toString() use corchetes, pero me gustaría usar llaves). Pero hashCode() funciona de manera diferente. Por alguna razón, pensé que sería compatible con Objects.hash() . Pero nada nos impide crear nuestra propia implementación de hashCode() . Hagámoslo, y al mismo tiempo transferiremos el método main() 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 Ahora verifiquemos la durabilidad del compilador. Hagamos algo incorrecto, por ejemplo, agregue un 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) 

Por lo tanto, puede agregar solo campos estáticos.

Me pregunto qué pasará si haces que los componentes sean final . ¿Serán aún más definitivos?

 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) { ^ 

Quizás esta es una prohibición lógica. Para que no haya ilusión de que los componentes se volverán mutables si quita final. Sí, y enum tiene una regla similar, así que nada nuevo:

 enum A { final X; // No modifiers allowed for enum constants } 

¿Qué pasa si anula el tipo de método de acceso?

 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) 

Esto es absolutamente lógico.

¿Y si cambias la visibilidad?

 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) 

También es imposible.

Se prohíbe heredar de clases, incluso de Object :

 public record Point(float x, float y) extends Object { } 

 Point.java:1: error: '{' expected public record Point(float x, float y) extends Object { ^ 

Pero puedes 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 

Es interesante jugar un poco con el constructor canónico. Primero, escribamos un constructor canónico compacto, es decir constructor canónico sin argumentos, y agregue la validación de los parámetros de entrada allí:

 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) 

Ganado Pero me pregunto si funcionará si escribes el mismo código, pero a través de 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) 

Un detalle interesante Es poco probable que esto me haga mucho daño en la vida, ya que no soy fanático de escribir return , pero todos los desarrolladores de IDE deben tener esto en cuenta.

Probemos un constructor canónico explícito. Me pregunto si es posible cambiar el nombre de los 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) 

Resulta que no puedes cambiar el nombre. Pero no veo nada malo con tal restricción. El código será más limpio.

¿Y qué hay del orden de inicialización?

 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] 

El primer Point imprimió con ceros, lo que significa que los campos se asignaron al final del constructor, después de System.out.println(this) .

Bueno ¿Qué tal agregar un constructor no canónico? Por ejemplo, un constructor sin 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() { ^ 

Sí, olvidé escribir this(0, 0) . Pero no corregiremos y verificaremos esto.

¿Qué pasa con los 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 Bueno, excepto que debe recordar que los parámetros de tipo deben establecerse antes de grabar los parámetros.

¿Es posible crear un registro sin 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[] 

Por qué no

¿Qué cosas no hemos probado? ¿Qué pasa con las publicaciones anidadas?

 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); ^ 

Esto significa que las entradas anidadas siempre son estáticas (como enum ). Si es así, ¿qué pasa si declaras un registro local? En teoría, entonces no debería capturar un contexto externo no estático:

 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, funcionó. Creo que esto es un error. O simplemente una falla: este comportamiento se hereda de las clases locales ordinarias que pueden capturar variables externas efectivamente finales, pero se olvidaron de arreglarlas para los registros.

Un punto doloroso que me interesa: ¿es posible crear varios registros públicos en un solo archivo?

 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) { ^ 

Es imposible Me pregunto si esto será un problema en proyectos reales. Seguramente muchos querrán escribir muchos registros para modelar sus entidades. Luego tendrá que descomponerlos todos en sus propios archivos, o usar los registros adjuntos.

Finalmente, también me gustaría jugar con la reflexión. ¿Cómo, en tiempo de ejecución, encontrar información sobre los componentes que contiene un registro? Para esto, puede usar el 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 

También noté que en Java 14 apareció un nuevo tipo de anotación específicamente para grabar componentes: ElementType.RECORD_COMPONENT . ¿Y qué sucede si usa los tipos antiguos FIELD y PARAMETER ? Después de todo, los componentes parecen no ser campos ni 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 { } 

Sí, el código se compila, así que los tres funcionan. Bueno, eso es lógico. Me pregunto si serán "arrastrados" a los 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() 

Esto significa que solo las anotaciones de FIELD se "arrastran", pero no RECORD_COMPONENT y PARAMETER .

Quizás termine con esto, porque el artículo ya parece bastante engorroso. Sería posible "cavar" durante mucho tiempo y profundamente, probando todo tipo de casos de borde diferentes, pero creo que el nivel actual de profundidad es más que suficiente.

Conclusiones


Los registros son, sin duda, algo genial y muy esperado por la comunidad que nos ahorrará tiempo en el futuro y nos ahorrará una gran cantidad de código repetitivo. Ahora las grabaciones están casi listas, y solo queda esperar a que se solucionen algunas asperezas y se publique el lanzamiento público de Java 14. Es cierto, entonces aún tendrá que esperar 1-2 lanzamientos cuando las grabaciones se estabilicen, pero si lo desea, pueden usarse en modo de vista previa .

Y aquellos que no tienen prisa por cambiar de Java 8, creo que deberíamos esperar hasta septiembre de 2021 e inmediatamente cambiar a Java 17, donde ya habrá expresiones de cambio estables, bloques de texto , instancia mejorada de registros y tipos sellados (con alta probabilidad).

PD: si no quiere perderse mis noticias y artículos sobre Java, le recomiendo que se suscriba a mi canal en Telegram .

Todo con la venida!

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


All Articles