Java 14-Datensätze und -Arrays

11

Gegeben den folgenden Code:

public static void main(String[] args) {
    record Foo(int[] ints){}

    var ints = new int[]{1, 2};
    var foo = new Foo(ints);
    System.out.println(foo); // Foo[ints=[I@6433a2]
    System.out.println(new Foo(new int[]{1,2}).equals(new Foo(new int[]{1,2}))); // false
    System.out.println(new Foo(ints).equals(new Foo(ints))); //true
    System.out.println(foo.equals(foo)); // true
}

Es scheint offensichtlich, das Arrays toString, equalsMethoden verwendet werden (anstelle von statischen Methoden Arrays::equals, Arrays::deepEquals oder Array::toString).

Ich denke, Java 14-Datensätze ( JEP 359 ) funktionieren mit Arrays nicht besonders gut. Die jeweiligen Methoden müssen mit einer IDE generiert werden (die zumindest in IntelliJ standardmäßig "nützliche" Methoden generiert, dh sie verwenden die statischen Methoden in Arrays).

Oder gibt es eine andere Lösung?

user140547
quelle
3
Wie wäre es mit Listanstelle eines Arrays?
Oleg
Ich verstehe nicht, warum solche Methoden mit einer IDE generiert werden müssen. Alle Methoden sollten von Hand generiert werden können.
NomadMaker
1
Die toString(), equals()und hashCode()Verfahren eines Datensatzes implementiert eine invokedynamic Referenz. . Wenn nur das kompilierte Klassenäquivalent näher an dem gewesen wäre, was die Methode heute Arrays.deepToStringin ihrer privaten überladenen Methode tut, hätte es möglicherweise für die primitiven Fälle gelöst.
Naman
1
Um die Entwurfsentscheidung für die Implementierung zu unterstützen, ist es keine schlechte Idee, auf etwas zurückzugreifen, das keine überschriebene Implementierung dieser Methoden bietet Object, da dies auch bei benutzerdefinierten Klassen passieren kann. zB falsch gleich
Naman
1
@Naman Die Wahl der Verwendung invokedynamichat absolut nichts mit der Auswahl der Semantik zu tun. indy ist hier ein reines Implementierungsdetail. Der Compiler hätte Bytecode ausgeben können, um dasselbe zu tun. Dies war nur ein effizienterer und flexiblerer Weg dorthin. Während des Entwurfs von Datensätzen wurde ausführlich diskutiert, ob eine differenziertere Gleichheitssemantik (wie z. B. tiefe Gleichheit für Arrays) verwendet werden soll, aber dies stellte sich als weitaus problematischer heraus, als es angeblich gelöst wurde.
Brian Goetz

Antworten:

18

Java-Arrays stellen Datensätze vor verschiedene Herausforderungen, die dem Design eine Reihe von Einschränkungen hinzufügen. Arrays sind veränderlich und ihre Gleichheitssemantik (von Object geerbt) beruht auf Identität und nicht auf Inhalten.

Das Grundproblem bei Ihrem Beispiel besteht darin, dass Sie wünschen, dass equals()auf Arrays Inhaltsgleichheit und nicht Referenzgleichheit bedeutet. Die (Standard-) Semantik für equals()Datensätze basiert auf der Gleichheit der Komponenten. in Ihrem Beispiel sind die beiden FooDatensätze verschiedene Arrays enthalten sind unterschiedlich, und der Datensatz korrekt verhält. Das Problem ist nur, dass Sie sich wünschen, der Gleichheitsvergleich wäre anders.

Das heißt, Sie können einen Datensatz mit der gewünschten Semantik deklarieren, es erfordert nur mehr Arbeit, und Sie haben möglicherweise das Gefühl, dass dies zu viel Arbeit ist. Hier ist eine Aufzeichnung, die macht, was Sie wollen:

record Foo(String[] ss) {
    Foo { ss = ss.clone(); }
    String[] ss() { return ss.clone(); }
    boolean equals(Object o) { 
        return o instanceof Foo 
            && Arrays.equals(((Foo) o).ss, ss);
    }
    int hashCode() { return Objects.hash(Arrays.hashCode(ss)); }
}

Dies ist eine defensive Kopie auf dem Weg nach innen (im Konstruktor) und auf dem Weg nach draußen (im Accessor) sowie die Anpassung der Gleichheitssemantik, um den Inhalt des Arrays zu verwenden. Dies unterstützt die in der Oberklasse geforderte Invariante, java.lang.Recorddass "das Zerlegen eines Datensatzes in seine Komponenten und das Rekonstruieren der Komponenten in einen neuen Datensatz einen gleichen Datensatz ergibt".

Man könnte sagen: "Aber das ist zu viel Arbeit. Ich wollte Datensätze verwenden, damit ich nicht all das Zeug eingeben muss." Datensätze sind jedoch nicht in erster Linie ein syntaktisches Werkzeug (obwohl sie syntaktisch angenehmer sind), sondern ein semantisches Werkzeug: Datensätze sind nominelle Tupel . Meistens liefert die kompakte Syntax auch die gewünschte Semantik. Wenn Sie jedoch eine andere Semantik wünschen, müssen Sie zusätzliche Arbeit leisten.

