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
}

配列のことを、明らかに、そうですtoStringequals(、代わりに静的メソッドのメソッドが使用されているArrays::equalsArrays::deepEquals またはArray::toString)。

したがって、Java 14 Records(JEP 359)は配列ではうまく機能しないと思います。それぞれのメソッドはIDEで生成する必要があります(少なくともIntelliJでは、デフォルトで「便利な」メソッドを生成します。つまり、静的メソッドを使用します)でArrays)。

または他の解決策はありますか?


3
List配列の代わりに使用してみませんか?
Oleg

IDEでそのようなメソッドを生成する必要がある理由がわかりません。すべてのメソッドは手動で生成できる必要があります。
NomadMaker

1
toString()equals()およびhashCode()レコードの方法が実装されinvokedynamicの基準を使用して。。同等のコンパイル済みクラスのみArrays.deepToStringが、そのメソッドがそのプライベートオーバーロードメソッドで現在行っていることにより近い可能性がある場合、プリミティブの場合は解決された可能性があります。
ナマン

1
これらのメソッドのオーバーライドされた実装を提供しないものについては、実装の設計選択の2番目に、Objectユーザー定義クラスでも発生する可能性があるため、にフォールバックすることは悪い考えではありません。例:正しくないイコール
ナマン

1
@Naman使用invokedynamicする選択は、セマンティクスの選択とはまったく関係ありません。インディはここでの純粋な実装の詳細です。コンパイラーは同じことをするためにバイトコードを出力することができます。これは、そこに到達するためのより効率的で柔軟な方法でした。レコードの設計中に、よりニュアンスの高い等価セマンティクス(配列の深い等価など)を使用するかどうかについて広範囲にわたって議論されましたが、これにより、予想以上に多くの問題が発生することが判明しました。
ブライアンゲッツ

回答:


18

Java配列はレコードにいくつかの課題をもたらし、これらは設計に多くの制約を追加しました。配列は変更可能であり、その等価セマンティクス(Objectから継承)は内容ではなくIDによるものです。

この例の基本的な問題は、equals()配列では参照の等価性ではなくコンテンツの等価性を意味することを望んでいることです。equals()レコードの(デフォルト)セマンティクスは、コンポーネントの同等性に基づいています。あなたの例では、Foo異なる配列を含む2つのレコード異なり、レコードは正しく動作しています。問題は、平等の比較が異なることを望んでいることです。

とはいえ、必要なセマンティクスでレコードを宣言できます。これは、より多くの作業が必要であり、作業が多すぎるように感じる場合があります。これはあなたが望むことをするレコードです:

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
また、配列の等価性が内容によるものであり、参照によって配列の等価性が望まれることはないと仮定することを望む人々にとっては、よくある間違いです。しかし、それは単に真実ではありません。すべての場合に有効な答えが1つもないというだけです。参照の等価性は、まさにあなたが望むものです。
ブライアンゲッツ

9

List< Integer > 回避策

回避策:A ListIntegerオブジェクト(List< Integer >)ではなく、プリミティブの配列(int[])。

この例では、Java 9に追加された機能を使用して、未指定のクラスの変更不可能なリストをインスタンス化します。配列を基にした変更可能なリストには、をList.of使用することもでき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
    }
}

配列ではなくリストを選択することは必ずしも良い考えではありません。とにかくこれについて議論していました。
ナマン

@ナマン私は「良いアイデア」だと主張したことはありません。質問は文字通り他の解決策を求めました。1つ提供しました。そして、あなたがリンクしたコメントの1時間前にそうしました。
バジルブルク

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を保存する場合、少なくとも2つの理由で、int[]a List<Integer>は同じではありません。1)リストはより多くのメモリを使用します。2)それらの間の組み込みの変換はありません。Integer[]🡘 List<Integer>はかなり簡単(toArray(...)Arrays.asList(...))ですが、int[]🡘 List<Integer>より多くの作業が必要で、変換に時間がかかるため、常に実行したくないものです。があり、int[]それをレコードに保存したい場合(他のものと一緒に、それ以外の理由でレコードを使用する理由)、それをとして必要とする場合、必要なint[]ときに毎回変換するのは誤りです。
アンドレアス

確かに良い点。あなたが許可した場合でも、私はつべこべはメリットは、我々はまだで見る何をすべきかを尋ねることができArrays.equalsArrays.toString上でList交換しながら使用される実装int[]他の回答で提案されているように。(いずれにしても、これらはどちらも回避策であると想定しています。)
ナマン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.