Записи и массивы Java 14

Учитывая следующий код:

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
}

Очевидно, что этот массив toString, equals используются методы (вместо статических, Arrays::equals,Arrays::deepEquals или Array::toString).

Поэтому я полагаю, что записи Java 14 ( JEP 359) не слишком хорошо работают с массивами, соответствующие методы должны быть сгенерированы с помощью IDE (которая, по крайней мере, в IntelliJ, по умолчанию генерирует "полезные" методы, т.е. они используют статические методы. вArrays).

Или есть другое решение?

3 ответа

Решение

Массивы Java создают несколько проблем для записей, и это добавило ряд ограничений к конструкции. Массивы изменяемы, и их семантика равенства (унаследованная от Object) определяется идентичностью, а не содержимым.

Основная проблема с вашим примером заключается в том, что вы хотите, чтобы equals()для массивов означало равенство содержимого, а не равенство ссылок. Семантика (по умолчанию) дляequals()для записей основывается на равенстве компонентов; в вашем примере дваFooзаписи, содержащие отдельные массивы , различны, и запись ведет себя правильно. Проблема в том, что вы просто хотите, чтобы сравнение равенства было другим.

Тем не менее, вы можете объявить запись с нужной семантикой, это просто требует больше работы, и вам может показаться, что это слишком много. Вот запись, которая делает то, что вы хотите:

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

Это защитная копия на входе (в конструкторе) и на выходе (в методе доступа), а также настройка семантики равенства для использования содержимого массива. Это поддерживает инвариант, необходимый в суперклассеjava.lang.Record, что "разделение записи на компоненты и реконструкция компонентов в новую запись дает одинаковую запись".

Вы могли бы сказать: "Но это слишком много работы, я хотел использовать записи, чтобы не печатать все это". Но записи - это в первую очередь не синтаксический инструмент (хотя они синтаксически более приятны), они являются семантическим инструментом: записи представляют собой номинальные кортежи. В большинстве случаев компактный синтаксис также дает желаемую семантику, но если вам нужна другая семантика, вам придется проделать некоторую дополнительную работу.

List< Integer > обходной путь

Решение: используйте List из Integer объекты (List< Integer >), а не массив примитивов (int[]).

В этом примере я создаю неизменяемый список неуказанного класса с помощью List.of в Java 9 добавлена ​​функция. С таким же успехом можно использовать ArrayList для изменяемого списка, поддерживаемого массивом.

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

Решение: создайтеIntArray класс и завернуть int[].

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

Не идеально, потому что теперь вам нужно позвонить foo.getInts() вместо того foo.ints(), но все остальное работает так, как вы хотите.

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

Выход

Foo[ints=[1, 2]]
true
true
true
Другие вопросы по тегам