¿Los registros de Java 14 realmente ahorran memoria sobre una declaración de clase similar o se parecen más al azúcar sintáctico?

8

Espero que los registros de Java 14 realmente usen menos memoria que una clase de datos similar.

¿Son o el uso de memoria es el mismo?

Clancy Merrick
fuente
66
Si lo entendí correctamente, el compilador genera una clase final que extiende el Registro con los accesores, las variables de instancia, el constructor necesario y toString, hashCode y métodos igual. Así que supongo que la memoria utilizada sería muy similar. Por supuesto, el código fuente usaría menos memoria;)
lugiorgi
44
¿De dónde crees que vendrían los ahorros de memoria? Obviamente, todavía tendrían que almacenar todos los componentes.
Brian Goetz
@BrianGoetz Eso se entiende. Si no le importa responder una pregunta posterior, me preguntaba sobre la diferencia en la representación de bytecode y las constantes dinámicas invocadas que se utilizan allí. (¿Hay alguna manera de encontrar el valor de todas estas constantes dentro o fuera de JDK?). Si hay una buena cantidad de detalles para entender aquí, me encantaría crear otras preguntas y respuestas aquí.
Naman
2
Usamos invokedynamicpara generar perezosamente las implementaciones de métodos Object (equals, hashCode) en lugar de generarlas estáticamente en tiempo de compilación.
Brian Goetz

Respuestas:

7

Para agregar al análisis básico realizado por @lugiorgi y una diferencia notable similar que podría llegar a analizar el código de bytes, está en la implementación de toString, equalsy hashcode.

Por un lado, clase utilizada anteriormente con ObjectAPI de clase anuladas que se parecen a

public class City {
    private final Integer id;
    private final String name;
    // all-args, toString, getters, equals, and hashcode
}

produce el código de bytes de la siguiente manera

 public java.lang.String toString();
    Code:
       0: aload_0
       1: getfield      #7                  // Field id:Ljava/lang/Integer;
       4: aload_0
       5: getfield      #13                 // Field name:Ljava/lang/String;
       8: invokedynamic #17,  0             // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/Integer;Ljava/lang/String;)Ljava/lang/String;
      13: areturn

  public boolean equals(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: if_acmpne     7
       5: iconst_1
       6: ireturn
       7: aload_1
       8: ifnull        22
      11: aload_0
      12: invokevirtual #21                 // Method java/lang/Object.getClass:()Ljava/lang/Class;
      15: aload_1
      16: invokevirtual #21                 // Method java/lang/Object.getClass:()Ljava/lang/Class;
      19: if_acmpeq     24
      22: iconst_0
      23: ireturn
      24: aload_1
      25: checkcast     #8                  // class edu/forty/bits/records/equals/City
      28: astore_2
      29: aload_0
      30: getfield      #7                  // Field id:Ljava/lang/Integer;
      33: aload_2
      34: getfield      #7                  // Field id:Ljava/lang/Integer;
      37: invokevirtual #25                 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
      40: ifne          45
      43: iconst_0
      44: ireturn
      45: aload_0
      46: getfield      #13                 // Field name:Ljava/lang/String;
      49: aload_2
      50: getfield      #13                 // Field name:Ljava/lang/String;
      53: invokevirtual #31                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      56: ireturn

  public int hashCode();
    Code:
       0: aload_0
       1: getfield      #7                  // Field id:Ljava/lang/Integer;
       4: invokevirtual #34                 // Method java/lang/Integer.hashCode:()I
       7: istore_1
       8: bipush        31
      10: iload_1
      11: imul
      12: aload_0
      13: getfield      #13                 // Field name:Ljava/lang/String;
      16: invokevirtual #38                 // Method java/lang/String.hashCode:()I
      19: iadd
      20: istore_1
      21: iload_1
      22: ireturn

Por otro lado la representación de registro para el mismo

record CityRecord(Integer id, String name) {}

produce el bytecode tan poco como

 public java.lang.String toString();
    Code:
       0: aload_0
       1: invokedynamic #19,  0             // InvokeDynamic #0:toString:(Ledu/forty/bits/records/equals/CityRecord;)Ljava/lang/String;
       6: areturn

  public final int hashCode();
    Code:
       0: aload_0
       1: invokedynamic #23,  0             // InvokeDynamic #0:hashCode:(Ledu/forty/bits/records/equals/CityRecord;)I
       6: ireturn

  public final boolean equals(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: invokedynamic #27,  0             // InvokeDynamic #0:equals:(Ledu/forty/bits/records/equals/CityRecord;Ljava/lang/Object;)Z
       7: ireturn

Nota : Por lo que pude observar en los accesos y el código de bytes de constructor generado, son iguales tanto para la representación como, por lo tanto, también están excluidos de los datos aquí.

Naman
fuente
1

Hice algunas pruebas rápidas y sucias con los siguientes

public record PersonRecord(String firstName, String lastName) {}

vs.

import java.util.Objects;

public final class PersonClass {
    private final String firstName;
    private final String lastName;

    public PersonClass(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String firstName() {
        return firstName;
    }

    public String lastName() {
        return lastName;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PersonClass that = (PersonClass) o;
        return firstName.equals(that.firstName) &&
                lastName.equals(that.lastName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(firstName, lastName);
    }

    @Override
    public String toString() {
        return "PersonRecord[" +
                "firstName=" + firstName +
                ", lastName=" + lastName +
                "]";
    }
}

El archivo de registro compilado asciende a 1.475 bytes, la clase a 1.643 bytes. La diferencia de tamaño probablemente proviene de diferentes implementaciones equals / toString / hashCode.

Tal vez alguien pueda hacer una búsqueda de código de bytes ...

lugiorgi
fuente
0

correcta, estoy de acuerdo con [@lugiorgi] y [@Naman], la única diferencia en el código de bytes que se genera entre un registro y la clase es equivalente en la aplicación de métodos: toString, equalsy hashCode. Que en el caso de una clase de registro se implementan utilizando una instrucción dinámica de invocación (indy) para el mismo método de arranque en clase: java.lang.runtime.ObjectMethods(recién agregado en el proyecto de registros). El hecho de que estos tres métodos, toString, equalsy hashCode, invocar el mismo arranque método ahorra más espacio en el archivo de clase 3 de invocar métodos bootstraps diferentes. Y, por supuesto, como ya se mostró en las otras respuestas, ahorra más espacio que generar el bytecode obvio

Vicente Romero
fuente