Registros e matrizes Java 14

11

Dado o seguinte código:

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
}

Ao que parece, obviamente, que do array toString, equalsos métodos são utilizados (em vez de métodos estáticos, Arrays::equals, Arrays::deepEquals ou Array::toString).

Então, acho que o Java 14 Records ( JEP 359 ) não funciona muito bem com matrizes, os respectivos métodos precisam ser gerados com um IDE (que pelo menos no IntelliJ, por padrão, gera métodos "úteis", ou seja, eles usam os métodos estáticos in Arrays).

Ou existe alguma outra solução?

user140547
fonte
3
Que tal usar em Listvez de uma matriz?
Oleg
Não entendo por que esses métodos precisam ser gerados com um IDE? Todos os métodos devem poder ser gerados manualmente.
NomadMaker 16/04
11
Os métodos toString(), equals()e hashCode()de um registro são implementados usando uma referência dinâmica invocada. . Se apenas o equivalente da classe compilada pudesse estar mais próximo do que o método Arrays.deepToStringfaz hoje em seu método privado sobrecarregado, ele pode ter sido resolvido para os casos primitivos.
Naman 17/04
11
Para secundar a opção de design para implementação, para algo que não fornece uma implementação substituída desses métodos, não é uma má idéia recorrer Object, pois isso também pode acontecer com as classes definidas pelo usuário. por exemplo, iguais incorretos
Naman
11
@ Naman A escolha de usar não invokedynamictem absolutamente nada a ver com a seleção da semântica; indy é um puro detalhe de implementação aqui. O compilador poderia ter emitido o bytecode para fazer a mesma coisa; essa era apenas uma maneira mais eficiente e flexível de chegar lá. Foi discutido extensivamente durante o design dos registros se seria usada uma semântica de igualdade mais sutil (como igualdade profunda para matrizes), mas isso acabou causando muito mais problemas do que supostamente resolvia.
Brian Goetz

Respostas:

18

As matrizes Java representam vários desafios para os registros, e isso adicionou uma série de restrições ao design. As matrizes são mutáveis ​​e sua semântica de igualdade (herdada de Object) é por identidade, não por conteúdo.

O problema básico do seu exemplo é que você deseja que equals()em matrizes signifique igualdade de conteúdo, não referência a igualdade. A semântica (padrão) equals()para registros é baseada na igualdade dos componentes; no seu exemplo, os dois Fooregistros contendo matrizes distintas são diferentes e o registro está se comportando corretamente. O problema é que você apenas deseja que a comparação de igualdade seja diferente.

Dito isto, você pode declarar um registro com a semântica desejada, basta apenas mais trabalho e você pode achar que é muito trabalho. Aqui está um registro que faz o que você deseja:

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

O que isso faz é uma cópia defensiva na entrada (no construtor) e na saída (no acessador), além de ajustar a semântica da igualdade para usar o conteúdo da matriz. Isso suporta o invariante, exigido na superclasse java.lang.Record, que "desmontar um registro em seus componentes e reconstruir os componentes em um novo registro, produz um registro igual".

Você pode dizer "mas isso é muito trabalho, eu queria usar registros para não precisar digitar tudo isso". Porém, os registros não são primariamente uma ferramenta sintática (embora sejam sintaticamente mais agradáveis), são uma ferramenta semântica: registros são tuplas nominais . Na maioria das vezes, a sintaxe compacta também produz a semântica desejada, mas se você quiser uma semântica diferente, precisará fazer algum trabalho extra.

Brian Goetz
fonte
6
Além disso, é um erro comum das pessoas que desejam que a igualdade de matrizes seja pelo conteúdo, assumindo que ninguém jamais deseja igualdade de matrizes por referência. Mas isso simplesmente não é verdade; é que não há uma resposta que funcione para todos os casos. Às vezes, igualdade de referência é exatamente o que você deseja.
Brian Goetz
9

List< Integer > Gambiarra

Solução alternativa: use um Listde Integerobjetos ( List< Integer >) em vez de uma matriz de primitivas ( int[]).

Neste exemplo, instanciamos uma lista não modificável de classe não especificada usando o List.ofrecurso adicionado ao Java 9. Você também pode usar ArrayListuma lista modificável apoiada por uma matriz.

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
fonte
Porém, nem sempre é uma boa ideia escolher Lista em um array e estávamos discutindo isso de qualquer maneira.
Naman 17/04
@ Naman Eu nunca fiz uma reivindicação sobre ser uma “boa ideia”. A pergunta literalmente pedia outras soluções. Eu forneci um. E fiz isso uma hora antes dos comentários que você vinculou.
Basil Bourque 17/04
5

Solução alternativa: crie umaIntArrayclasse e envolva oint[].

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

Não é perfeito, porque agora você precisa ligar em foo.getInts()vez de foo.ints(), mas tudo funciona da maneira que você deseja.

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

Resultado

Foo[ints=[1, 2]]
true
true
true
Andreas
fonte
11
Não é o mesmo que dizer para usar uma classe e não um registro nesse caso?
Naman 17/04
11
@ Naman De modo algum, porque o seu recordpode consistir em muitos campos e apenas os campos da matriz são agrupados dessa maneira.
Andreas
Entendo o que você está tentando enfatizar em termos de reutilização, mas há classes embutidas, como as Listque fornecem o tipo de embalagem que se pode buscar com a solução proposta aqui. Ou você considera que isso pode ser uma sobrecarga para esses casos de uso?
Naman 17/04
3
@ Naman Se você deseja armazenar um int[], então a List<Integer>não é o mesmo, por pelo menos dois motivos: 1) A lista usa muito mais memória e 2) Não há conversão interna entre eles. Integer[]🡘 List<Integer>é bastante fácil ( toArray(...)e Arrays.asList(...)), mas int[]🡘 List<Integer>exige mais trabalho e a conversão leva tempo, portanto é algo que você não deseja fazer o tempo todo. Se você possui int[]e deseja armazená-lo em um registro (com outras coisas, caso contrário, por que usar um registro), e precisa dele como um int[], então a conversão sempre que precisar é errada.
Andreas
Bom ponto de fato. Eu poderia, se você permitir, perguntar quais benefícios ainda vemos Arrays.equals, Arrays.toStringsobre a Listimplementação usada ao substituir o int[]sugerido na outra resposta. (Supondo que ambas sejam soluções alternativas de qualquer maneira.)
Naman 17/04