Javaの配列でequalsがfalseになる理由とArrays.equalsの違いを解説する記事のアイキャッチ。

Java クイズ

Javaの配列でequalsがfalseになる理由|Arrays.equalsとの違い

AIによる要約

Javaの配列は内容比較用のequalsをオーバーライドしていないため、a.equals(b)は同じ配列オブジェクトかを確認します。要素を順番に比較するならArrays.equals、多次元配列ならArrays.deepEqualsを使います。JUnitでもassertEqualsではなくassertArrayEqualsを選びます。配列をDTOやMapのキーに持たせる場合は、比較だけでなくhashCodeや防御的コピーも検討します。

この記事はXへ投稿したJavaクイズの詳しい解説です。まず問題を解きたい方は、Xの元ポストを開くことができます。

新人SE
新人SE
同じ1と2が入ったint配列なのに、equalsがfalseになるのはバグですか?
ポンコツSE
ポンコツSE
配列のequalsは要素ではなく、同じ配列オブジェクトかを見ます。内容比較にはArrays.equalsを使います。

文字列やListではequalsで内容を比較できるため、配列も同じだと思いやすいところです。しかし配列はObjectのequalsをそのまま使い、参照が同じかを比較します。

バイト列、CSVの列、外部APIのレスポンス、暗号化前後のデータなど、業務コードでも配列比較は登場します。条件分岐だけでなく、JUnitの期待値比較やDTOのequalsにも影響するため、用途ごとのAPIを選びます。

この記事のポイント

  • 配列のequalsは同じ配列参照かを比較する
  • 1次元配列の内容比較はArrays.equalsを使う
  • 多次元配列はArrays.deepEqualsを使う
  • JUnitではassertArrayEqualsを使う
  • 配列をフィールドに持つクラスではequals・hashCode・コピーを確認する

まずはXのJavaクイズを確認する

同じ要素を持つint配列でequalsとArrays.equalsの結果を問うJavaクイズ画像。
2つの出力はtrueとfalseのどの組み合わせでしょうか。
int[] a = {1, 2};
int[] b = {1, 2};

System.out.println(a.equals(b));
System.out.println(Arrays.equals(a, b));

答えはfalse、trueです。a.equals(b)は参照を、Arrays.equals(a, b)は各要素を比較します。

配列equalsはfalse、Arrays.equalsはtrueになると説明する画像。
答えはfalse、trueです。比較している対象が異なります。

元の投稿は、Xの出題ポスト回答ポストでも確認できます。

配列のequalsは参照比較

配列型はObjectを継承していますが、内容比較用のequalsを独自実装していません。そのため、同じ変数を指す場合だけtrueになります。

int[] a = {1, 2};
int[] same = a;
int[] copy = {1, 2};

System.out.println(a.equals(same)); // true
System.out.println(a.equals(copy)); // false

==でも同じ参照比較になります。配列の見た目や要素数が同じでも、newや配列リテラルで別々に作れば別オブジェクトです。

1次元配列はArrays.equalsで比較する

Arrays.equalsは、長さが同じか確認した後、先頭から要素を比較します。プリミティブ配列向けとオブジェクト配列向けのオーバーロードがあります。

byte[] expected = {10, 20, 30};
byte[] actual = response.getBody();

if (!Arrays.equals(expected, actual)) {
    throw new IllegalStateException("応答データが一致しません");
}

配列全体を文字列へ変換して比較する方法は、表示形式へ依存し意図が伝わりません。内容比較という目的を直接表すArrays.equalsを使います。

多次元配列はArrays.deepEqualsを使う

2次元配列は、要素自体が配列です。Arrays.equalsだけでは内側の配列を参照として比較するため、内容が同じでもfalseになることがあります。

int[][] a = {{1, 2}, {3, 4}};
int[][] b = {{1, 2}, {3, 4}};

System.out.println(Arrays.equals(a, b));     // false
System.out.println(Arrays.deepEquals(a, b)); // true

多次元構造を配列で持つ必要があるかも確認します。行や列に意味がある業務データなら、専用クラスやListにした方が読みやすく、検証処理も置きやすい場合があります。

nullを含む比較

Arrays.equalsは両方がnullならtrue、片方だけnullならfalseを返します。ただし、null変数に対してインスタンスメソッドのequalsを呼ぶとNullPointerExceptionです。

byte[] a = null;
byte[] b = null;

