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

実装・エラー解決

Java配列のequalsがfalseになる原因|Arrays.equalsとassertArrayEqualsで直す

AIによる要約

Java配列のequalsやJUnitのassertEqualsへ別々の配列を渡すと、要素が同じでも一致しません。業務コードの1次元配列はArrays.equals、JUnitの期待値比較はassertArrayEqualsを使います。この記事では同じ入力で失敗と修正後の成功を確かめ、順序・長さ違い、多次元配列、配列フィールドの防御的コピーまで整理します。

新人SE
新人SE
同じ1と2が入った配列なのに、equalsがfalseです。JUnitのassertEqualsも失敗します。要素を比べるにはどう直せばよいですか?
ポンコツSE
ポンコツSE
配列のequalsは同じ配列オブジェクトかを見ます。業務コードではArrays.equals、JUnitではassertArrayEqualsを選び、同じ入力で結果を確かめます。

JUnitで同じ要素の配列を比較して失敗した場合は、まずassertEquals(expected, actual)をassertArrayEquals(expected, actual)へ変更します。JUnitの再現テストでは、別々に作った{1, 2}を使い、修正前の失敗と修正後の成功を同じ条件で確認できます。業務コードの条件分岐を直す場合は、先に次の表でArrays.equalsとArrays.deepEqualsを選んでください。

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

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

この記事のポイント

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

配列の比較方法を目的から選ぶ

確認したいこと使う方法注意点
同じ配列オブジェクトかa == b要素が同じでも別配列ならfalse
1次元配列の要素と順序が同じかArrays.equals(a, b)長さ・順序も比較する
配列が入れ子になっているかArrays.deepEquals(a, b)外側がObject[]になる多次元配列に使う
JUnitで配列の期待値を検証するかassertArrayEquals(expected, actual)assertEqualsへ配列を渡さない

Java 17で以下の参照・要素・多次元比較とFileSignatureの動作を確認しました。内容が一致しているのにequalsがfalseなら、まず1次元か多次元かを見分けます。

最小例:equalsとArrays.equalsの結果を確認する

同じ要素を持つint配列でequalsとArrays.equalsの結果を問うJavaクイズ画像。
同じ要素を持つ別々の配列を比較します。出力はfalse、trueになります。
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を使うと、異なる要素位置も分かりやすく報告されます。

同じ要素でもassertEqualsが失敗する例と修正

検証環境はJava 17.0.8、JUnit Jupiter 5.12.2、Maven 3.9.16です。org.junit.jupiter:junit-jupiter:5.12.2をテスト依存関係に設定したJava 17のMavenプロジェクトで、次のクラス全体をsrc/test/java/ArrayComparisonTest.javaへ保存し、mvn testで実行できます。既存プロジェクトへ追加する場合は、プロジェクトで採用しているJUnitのバージョンも確認してください。

最初のテストでは、expectedとactualを別々の配列として作ります。assertEqualsはAssertionFailedErrorになるので、修正前の失敗をassertThrowsで確認しています。その直後のassertArrayEqualsは、同じ2つの配列を変更せずに比較する修正後の例です。

import java.util.Arrays;

import org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

class ArrayComparisonTest {

    @Test
    void assertEqualsFailsButAssertArrayEqualsPasses() {
        int[] expected = {1, 2};
        int[] actual = {1, 2};

        // Before: 要素が同じでも、別配列へのassertEqualsは失敗する
        assertThrows(AssertionFailedError.class,
                () -> assertEquals(expected, actual));

        // After: 同じ入力を配列専用のアサーションで比較する
        assertArrayEquals(expected, actual);
    }

    @Test
    void differentOrderIsDetected() {
        assertThrows(AssertionFailedError.class,
                () -> assertArrayEquals(
                        new int[]{1, 2}, new int[]{2, 1}));
    }

    @Test
    void differentLengthIsDetected() {
        assertThrows(AssertionFailedError.class,
                () -> assertArrayEquals(
                        new int[]{1, 2}, new int[]{1, 2, 3}));
    }

