Javaで文字列の中身が同じかどうかを比べるときは、== ではなく equals メソッドを使います。== は「同じオブジェクト(メモリ上の同じ実体)かどうか」を比べるため、中身が同じ文字列でも false になることがあるからです。

やっかいなのは、== で比べてもテストではたまたま正しく動いてしまうことがある点です。この記事では、== が失敗する理由と、たまたま動いてしまう仕組み(文字列プール)、nullでも安全な比べ方、大文字・小文字を無視する比較や並べ替えの比較、そして全角・半角や空白の違いまで、Javaで実際に実行した結果とあわせて解説します。

まずは結論:用途別の比べ方

やりたいこと 書き方
中身が同じか a.equals(b)
中身が同じか(aがnullの可能性あり) Objects.equals(a, b)
固定の文字列と同じか "yes".equals(input)
大文字・小文字を無視して同じか a.equalsIgnoreCase(b)
辞書順でどちらが前か(並べ替え) a.compareTo(b)
空文字か s.isEmpty()
空文字または空白だけか s.isBlank()(Java 11以降)
StringBuilderなどと中身を比べる s.contentEquals(sb)

==とequalsの違い

String a = "hello";
String c = new String("hello");

System.out.println(a == c);        // false
System.out.println(a.equals(c));   // true
== equals
比べるもの 同じオブジェクトか(参照) 中身の文字の並びが同じか(値)
たとえ 同じ本(1冊の実物)か 同じ内容の本か
文字列の比較での使い方 使わない 使う

変数 a と c は、どちらも「hello」という中身を持っていますが、メモリ上では別々のオブジェクトです。== は、この「どのオブジェクトを指しているか」を比べるため、false になります。

int や double のような基本型では、== で値を比べます。String は参照型(オブジェクト)なので、== では値ではなく参照が比べられる、という違いがあります。

==でも「たまたま動く」理由:文字列プール

String a = "hello";
String b = "hello";

System.out.println(a == b);   // true  ← == なのに true になる

ソースコードに直接書いた文字列(リテラル)は、Javaが「文字列プール」という場所で1つだけ持ち、使い回します。そのため、a と b は同じオブジェクトを指し、== でも true になります。

これが、「テストでは動いたのに、本番で動かない」原因になります。

String h = "hel";
String e = h + "lo";                  // 実行時に連結して作った文字列
System.out.println("hello" == e);     // false

String input = new Scanner("hello").next();   // 入力から読み込んだ文字列
System.out.println(input == "hello");         // false
System.out.println(input.equals("hello"));    // true
文字列の作られ方 == の結果("hello"と比較)
リテラル "hello" true
定数どうしの連結 final String h = "hel"; h + "lo" true(コンパイル時に1つの文字列になる)
変数を使った連結 h + "lo" false
new String("hello") false
画面の入力、ファイル、データベース、APIから読み込んだ文字列 false(多くの場合)
new String("hello").intern() true(プールの文字列を返す)

実務で比べる文字列の多くは、入力やデータベースから読み込んだものです。テストでリテラル同士を比べて動いたからといって、== を使うと、本番のデータでは失敗します。文字列の中身の比較には、常にequalsを使うと覚えておくのが確実です。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Javaの文字列比較|equalsと==の違いと正しい比べ方」

nullでも安全な比べ方

equals は、呼び出す側の変数がnullだと、NullPointerExceptionになります。

String input = null;
input.equals("yes");        // NullPointerException

方法1:固定の文字列を左に置く

"yes".equals(input);        // false(例外にならない)

リテラルはnullにならないので、比べる相手が固定の文字列なら、左側に置くのが定番の書き方です。

方法2:Objects.equals を使う

import java.util.Objects;

Objects.equals(a, b);       // 片方または両方がnullでも例外にならない
Objects.equals(null, null); // true

どちらの変数もnullになり得る場合は、Objects.equals を使います(Java 7以降)。

大文字・小文字を無視して比べる

"Hello".equals("hELLO");            // false
"Hello".equalsIgnoreCase("hELLO");  // true

メールアドレスのドメイン部分や、コマンドの入力(yes・YES・Yes)の判定などで使います。

並べ替えの比較:compareTo

compareTo は、2つの文字列の辞書順の前後関係を、数値で返します。

"apple".compareTo("banana");   // -1(負の数:appleの方が前)
"b".compareTo("a");            // 1 (正の数:bの方が後)
"a".compareTo("a");            // 0 (同じ)
"Apple".compareTo("apple");    // -32(大文字の方が前)
"Apple".compareToIgnoreCase("apple");   // 0
戻り値 意味
負の数 呼び出した側の文字列が前
0 同じ
正の数 呼び出した側の文字列が後

戻り値の具体的な数値(-1や-32)には意味がないので、< 0・== 0・> 0 で判定します。compareTo は、文字のコード(Unicodeの番号)の順で比べるため、日本語の読みの順(あいうえお順)にはなりません。日本語を読みの順に並べたい場合は、読み仮名の列で並べ替えるか、java.text.Collator を使います。

