Enregistrements et tableaux Java 14

11

Étant donné le code suivant:

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
}

Il semble, de toute évidence, de ce tableau toString, les equalsméthodes sont utilisées ( au lieu de méthodes statiques, Arrays::equals, Arrays::deepEquals ou Array::toString).

Donc je suppose que les enregistrements Java 14 ( JEP 359 ) ne fonctionnent pas trop bien avec les tableaux, les méthodes respectives doivent être générées avec un IDE (qui au moins dans IntelliJ, génère par défaut des méthodes "utiles", c'est-à-dire qu'elles utilisent les méthodes statiques dans Arrays).

Ou existe-t-il une autre solution?

user140547
la source
3
Que diriez-vous d'utiliser Listau lieu d'un tableau?
Oleg
Je ne comprends pas pourquoi de telles méthodes doivent être générées avec un IDE? Toutes les méthodes doivent pouvoir être générées à la main.
NomadMaker
1
Les toString(), equals()et les hashCode()méthodes d'un enregistrement sont mises en œuvre en utilisant une référence invokedynamic. . Si seulement l'équivalent de classe compilé aurait pu être plus proche de ce que fait la méthode Arrays.deepToStringdans sa méthode privée surchargée aujourd'hui, il aurait pu résoudre les cas de primitives.
Naman
1
Pour seconder le choix de conception pour l'implémentation, pour quelque chose qui ne fournit pas une implémentation remplacée de ces méthodes, ce n'est pas une mauvaise idée de se replier Object, car cela pourrait également arriver avec les classes définies par l'utilisateur. par exemple des égaux incorrects
Naman
1
@Naman Le choix à utiliser invokedynamicn'a absolument rien à voir avec la sélection de la sémantique; indy est un pur détail d'implémentation ici. Le compilateur aurait pu émettre du bytecode pour faire la même chose; c'était juste un moyen plus efficace et plus flexible pour y arriver. Lors de la conception des enregistrements, il a été longuement discuté de l'opportunité d'utiliser une sémantique d'égalité plus nuancée (telle qu'une égalité profonde pour les tableaux), mais cela s'est avéré causer bien plus de problèmes qu'il n'en aurait résolu.
Brian Goetz

Réponses:

18

Les tableaux Java posent plusieurs défis pour les enregistrements, et ceux-ci ont ajouté un certain nombre de contraintes à la conception. Les tableaux sont modifiables et leur sémantique d'égalité (héritée d'Object) est basée sur l'identité et non sur le contenu.

Le problème de base avec votre exemple est que vous souhaitez que equals()sur les tableaux signifie l'égalité de contenu, pas l'égalité de référence. La sémantique (par défaut) equals()pour les enregistrements est basée sur l'égalité des composants; dans votre exemple, les deux Fooenregistrements contenant des tableaux distincts sont différents et l'enregistrement se comporte correctement. Le problème est que vous souhaitez juste que la comparaison d'égalité soit différente.

Cela dit, vous pouvez déclarer un enregistrement avec la sémantique que vous souhaitez, cela prend juste plus de travail, et vous pouvez penser que c'est trop de travail. Voici un enregistrement qui fait ce que vous voulez:

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

Ce que cela fait, c'est une copie défensive à l'entrée (dans le constructeur) et à la sortie (dans l'accesseur), ainsi qu'en ajustant la sémantique d'égalité pour utiliser le contenu du tableau. Cela prend en charge l'invariant, requis dans la superclasse java.lang.Record, que "démonter un enregistrement dans ses composants et reconstruire les composants dans un nouvel enregistrement, donne un enregistrement égal".

Vous pourriez bien dire "mais c'est trop de travail, je voulais utiliser des disques donc je n'ai pas eu à taper tout ça." Mais, les enregistrements ne sont pas principalement un outil syntaxique (bien qu'ils soient syntaxiquement plus agréables), ils sont un outil sémantique: les enregistrements sont des tuples nominaux . La plupart du temps, la syntaxe compacte produit également la sémantique souhaitée, mais si vous voulez une sémantique différente, vous devez faire un travail supplémentaire.

Brian Goetz
la source
6
En outre, c'est une erreur courante de ceux qui souhaitent que l'égalité de tableau soit par le contenu de supposer que personne ne veut jamais l' égalité de tableau par référence. Mais ce n'est tout simplement pas vrai; c'est juste qu'il n'y a pas de réponse unique qui fonctionne pour tous les cas. Parfois, l'égalité de référence est exactement ce que vous voulez.
Brian Goetz
9

List< Integer > solution de contournement

Solution: Utiliser un Listdes Integerobjets ( List< Integer >) plutôt que de tableau de primitives ( int[]).

Dans cet exemple, j'instancie une liste non modifiable de classe non spécifiée en utilisant la List.offonctionnalité ajoutée à Java 9. Vous pouvez tout aussi bien l'utiliser ArrayList, pour une liste modifiable appuyée par un tableau.

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
la source
Ce n'est pas toujours une bonne idée de choisir List plutôt qu'un tableau et nous en discutions quand même.
Naman
@Naman Je n'ai jamais prétendu être une «bonne idée». La Question demandait littéralement d'autres solutions. J'en ai fourni un. Et je l'ai fait une heure avant les commentaires que vous avez liés.
Basil Bourque
5

Solution: créez uneIntArrayclasse et encapsulez le fichierint[].

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

Pas parfait, car vous devez maintenant appeler foo.getInts()au lieu de foo.ints(), mais tout le reste fonctionne comme vous le souhaitez.

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

Production

Foo[ints=[1, 2]]
true
true
true
Andreas
la source
1
N'est-ce pas équivalent à dire d'utiliser une classe et non un enregistrement dans un tel cas?
Naman
1
@Naman Pas du tout, car votre recordpeut être composé de plusieurs champs, et seuls les champs du tableau sont enveloppés comme ceci.
Andreas
J'obtiens l'argument que vous essayez de faire valoir en termes de réutilisabilité, mais il y a ensuite des classes intégrées telles que celles Listqui fournissent ce type d'emballage que l'on pourrait rechercher avec la solution proposée ici. Ou pensez-vous que cela pourrait être une surcharge pour de tels cas d'utilisation?
Naman
3
@Naman Si vous souhaitez stocker un int[], alors a List<Integer>n'est pas le même, pour au moins deux raisons: 1) La liste utilise beaucoup plus de mémoire, et 2) Il n'y a pas de conversion intégrée entre eux. Integer[]🡘 List<Integer>est assez facile ( toArray(...)et Arrays.asList(...)), mais int[]🡘 demande List<Integer>plus de travail et la conversion prend du temps, c'est donc quelque chose que vous ne voulez pas faire tout le temps. Si vous en avez un int[]et que vous souhaitez le stocker dans un enregistrement (avec d'autres éléments, sinon pourquoi utiliser un enregistrement) et que int[]vous en avez besoin en tant que , la conversion à chaque fois que vous en avez besoin est incorrecte.
Andreas
Bon point en effet. Je pourrais cependant, si vous autorisez la piqûre demander quels avantages voyons-nous encore Arrays.equals, par Arrays.toStringrapport à la Listmise en œuvre utilisée tout en remplaçant le int[]comme suggéré dans l'autre réponse. (En supposant que ces deux solutions soient de toute façon.)
Naman