Записи та масиви 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).

Або є якесь інше рішення?


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

Не завжди гарна ідея хоч вибрати Список через масив, і ми все одно це обговорювали.
Наман

@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

1
Хіба це не рівнозначно сказати використовувати клас, а не запис у такому випадку?
Наман

1
@Naman Зовсім не тому, що ваше recordможе складатися з багатьох полів, і лише поля масивів загортаються таким чином.
Андреас

Я розумію, що ви намагаєтеся зробити з точки зору повторного використання, але існують такі вбудовані класи, Listякі забезпечують такий вид обгортання, який можна шукати з запропонованим тут рішенням. Або ви вважаєте, що це може бути накладними витратами на такі випадки використання?
Наман

3
@Naman Якщо ви хочете зберегти int[]файл, то a List<Integer>- це не те саме, щонайменше з двох причин: 1) У списку використовується набагато більше пам’яті; 2) між ними немає вбудованої конверсії. Integer[]🡘 List<Integer>досить легко ( toArray(...)і Arrays.asList(...)), але int[]🡘 List<Integer>потребує більше роботи, і перетворення вимагає часу, тому це те, що ви не хочете робити весь час. Якщо у вас є int[]і хочете зберегти його в записі (з іншими речами, інакше навіщо використовувати запис), і вам потрібен як an int[], тоді перетворення кожного разу, коли вам це потрібно, буде неправильним.
Андреас

Дійсний момент. Я можу, хоч, якщо ви дозволите, що запитуєте, запитайте, які переваги ми все ще бачимо Arrays.equals, Arrays.toStringнад Listреалізацією, яка використовується, замінюючи те int[], що було запропоновано в іншій відповіді. (Якщо припустити, що це обоє вирішуються.)
Наман
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.