Java 14 rekordów i tablic

11

Biorąc pod uwagę następujący kod:

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
}

Wydaje się, oczywiście, na tej tablicy toString, equalssposoby są stosowane (zamiast metod statycznych Arrays::equals, Arrays::deepEquals bądź Array::toString).

Sądzę więc, że Java 14 Records ( JEP 359 ) nie działa zbyt dobrze z tablicami, odpowiednie metody muszą zostać wygenerowane za pomocą IDE (które przynajmniej w IntelliJ domyślnie generuje „przydatne” metody, tj. Używają metod statycznych w Arrays).

Czy jest jakieś inne rozwiązanie?

użytkownik140547
źródło
3
Co powiesz na użycie Listzamiast tablicy?
Oleg
Nie rozumiem, dlaczego takie metody muszą być generowane za pomocą IDE? Wszystkie metody powinny być możliwe do wygenerowania ręcznie.
NomadMaker
1
Te toString(), equals()oraz hashCode()sposoby zapisu są realizowane przy użyciu invokedynamic odniesienia. . Gdyby tylko skompilowany ekwiwalent klasy mógł być bliższy temu, co Arrays.deepToStringrobi dziś metoda w swojej prywatnej, przeciążonej metodzie, mógłby rozwiązać problem pierwotnych przypadków.
Naman
1
Po drugie, wybór projektu do implementacji, w przypadku czegoś, co nie zapewnia przesłoniętej implementacji tych metod, nie jest złym pomysłem, aby do niego wrócić Object, ponieważ może się to zdarzyć również w przypadku klas zdefiniowanych przez użytkownika. np. niepoprawny równa się
Naman
1
@Naman Wybór zastosowania invokedynamicnie ma absolutnie nic wspólnego z wyborem semantyki; indy jest tutaj czystym szczegółem implementacji. Kompilator mógł wysłać kod bajtowy, aby zrobić to samo; był to po prostu bardziej wydajny i elastyczny sposób dotarcia do celu. Podczas projektowania rekordów szeroko dyskutowano, czy należy stosować bardziej szczegółową semantykę równości (np. Głęboką równość dla tablic), ale okazało się, że powoduje to o wiele więcej problemów, niż przypuszczalnie rozwiązano.
Brian Goetz

Odpowiedzi:

18

Macierze Java stanowią kilka wyzwań dla rekordów, które dodały wiele ograniczeń w projekcie. Tablice są zmienne, a ich semantyka równości (dziedziczona z Object) jest oparta na tożsamości, a nie na zawartości.

Podstawowym problemem związanym z twoim przykładem jest to, że chciałbyś, aby equals()tablice oznaczały równość treści, a nie równość odniesienia. (Domyślna) semantyka dla equals()rekordów oparta jest na równości składników; w twoim przykładzie dwa Foorekordy zawierające odrębne tablice różne, a rekord zachowuje się poprawnie. Problem polega na tym, że po prostu chciałbyś, aby porównanie równości było inne.

To powiedziawszy, możesz zadeklarować rekord z pożądaną semantyką, to po prostu wymaga więcej pracy i możesz poczuć, że to zbyt wiele pracy. Oto rekord, który robi to, co chcesz:

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

To robi obronną kopię na wejściu (w konstruktorze) i na wyjściu (w akcesorium), a także dostosowuje semantykę równości do korzystania z zawartości tablicy. Wspiera to niezmiennik wymagany w nadklasie java.lang.Record, który „rozebranie rekordu na jego komponenty i przebudowanie komponentów na nowy rekord, daje taki sam rekord”.

Można powiedzieć: „ale to za dużo pracy, chciałem używać płyt, więc nie musiałem wpisywać tych wszystkich rzeczy”. Ale rekordy nie są przede wszystkim narzędziem składniowym (chociaż są przyjemniejsze składniowo), są narzędziem semantycznym: rekordy są krotkami nominalnymi . Przez większość czasu zwarta składnia daje również pożądaną semantykę, ale jeśli chcesz mieć inną semantykę, musisz wykonać dodatkową pracę.

Brian Goetz
źródło
6
Ponadto powszechnym błędem osób, które chcą, aby równość tablic była z treści, jest założenie, że nikt nigdy nie chce równości tablic przez odniesienie. Ale to po prostu nieprawda; po prostu nie ma jednej odpowiedzi, która działałaby we wszystkich przypadkach. Czasami równość odniesienia jest dokładnie tym, czego chcesz.
Brian Goetz
9

List< Integer > obejście

Rozwiązanie: Użyć Listz Integerprzedmiotów ( List< Integer >) niż szereg pierwotnych ( int[]).

W tym przykładzie tworzę instancję niemodyfikowalnej listy nieokreślonej klasy, używając List.offunkcji dodanej do Java 9. Równie dobrze możesz użyć ArrayListdo modyfikowalnej listy wspieranej przez tablicę.

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
źródło
Jednak nie zawsze dobrym pomysłem jest wybranie List zamiast tablicy, ai tak dyskutowaliśmy.
Naman
@Naman Nigdy nie twierdziłem, że jestem „dobrym pomysłem”. Pytanie dosłownie dotyczyło innych rozwiązań. Podałem jeden. Zrobiłem to godzinę przed komentarzami, które podłączyłeś.
Basil Bourque
5

Obejście: UtwórzIntArrayklasę i zawińint[].

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

Nie idealnie, bo teraz musisz dzwonić foo.getInts()zamiast foo.ints(), ale wszystko inne działa tak, jak chcesz.

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

Wynik

Foo[ints=[1, 2]]
true
true
true
Andreas
źródło
1
Czy to nie jest równoznaczne z mówieniem o użyciu klasy, a nie zapisaniem w takim przypadku?
Naman
1
@Naman Wcale nie, ponieważ twój recordmoże składać się z wielu pól, a tylko pola (pola) tablicy są opakowane w ten sposób.
Andreas
Rozumiem, co próbujesz zrobić, jeśli chodzi o możliwość ponownego użycia, ale są też wbudowane klasy, Listktóre zapewniają takie opakowanie, którego można szukać za pomocą proponowanego tutaj rozwiązania. A może uważasz, że takie przypadki użycia mogą być narzutem?
Naman
3
@Naman Jeśli chcesz zapisać int[], to a List<Integer>nie jest takie samo, z co najmniej dwóch powodów: 1) Lista zajmuje dużo więcej pamięci i 2) Nie ma wbudowanej konwersji między nimi. Integer[]🡘 List<Integer>jest dość łatwe ( toArray(...)i Arrays.asList(...)), ale int[]🡘 List<Integer>wymaga więcej pracy, a konwersja wymaga czasu, więc jest to coś, czego nie chcesz cały czas robić. Jeśli masz int[]i chcesz zapisać go w rekordzie (z innymi rzeczami, w przeciwnym razie dlaczego warto go użyć) i potrzebujesz go jako int[], to konwersja za każdym razem, gdy go potrzebujesz, jest błędna.
Andreas
Rzeczywiście dobra uwaga. Mógłbym jednak, jeśli pozwolisz, aby nitpicking pytał, jakie korzyści nadal widzimy Arrays.equals, w Arrays.toStringporównaniu z Listzastosowaną implementacją, zastępując int[]jak sugerowano w drugiej odpowiedzi. (Zakładając, że oba są i tak obejściami).
Naman