Brian Goetz
quelle
6
Es ist auch ein häufiger Fehler von Menschen, die wünschen, dass die Array-Gleichheit inhaltlich ist, anzunehmen, dass niemand jemals die Array-Gleichheit als Referenz wünscht. Das stimmt aber einfach nicht; Es gibt nur keine Antwort, die für alle Fälle funktioniert. Manchmal ist Referenzgleichheit genau das, was Sie wollen.
Brian Goetz
9

List< Integer > Problemumgehung

Umgehung: Verwenden Sie eine Listvon IntegerObjekten ( List< Integer >) statt Array von Primitiven ( int[]).

In diesem Beispiel instanziiere ich eine nicht modifizierbare Liste nicht spezifizierter Klassen mithilfe der List.ofzu Java 9 hinzugefügten Funktion. Sie können sie auch ArrayListfür eine modifizierbare Liste verwenden, die von einem Array unterstützt wird.

package work.basil.example;

import java.util.List;

public class RecordsDemo
{
    public static void main ( String[] args )
    {
        RecordsDemo app = new RecordsDemo();
        app.doIt();
    }

    private void doIt ( )
    {

        record Foo(List < Integer >integers)
        {
        }

        List< Integer > integers = List.of( 1 , 2 );
        var foo = new Foo( integers );

        System.out.println( foo ); // Foo[integers=[1, 2]]
        System.out.println( new Foo( List.of( 1 , 2 ) ).equals( new Foo( List.of( 1 , 2 ) ) ) ); // true
        System.out.println( new Foo( integers ).equals( new Foo( integers ) ) ); // true
        System.out.println( foo.equals( foo ) ); // true
    }
}
Basil Bourque
quelle
Es ist jedoch nicht immer eine gute Idee, List anstelle eines Arrays zu wählen, und wir haben dies trotzdem diskutiert.
Naman
@Naman Ich habe nie behauptet, eine „gute Idee“ zu sein. Die Frage fragte buchstäblich nach anderen Lösungen. Ich habe einen zur Verfügung gestellt. Und das habe ich eine Stunde vor den Kommentaren getan, die Sie verlinkt haben.
Basil Bourque
5

Problemumgehung: Erstellen Sie eineIntArrayKlasse und schließen Sie dieint[].

record Foo(IntArray ints) {
    public Foo(int... ints) { this(new IntArray(ints)); }
    public int[] getInts() { return this.ints.get(); }
}

Nicht perfekt, weil Sie jetzt foo.getInts()statt anrufen müssen foo.ints(), aber alles andere funktioniert so, wie Sie es möchten.

public final class IntArray {
    private final int[] array;
    public IntArray(int[] array) {
        this.array = Objects.requireNonNull(array);
    }
    public int[] get() {
        return this.array;
    }
    @Override
    public int hashCode() {
        return Arrays.hashCode(this.array);
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null || getClass() != obj.getClass())
            return false;
        IntArray that = (IntArray) obj;
        return Arrays.equals(this.array, that.array);
    }
    @Override
    public String toString() {
        return Arrays.toString(this.array);
    }
}

Ausgabe

Foo[ints=[1, 2]]
true
true
true
Andreas
quelle
1
Ist das nicht gleichbedeutend damit, in einem solchen Fall zu sagen, dass eine Klasse und kein Datensatz verwendet werden soll?
Naman
1
@Naman Überhaupt nicht, da Ihr recordFeld möglicherweise aus vielen Feldern besteht und nur die Array-Felder so umbrochen werden.
Andreas
Ich verstehe den Punkt, den Sie in Bezug auf die Wiederverwendbarkeit anstreben, aber dann gibt es eingebaute Klassen, Listdie die Art von Wrapping bieten, die man mit der hier vorgeschlagenen Lösung suchen könnte. Oder denken Sie, dass dies für solche Anwendungsfälle ein Overhead sein könnte?
Naman
3
@Naman Wenn Sie ein speichern möchten int[], ist a List<Integer>aus mindestens zwei Gründen nicht dasselbe: 1) Die Liste verwendet viel mehr Speicher und 2) es gibt keine integrierte Konvertierung zwischen ihnen. Integer[]🡘 List<Integer>ist ziemlich einfach ( toArray(...)und Arrays.asList(...)), aber int[]🡘 erfordert List<Integer>mehr Arbeit, und die Konvertierung braucht Zeit, sodass Sie dies nicht immer tun möchten. Wenn Sie eine haben int[]und diese in einem Datensatz speichern möchten (mit anderen Dingen, andernfalls warum sollten Sie einen Datensatz verwenden) und sie als benötigen int[], ist die Konvertierung jedes Mal, wenn Sie sie benötigen, falsch.
Andreas
Guter Punkt in der Tat. Ich könnte jedoch, wenn Sie Nitpicking zulassen, fragen, welche Vorteile wir noch sehen Arrays.equals, Arrays.toStringgegenüber der ListImplementierung, die beim Ersetzen der int[]in der anderen Antwort vorgeschlagenen verwendet wird. (Vorausgesetzt, beide sind sowieso Problemumgehungen.)
Naman