Os registros Java 14 realmente economizam memória em uma declaração de classe semelhante ou são mais como açúcar sintático?

8

Espero que os registros Java 14 realmente usem menos memória que uma classe de dados semelhante.

Eles ou o uso de memória é o mesmo?

Clancy Merrick
fonte
6
Se eu entendi direito, o compilador gera uma classe final estendendo Record com acessadores, as variáveis ​​de instância, o construtor necessário e os métodos toString, hashCode e equals. Então, suponho que a memória usada seria muito semelhante. Claro que o código fonte usaria menos memória;)
lugiorgi 14/04
4
De onde você acha que a economia de memória viria? Obviamente, eles ainda teriam que armazenar todos os componentes.
Brian Goetz
@BrianGoetz Isso é entendido. Se você não se importa em responder a uma pergunta subsequente - eu estava pensando sobre a diferença na representação de bytecode e nas constantes dinâmicas invocadas usadas lá. (Existe uma maneira de encontrar o valor para todas essas constantes dentro ou fora do JDK?). Se houver uma boa quantidade de detalhes a serem entendidos aqui, eu ficaria feliz em criar outra sessão de perguntas e respostas aqui.
Naman
2
Usamos invokedynamicpara gerar preguiçosamente as implementações dos métodos Object (equals, hashCode) em vez de gerá-las estaticamente em tempo de compilação.
Brian Goetz

Respostas:

7

Para adicionar à análise básica realizada pelo @lugiorgi e uma diferença perceptível semelhante que eu poderia analisar com o código de bytes, está na implementação de toString, equalse hashcode.

Por um lado, a classe usada anteriormente com ObjectAPIs de classe substituídas que parecem

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

produz o código de bytes da seguinte maneira

 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 outro lado, a representação de registro para o mesmo

record CityRecord(Integer id, String name) {}

produz o bytecode tão menos quanto

 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 : Para o que eu pude observar nos acessadores e no código de bytes do construtor gerado, eles são parecidos para a representação e, portanto, excluídos dos dados aqui também.

Naman
fonte
1

Eu fiz alguns testes rápidos e sujos com as seguintes

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 +
                "]";
    }
}

O arquivo de registro compilado equivale a 1.475 bytes, a classe a 1.643 bytes. A diferença de tamanho provavelmente vem de diferentes implementações iguais / toString / hashCode.

Talvez alguém possa cavar um bytecode ...

lugiorgi
fonte
0

correcta, I de acordo com [@lugiorgi] e [@Naman], a única diferença do código de bytes gerado entre uma ficha e a classe é equivalente na execução de métodos de: toString, equalse hashCode. Que no caso de uma classe de registro é implementada usando uma instrução de chamada dinâmica (indy) para o mesmo método de autoinicialização na classe: java.lang.runtime.ObjectMethods(adicionado recentemente no projeto de registros). O fato de que esses três métodos, toString, equalse hashCode, invocar a mesma inicialização método economiza mais espaço no arquivo de classe do que invocando 3 diferentes métodos Bootstraps. E, é claro, como já mostrado nas outras respostas, economiza mais espaço do que gerar o óbvio bytecode

Vicente Romero
fonte