Java 14 registros y matrices

11

Dado el siguiente 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
}

Parece, por supuesto, de esa matriz toString, equalslos métodos se utilizan (en lugar de los métodos estáticos,Arrays::equals , Arrays::deepEquals o Array::toString).

Así que supongo que Java 14 Records ( JEP 359 ) no funcionan demasiado bien con las matrices, los métodos respectivos tienen que generarse con un IDE (que al menos en IntelliJ, por defecto genera métodos "útiles", es decir, usan los métodos estáticos en Arrays).

¿O hay alguna otra solución?

usuario140547
fuente
3
¿Qué tal usar en Listlugar de una matriz?
Oleg
No entiendo por qué tales métodos deben generarse con un IDE? Todos los métodos deben poder generarse a mano.
NomadMaker
1
Las toString(), equals()y los hashCode()métodos de un registro se implementan utilizando una referencia invokedynamic. . Si solo el equivalente de la clase compilada pudiera haber estado más cerca de lo que hace el método Arrays.deepToStringen su método privado sobrecargado hoy, podría haber resuelto los casos primitivos.
Naman
1
Para respaldar la opción de diseño para la implementación, para algo que no proporciona una implementación anulada de estos métodos, no es una mala idea recurrir Object, ya que eso también podría suceder con las clases definidas por el usuario. por ejemplo, incorrecto es igual
Naman
1
@Naman La elección de usar no invokedynamictiene absolutamente nada que ver con la selección de la semántica; indy es un detalle de implementación pura aquí. El compilador podría haber emitido bytecode para hacer lo mismo; esta era solo una forma más eficiente y flexible de llegar allí. Durante el diseño de los registros se discutió extensamente si utilizar una semántica de igualdad más matizada (como la igualdad profunda para matrices), pero esto causó muchos más problemas de los que supuestamente resolvió.
Brian Goetz

Respuestas:

18

Las matrices Java plantean varios desafíos para los registros, y estos agregaron una serie de restricciones al diseño. Las matrices son mutables, y su semántica de igualdad (heredada de Object) es por identidad, no por contenido.

El problema básico con su ejemplo es que desea que equals()en las matrices signifique igualdad de contenido, no igualdad de referencia. La semántica (predeterminada) equals()para los registros se basa en la igualdad de los componentes; en su ejemplo, los dos Fooregistros que contienen matrices distintas son diferentes y el registro se comporta correctamente. El problema es que solo desearías que la comparación de igualdad fuera diferente.

Dicho esto, puede declarar un registro con la semántica que desee, solo requiere más trabajo y puede sentir que es demasiado trabajo. Aquí hay un registro que hace lo que quieres:

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

Lo que esto hace es una copia defensiva al entrar (en el constructor) y al salir (en el descriptor de acceso), así como ajustar la semántica de igualdad para usar los contenidos de la matriz. Esto es compatible con el invariante, requerido en la superclasejava.lang.Record , que "separar un registro en sus componentes y reconstruir los componentes en un nuevo registro, produce un registro igual".

Bien podría decir "pero eso es demasiado trabajo, quería usar registros para no tener que escribir todo eso". Pero los registros no son principalmente una herramienta sintáctica (aunque son sintácticamente más agradables), son una herramienta semántica: los registros son tuplas nominales . La mayoría de las veces, la sintaxis compacta también produce la semántica deseada, pero si desea una semántica diferente, debe realizar un trabajo adicional.

Brian Goetz
fuente
66
Además, es un error común por parte de las personas que desean que la igualdad de la matriz sea por contenido asumir que nadie quiere la igualdad de la matriz por referencia. Pero eso simplemente no es cierto; es solo que no hay una respuesta única que funcione para todos los casos. A veces, la igualdad de referencia es exactamente lo que quieres.
Brian Goetz
9

List< Integer > solución alterna

Solución alternativa: utilice a Listof Integerobjects ( List< Integer >) en lugar de una matriz de primitivas ( int[]).

En este ejemplo, ejemplifico una lista no modificable de clase no especificada usando la List.ofcaracterística agregada a Java 9. También podría usar ArrayList, para una lista modificable respaldada por una 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
    }
}
Albahaca Bourque
fuente
Sin embargo, no siempre es una buena idea elegir Lista en lugar de una matriz y de todos modos
Naman
@Naman Nunca hice un reclamo sobre ser una "buena idea". La pregunta literalmente pedía otras soluciones. Yo proporcioné uno. Y lo hice una hora antes de los comentarios que vinculaste.
Basil Bourque
5

Solución alternativa: cree unaIntArrayclase y ajuste elint[].

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

No es perfecto, porque ahora tienes que llamar en foo.getInts()lugar de hacerlo foo.ints(), pero todo lo demás funciona de la manera que deseas.

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

Salida

Foo[ints=[1, 2]]
true
true
true
Andreas
fuente
1
¿No es eso equivalente a decirle que use una clase y no un registro en tal caso?
Naman
1
@Naman En absoluto, porque recordpuede consistir en muchos campos, y solo los campos de la matriz se envuelven de esta manera.
Andreas
Entiendo lo que está tratando de hacer en términos de reutilización, pero luego hay clases incorporadas Listque proporcionan ese tipo de envoltura que uno podría buscar con la solución propuesta aquí. ¿O considera que podría ser una sobrecarga para tales casos de uso?
Naman
3
@Naman Si desea almacenar un int[], entonces a List<Integer>no es lo mismo, al menos por dos razones: 1) La lista usa mucha más memoria y 2) No hay conversión integrada entre ellos. Integer[]🡘 List<Integer>es bastante fácil ( toArray(...)y Arrays.asList(...)), pero int[]🡘 List<Integer>requiere más trabajo y la conversión lleva tiempo, por lo que es algo que no desea hacer todo el tiempo. Si tiene un int[]y desea almacenarlo en un registro (con otras cosas, de lo contrario, ¿por qué usar un registro?) Y lo necesita como un int[]archivo, entonces la conversión cada vez que lo necesite está mal.
Andreas
Buen punto de hecho. Sin embargo, podría permitir que si quisieras cuestionar qué beneficios aún vemos Arrays.equals, Arrays.toStringsobre la Listimplementación utilizada mientras reemplazas la int[]que se sugiere en la otra respuesta. (Suponiendo que ambas sean soluciones alternativas de todos modos.)
Naman