Record e array Java 14


11

Dato il seguente codice:

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
}

Sembra, ovviamente, tale matrice toString, equalsmetodi sono utilizzati (invece di metodi statici, Arrays::equals, Arrays::deepEquals o Array::toString).

Quindi suppongo che Java 14 Records ( JEP 359 ) non funzionino troppo bene con le matrici, i rispettivi metodi devono essere generati con un IDE (che almeno in IntelliJ, per impostazione predefinita genera metodi "utili", cioè usano i metodi statici in Arrays).

O c'è qualche altra soluzione?


3
Che ne dici di usare Listinvece di un array?
Oleg,

Non capisco perché tali metodi debbano essere generati con un IDE? Tutti i metodi dovrebbero poter essere generati a mano.
NomadMaker

1
I toString(), equals()e hashCode()metodi di un record sono implementati utilizzando un riferimento invokedynamic. . Se solo l'equivalente di classe compilato avrebbe potuto essere più vicino a quello che il metodo Arrays.deepToStringfa oggi nel suo metodo di sovraccarico privato, avrebbe potuto risolvere i casi di primitive.
Naman,

1
Secondo la scelta progettuale per l'implementazione, per qualcosa che non fornisce un'implementazione forzata di questi metodi, non è una cattiva idea ricorrere a questo Object, poiché ciò potrebbe accadere anche con classi definite dall'utente. ad esempio, uguale a errato
Naman

1
@Naman La scelta da usare non invokedynamicha assolutamente nulla a che fare con la selezione della semantica; indy è un puro dettaglio di implementazione qui. Il compilatore avrebbe potuto emettere bytecode per fare la stessa cosa; questo era solo un modo più efficiente e flessibile per arrivarci. Durante la progettazione dei record è stato ampiamente discusso se usare una semantica di uguaglianza più sfumata (come l'uguaglianza profonda per le matrici), ma ciò si è rivelato causare molti più problemi di quanto si supponesse risolto.
Brian Goetz,

Risposte:


18

Le matrici Java pongono diverse sfide per i record e queste hanno aggiunto una serie di vincoli alla progettazione. Le matrici sono mutabili e la loro semantica sull'uguaglianza (ereditata dall'Oggetto) è per identità, non per contenuto.

Il problema di base con il tuo esempio è che desideri che equals()sugli array significasse uguaglianza dei contenuti, non riferimento all'uguaglianza. La semantica (predefinita) per equals()per i record si basa sull'uguaglianza dei componenti; nel tuo esempio, i due Foorecord contenenti array distinti sono diversi e il record si sta comportando correttamente. Il problema è che desideri solo che il confronto sull'uguaglianza fosse diverso.

Detto questo, puoi dichiarare un record con la semantica che vuoi, ci vuole solo più lavoro e potresti pensare che sia troppo lavoro. Ecco un disco che fa quello che vuoi:

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

Ciò che fa è una copia difensiva sull'ingresso (nel costruttore) e sull'uscita (nell'accessorio), oltre a regolare la semantica dell'uguaglianza per utilizzare il contenuto dell'array. Ciò supporta l'invariante, richiesto nella superclasse java.lang.Record, che "smontare un record nei suoi componenti e ricostruire i componenti in un nuovo record, produce un record uguale".

Potresti ben dire "ma è troppo lavoro, volevo usare i dischi, quindi non ho dovuto scrivere tutto quel materiale". Ma i record non sono principalmente uno strumento sintattico (sebbene siano sintatticamente più piacevoli), sono uno strumento semantico: i record sono tuple nominali . Il più delle volte, la sintassi compatta produce anche la semantica desiderata, ma se si desidera una semantica diversa, è necessario fare un lavoro extra.


6
Inoltre, è un errore comune da parte di persone che desiderano che l'uguaglianza di array sia per contenuto supporre che nessuno voglia mai l' uguaglianza di array per riferimento. Ma questo non è semplicemente vero; è solo che non esiste una risposta valida per tutti i casi. A volte l'uguaglianza di riferimento è esattamente ciò che vuoi.
Brian Goetz,

9

List< Integer > soluzione

Soluzione alternativa: utilizzare a Listof Integerobjects ( List< Integer >) anziché array di primitives ( int[]).

In questo esempio, ho un'istanza di un elenco non modificabile di classe non specificata utilizzando la List.offunzione aggiunta a Java 9. Si potrebbe anche usare ArrayList, per un elenco modificabile supportato da un array.

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

Non è sempre una buona idea scegliere List su un array e ne stiamo discutendo comunque.
Naman,

@Naman Non ho mai fatto alcuna pretesa di essere una "buona idea". La domanda ha letteralmente chiesto altre soluzioni. Ne ho fornito uno. E l'ho fatto un'ora prima dei commenti che hai collegato.
Basil Bourque,

5

Soluzione alternativa: creare unaIntArrayclasse e avvolgere il fileint[].

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

Non perfetto, perché ora devi chiamare foo.getInts()invece di foo.ints(), ma tutto il resto funziona nel modo desiderato.

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

Produzione

Foo[ints=[1, 2]]
true
true
true

1
Ciò non equivale a dire di usare una classe e non un record in tal caso?
Naman,

1
@Naman Niente affatto, perché il tuo recordpotrebbe essere costituito da molti campi e solo i campi dell'array sono racchiusi in questo modo.
Andreas,

Capisco il punto che stai cercando di fare in termini di riusabilità, ma poi ci sono classi integrate come quelle Listche forniscono quel tipo di involucro che si potrebbe cercare con la soluzione, come proposto qui. O pensi che potrebbe essere un sovraccarico per tali casi d'uso?
Naman,

3
@Naman Se si desidera memorizzare un int[], allora a List<Integer>non è lo stesso, per almeno due motivi: 1) L'elenco utilizza molta più memoria e 2) Non esiste una conversione integrata tra di loro. Integer[]🡘 List<Integer>è abbastanza semplice ( toArray(...)e Arrays.asList(...)), ma int[]🡘 List<Integer>richiede più lavoro e la conversione richiede tempo, quindi è qualcosa che non vuoi fare sempre. Se hai un int[]e vuoi memorizzarlo in un record (con altre cose, altrimenti perché usare un record), e ne int[]hai bisogno come un , quindi la conversione ogni volta che ne hai bisogno è sbagliata.
Andreas,

Buon punto davvero. Potrei però, se permetti che il nitpicking ti chieda con quali benefici vediamo ancora Arrays.equals, Arrays.toStringrispetto Listall'implementazione utilizzata mentre sostituisci il int[]suggerito nell'altra risposta. (Supponendo che entrambe siano comunque soluzioni alternative.)
Naman
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.