空文字・空白の判定

"".isEmpty();       // true
" ".isEmpty();      // false(半角スペースがある)
" ".isBlank();      // true (空白だけ。Java 11以降)
" ".isBlank();     // true (全角スペースも空白として扱われる)
メソッド true になる条件 備考
isEmpty() 長さが0 nullには使えない
isBlank() 長さが0、または空白文字だけ Java 11以降。全角スペースも空白に含む

入力欄の「未入力チェック」には、スペースだけの入力も未入力として扱える isBlank() が便利です。nullの可能性もある場合は、s == null || s.isBlank() のように先にnullを判定します。

見た目が同じなのに一致しない文字列

equals が false を返すのに、画面上では同じに見える――という場合は、次の違いを疑います。

前後の空白

"hello ".equals("hello");          // false(末尾に空白)
"hello ".strip().equals("hello");  // true

trim() は半角スペースなどの制御文字しか取り除きませんが、strip()(Java 11以降)は全角スペースも取り除きます。日本語の入力を扱う場合は strip() が安全です。

全角と半角

"ABC".equals("ABC");   // false
Normalizer.normalize("ABC", Normalizer.Form.NFKC).equals("ABC");   // true

全角英数字と半角英数字は、別の文字です。java.text.Normalizer でNFKC正規化すると、全角英数字を半角に、半角カタカナを全角に揃えられます。

濁点の表し方

"が".equals("か\u3099");   // false(「か」+結合用の濁点)
Normalizer.normalize("か\u3099", Normalizer.Form.NFC).equals("が");   // true

「が」は、1文字の「が」と、「か」+濁点の2文字の2通りで表せます。Macで作られたファイル名などに後者が混ざることがあり、正規化(NFC)で揃えてから比べます。

switch文での文字列の比較

switch (command) {
    case "start" -> start();
    case "stop"  -> stop();
    default      -> help();
}

Java 7以降、switch文で文字列を使えます。内部では equals で比較されるので、== の問題は起きません。ただし、command がnullの場合はNullPointerExceptionになるため、事前にnullを判定します(Java 21以降は case null も書けます)。

文字列以外でも起きる「==の落とし穴」

同じ問題は、Integer などのラッパークラスでも起きます。

Integer x = 127, y = 127;
System.out.println(x == y);   // true

Integer p = 128, q = 128;
System.out.println(p == q);   // false

Integer は、-128〜127の範囲の値をキャッシュして使い回すため、その範囲だけ == でも true になります。文字列と同じく、オブジェクトの値の比較には equals を使います。

自分で作ったクラスのequals

自分で作ったクラスで equals を使って比べたい場合は、equals と hashCode の両方をオーバーライドする必要があります。オーバーライドしないと、equals は == と同じ動き(同じオブジェクトかどうか)になります。

Java 16以降の record は、equals と hashCode が自動で定義されるため、値を比べるだけのクラスには便利です。

record Point(int x, int y) {}

new Point(1, 2).equals(new Point(1, 2));   // true

よくある質問

Q. == で比べても動いているのですが、直すべきですか? A. 直すべきです。リテラル同士など、たまたま同じオブジェクトになる条件で動いているだけで、入力やデータベースの値と比べると失敗します。レビューや静的解析ツールでも指摘される典型的な問題です。

Q. equalsとcompareTo() == 0 は同じですか? A. Stringでは結果が一致します。ただし、目的が「同じかどうか」ならequals、「並べ替えの順番」ならcompareToを使うのが、読み手に意図が伝わる書き方です。

Q. 文字列が「含まれているか」「始まっているか」を調べるには? A. contains(含む)、startsWith(〜で始まる)、endsWith(〜で終わる)を使います。より複雑な条件は matches で正規表現を使います。

Q. KotlinやC#でも同じ注意が必要ですか? A. Kotlinでは == が内部でequalsを呼ぶため、中身の比較になります(参照の比較は ===)。C#でも、stringの == は中身を比較します。Javaの == の動きは、他の言語から来た人が特に間違えやすい点です。

まとめ

  • 文字列の中身の比較は equals。== は同じオブジェクトかを比べる
  • == でも動くのは、文字列プールでリテラルが使い回されるから。入力やDBの値では失敗する
  • nullの可能性があるなら "固定値".equals(変数) か Objects.equals(a, b)
  • 大文字・小文字の無視は equalsIgnoreCase、並べ替えは compareTo(結果は正負で判定)
  • 未入力チェックは isBlank()、前後の空白の除去は全角にも対応した strip()
  • 全角・半角や濁点の違いは、Normalizer で正規化してから比べる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Javaの文字列比較|equalsと==の違いと正しい比べ方」

株式会社RJCでは、Javaの基本を「動いた」で終わらせず、仕組みから理解することを大切にしています。こうした知識を仲間と共有しながら、一緒に成長を楽しんでいます。

株式会社RJC ― SI事業・SES事業・AI駆動開発。未経験からエンジニアに育てるのが得意な会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?