System.out.println(Arrays.equals(a, b)); // true
System.out.println(Objects.deepEquals(a, b)); // true

nullを空配列と同じとみなすかは別の業務判断です。『データ未取得』と『取得結果0件』を区別するなら、比較前に状態を分けます。

JUnitではassertArrayEqualsを使う

JUnitのassertEqualsへ配列を渡すと、期待した要素比較になりません。配列専用のassertArrayEqualsを使うと、異なる要素位置も分かりやすく報告されます。

@Test
void CSVの列を順番どおり変換する() {
    String[] actual = converter.split("A,B,C");

    assertArrayEquals(
            new String[]{"A", "B", "C"},
            actual);
}

順序を問わない仕様なら配列そのものが合っていない可能性があります。Setへ変換する、並べ替えて比較するなど、業務上の一致条件をテストへ反映します。

配列をフィールドに持つクラスのequals

IDEやLombokでequalsを生成しても、配列フィールドの扱いを確認します。内容比較とhashCodeが対応していないと、SetやHashMapで見つからない問題につながります。

final class FileSignature {
    private final byte[] value;

    @Override
    public boolean equals(Object obj) {
        return obj instanceof FileSignature other
                && Arrays.equals(value, other.value);
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(value);
    }
}

さらにコンストラクタやgetterで配列参照をそのまま渡すと、外部から要素を書き換えられます。識別子として使うなら防御的コピーも検討します。

比較条件をテストで固定する

同じ内容、異なる順序、長さ違い、null、空配列、多次元というケースを用意します。大きな配列では、失敗時に全データをログへ出して機密情報やログ容量の問題を起こさないようにします。

assertTrue(Arrays.equals(new int[]{1, 2}, new int[]{1, 2}));
assertFalse(Arrays.equals(new int[]{1, 2}, new int[]{2, 1}));
assertFalse(Arrays.equals(new int[]{1}, new int[]{1, 2}));
assertTrue(Arrays.equals(new int[]{}, new int[]{}));

現場レビューでよくある指摘

配列比較のレビューでは、equalsという名前だけで判断せず、参照、要素、順序、深さのどれを一致条件にするか確認します。

// レビューコメント例
配列のequalsは要素比較になりません。
1次元配列の内容を比べるならArrays.equalsを使ってください。

// レビューコメント例
これは2次元配列なので、Arrays.equalsでは内側の配列を参照比較します。
Arrays.deepEqualsまたはデータ構造の見直しを検討してください。

// レビューコメント例
テストのassertEqualsでは配列内容を比較できません。
assertArrayEqualsへ変更し、順序違いも追加してください。

『同じ』の意味が参照一致なのか、要素と順序の一致なのかをレビュー返信で明確にします。比較APIの置換だけでなく、DTOやテストの契約までそろえます。

提出前のセルフチェック

レビュー前に確認すること

  • 参照と内容のどちらを比較するか決めたか
  • 1次元配列にArrays.equalsを使ったか
  • 多次元配列にdeepEqualsが必要か
  • nullと空配列を区別したか
  • 順序を一致条件に含めるか
  • JUnitでassertArrayEqualsを使ったか
  • equalsとhashCodeが対応しているか
  • 配列の外部変更を防ぐ必要があるか

equalsとコレクションを深める参考書

配列の比較はequalsの契約やハッシュ値ともつながります。Javaの基本を押さえた後に、Effective Javaで値オブジェクトと防御的コピーの判断を補強できます。

書籍「Effective Java 第3版」の表紙
商品画像:Amazon.co.jp
PR Java基礎を終えた若手向け

Effective Java 第3版

Joshua Bloch (著), 柴田 芳樹 (翻訳)

Javaらしい設計と実装の判断基準を深める定番書。

equals、例外、ジェネリクス、ラムダ式などを、設計上の原則から学べます。初学者の一冊目ではなく、実務経験後の振り返り向けです。

  • Javaコードの設計判断を深めたい
  • レビュー指摘の背景まで理解したい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

まとめ

Java配列のequalsは要素ではなく同じ配列オブジェクトかを比較します。1次元配列の内容比較にはArrays.equals、多次元配列にはArrays.deepEqualsを使います。

テストではassertArrayEqualsを選び、null、空配列、順序、長さも確認してください。配列を値オブジェクトへ持たせる場合は、equalsとhashCode、防御的コピーまで一緒に設計します。

-Java, クイズ
-, , , ,