Sparen Java 14-Datensätze tatsächlich Speicher über eine ähnliche Klassendeklaration oder ähneln sie eher syntaktischem Zucker?

8

Ich hoffe, dass Java 14-Datensätze tatsächlich weniger Speicher belegen als eine ähnliche Datenklasse.

Sind sie oder ist die Speichernutzung gleich?

Clancy Merrick
quelle
6
Wenn ich es richtig verstanden habe, generiert der Compiler eine letzte Klasse, die Record mit Accessoren, den Instanzvariablen, dem benötigten Konstruktor und den Methoden toString, hashCode und equals erweitert. Ich gehe also davon aus, dass der verwendete Speicher sehr ähnlich ist. Natürlich würde der Quellcode weniger Speicher
verbrauchen
4
Woher würden Ihrer Meinung nach die Speichereinsparungen kommen? Sie müssten natürlich noch alle Komponenten lagern.
Brian Goetz
@BrianGoetz Das ist verstanden. Wenn es Ihnen nichts ausmacht, eine nachfolgende Frage zu beantworten, habe ich mich über den Unterschied in der Bytecode-Darstellung und den dort verwendeten aufgerufenen dynamischen Konstanten gewundert. (Gibt es eine Möglichkeit, den Wert für alle diese Konstanten innerhalb oder außerhalb von JDK zu ermitteln?) Wenn hier eine Menge Details zu verstehen sind, würde ich gerne hier ein weiteres Q & A erstellen.
Naman
2
Wir verwenden invokedynamic, um die Implementierungen von Object-Methoden (equals, hashCode) träge zu generieren, anstatt sie zur Kompilierungszeit statisch zu generieren.
Brian Goetz

Antworten:

7

Zu der von @lugiorgi durchgeführten Basisanalyse und einem ähnlichen bemerkenswerten Unterschied, den ich bei der Analyse des Bytecodes finden könnte, gehört die Implementierung von toString, equalsund hashcode.

Einerseits zuvor verwendete Klasse mit überschriebenen ObjectKlassen-APIs, die aussehen

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

erzeugt den Bytecode wie folgt

 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

Zum anderen die Datensatzdarstellung dafür

record CityRecord(Integer id, String name) {}

erzeugt den Bytecode so klein wie

 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

Hinweis : Soweit ich die generierten Accessoren und Konstruktorbyte-Codes beobachten konnte, sind sie sowohl für die Darstellung gleich als auch daher auch hier von den Daten ausgeschlossen.

Naman
quelle
1

Ich habe einige schnelle und schmutzige Tests mit folgenden durchgeführt

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

Die kompilierte Datensatzdatei beträgt 1,475 Byte, die Klasse 1,643 Byte. Der Größenunterschied beruht wahrscheinlich auf verschiedenen Implementierungen von equals / toString / hashCode.

Vielleicht kann jemand Bytecode graben ...

lugiorgi
quelle
0

richtig, ich bin einverstanden mit [@lugiorgi] und [@Naman], der einzigen Unterschied in der erzeugten Bytecode zwischen einem Datensatz und der äquivalenten Klasse ist bei der Umsetzung von Methoden: toString, equalsund hashCode. Welche im Fall einer Datensatzklasse mithilfe einer dynamischen (Indy-) Aufrufanweisung für dieselbe Bootstrap-Methode in der Klasse implementiert werden: java.lang.runtime.ObjectMethods(frisch im Datensatzprojekt hinzugefügt). Die Tatsache , dass diese drei Methoden toString, equalsund hashCoderufen Sie den gleichen Bootstrap - Methode mehr Platz in der Klassendatei speichert als Methoden 3 verschiedene Schopf aus dem Sumpf aufgerufen wird . Und natürlich spart, wie bereits in den anderen Antworten gezeigt, mehr Platz als das Generieren des offensichtlichen Bytecodes

Vicente Romero
quelle