Java 14 записей и массивов

11

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

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)

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

user140547
источник
3
Как насчет использования Listвместо массива?
Олег
Я не понимаю, почему такие методы должны генерироваться с IDE? Все методы должны быть созданы вручную.
NomadMaker
1
В toString(), equals()и hashCode()способы записи реализованы с использованием invokedynamic ссылки. , Если бы только эквивалент скомпилированного класса мог быть ближе к тому, что метод Arrays.deepToStringделает сегодня в своем закрытом перегруженном методе, он мог бы решить проблему для примитивов.
Наман
1
Во-вторых, выбор дизайна для реализации, для чего-то, не обеспечивающего переопределенную реализацию этих методов, не является плохой идеей, к которой можно вернуться Object, поскольку это может произойти и с пользовательскими классами. например, неверно равно
Наман
1
@Naman Выбор использования не invokedynamicимеет абсолютно никакого отношения к выбору семантики; Здесь indy - чистая деталь реализации. Компилятор мог выдать байт-код, чтобы сделать то же самое; это был просто более эффективный и гибкий способ добраться туда. Во время разработки записей широко обсуждалось, следует ли использовать более тонкую семантику равенства (например, глубокое равенство для массивов), но оказалось, что это вызывает гораздо больше проблем, чем предполагалось.
Брайан Гетц

Ответы:

18

Массивы 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, который «разбивая запись на ее компоненты и восстанавливая компоненты в новую запись, дает равную запись».

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

Брайан Гетц
источник
6
Кроме того, люди, желающие, чтобы равенство массивов было по содержанию, было распространенной ошибкой, полагая, что никто никогда не хочет равенство массивов по ссылке. Но это просто неправда; просто нет единого ответа, который подходит для всех случаев. Иногда ссылочное равенство - это именно то, что вы хотите.
Брайан Гетц
9

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
    }
}
Базилик Бурк
источник
Это не всегда хорошая идея, чтобы выбрать List из массива, и мы все равно обсуждали это.
Наман
@Naman Я никогда не претендовал на то, чтобы быть «хорошей идеей». Вопрос буквально задан для других решений. Я предоставил один. И я сделал это за час до комментариев, которые вы связали.
Василий Бурк
5

Обходной путь: создайте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
Andreas
источник
1
Разве это не эквивалентно использованию класса, а не записи в таком случае?
Наман
1
@Naman Вовсе нет, потому что ваш recordможет состоять из множества полей, и только поля массива оборачиваются так.
Андреас
Я понимаю, что вы пытаетесь сделать с точки зрения возможности повторного использования, но есть и встроенные классы, Listкоторые предоставляют такую ​​оболочку, которую можно искать с решением, предложенным здесь. Или вы считаете, что это может привести к накладным расходам для таких случаев использования?
Наман
3
@Naman Если вы хотите сохранить an int[], то a List<Integer>не то же самое, по крайней мере по двум причинам: 1) список использует намного больше памяти, и 2) между ними нет встроенного преобразования. Integer[]🡘 List<Integer>довольно легко ( toArray(...)и Arrays.asList(...)), но int[]🡘 List<Integer>требует больше работы, а преобразование занимает время, поэтому вы не хотите делать это все время. Если у вас есть int[]и вы хотите сохранить его в записи (с другими вещами, в противном случае зачем использовать запись), и вам нужно это как int[], тогда преобразование каждый раз, когда вам нужно, неправильно.
Андреас
Хороший вопрос действительно. Я мог бы, однако, если вы позволите придираться, спрашивая, какие преимущества мы все еще видим Arrays.equals, по Arrays.toStringсравнению сList реализацией, используемой при замене, int[]как предложено в другом ответе. (Предполагая, что оба они в любом случае являются обходными путями.)
Наман