    @Test
    void nestedArraysAreComparedByContents() {
        int[][] expected = {{1, 2}, {3, 4}};
        int[][] actual = {{1, 2}, {3, 4}};

        assertFalse(Arrays.equals(expected, actual));
        assertTrue(Arrays.deepEquals(expected, actual));
        assertArrayEquals(expected, actual);

        assertThrows(AssertionFailedError.class,
                () -> assertArrayEquals(
                        expected, new int[][]{{1, 2}, {3, 5}}));
    }
}

assertThrowsは、指定した型のエラーが発生したときに成功します。修正前の失敗を検証するために囲んでいるので、このクラスのテスト結果は4件すべて成功です。元のassertEqualsが成功したという意味ではありません。修正後のassertArrayEquals(expected, actual)はassertThrowsの外にあり、配列内容が一致すればそのまま通ります。

4件の実行結果と検出した違い

確認した条件比較の結果テストとして確認したこと
別々の{1, 2}assertEqualsはAssertionFailedError。同じ入力のassertArrayEqualsは成功修正前の失敗をassertThrowsで検出し、修正後の比較も成功
{1, 2}と{2, 1}assertArrayEqualsはAssertionFailedError順序違いを検出したのでテスト成功
{1, 2}と{1, 2, 3}assertArrayEqualsはAssertionFailedError長さ違いを検出したのでテスト成功
別々の{{1, 2}, {3, 4}}と、内側の要素が違う配列同じ内容ならArrays.equalsはfalse、Arrays.deepEqualsはtrue、JUnitのassertArrayEqualsは成功内側の4を5にした比較ではAssertionFailedErrorも検出

実行結果はTests run: 4, Failures: 0, Errors: 0, Skipped: 0でした。順序・長さ・内側の要素が違うケースは、誤った配列を正しいと認めるテストではなく、「比較が不一致を検出すること」を確認するテストです。この4件は整数配列を対象にしており、小数の許容誤差や独自クラスのequalsの契約全体を検証するものではありません。

int[][]の外側はObject[]として扱えるため、JUnitではassertArrayEquals(Object[], Object[])が選ばれ、内側の配列も比較されます。JUnit 5.12.2のassertArrayEqualsのAPI仕様と、assertThrowsのAPI仕様で確認できます。Java側の多次元比較は、Arrays.deepEqualsの仕様も参照してください。

CSV分割の結果も配列専用のアサーションで比べる

@Test
void カンマ区切りの文字列を順番どおり分割する() {
    String[] actual = "A,B,C".split(",", -1);

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

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

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

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

import java.util.Arrays;
import java.util.Objects;

final class FileSignature {
    private final byte[] value;

    FileSignature(byte[] value) {
        this.value = Objects.requireNonNull(value, "value").clone();
    }

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

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

この例はコンストラクタでnullを拒否し、clone()で配列をコピーしています。呼び出し元が元配列を書き換えても、Set登録後のequals・hashCodeが変わりません。getterを追加する場合も内部配列をそのまま返さず、コピーを返します。instanceof FileSignature otherの構文を使うため、このコードはJava 17を前提にしています。

関連記事JavaのequalsとhashCode入門 FileSignatureのような値オブジェクトで、equalsとhashCodeをそろえる理由を基本から確認できます。配列の内容比較をクラス全体の契約へ広げるときに役立ちます。 記事を読む

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

同じ内容、異なる順序、長さ違い、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のStringBuilderでequalsがfalseになる理由|文字列の内容を比較する方法 配列以外でも、equalsが内容比較にならない型があります。StringBuilderの文字列を比べる方法と、比較方法を選ぶ基準を確認できます。 記事を読む 関連記事JavaのList.containsがfalseになる原因|equalsを実装しても見つからない時の確認点 配列比較を直した後に、Listの検索で同じ値が見つからない場合の確認先です。containsと自作クラスのequalsの関係を実例で整理します。 記事を読む

仕様の確認:Java 17 API — Arrays(equals・deepEquals・hashCode)。

まとめ

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

JUnitで同じ要素なのに失敗する場合は、assertEqualsからassertArrayEqualsへ変更し、同じ入力で確認します。順序・長さ・多次元の違いもテストで固定してください。配列を値オブジェクトへ持たせる場合は、equalsとhashCode、防御的コピーまで一緒に設計します。

-実装・エラー解決
-, ,