Giải thích toàn diện về so sánh chuỗi trong Java | Sự khác nhau và cách dùng ==, equals, compareTo

目次

1. Giới thiệu

Tầm quan trọng của việc so sánh chuỗi trong Java là gì?

Trong lập trình Java, việc xử lý chuỗi (String) xuất hiện rất thường xuyên. Kiểm tra tên người dùng, so sánh giá trị nhập từ biểu mẫu, xác nhận phản hồi API, và trong mọi tình huống đều yêu cầu so sánh chuỗi.

Lúc này, “cách so sánh chuỗi một cách chính xác” là điểm mà người mới học thường gặp khó khăn. Đặc biệt, nếu không hiểu sự khác nhau giữa toán tử == và phương thức equals(), sẽ là nguyên nhân gây ra lỗi (bug) dẫn đến kết quả không mong muốn.

Nguy hiểm khi không hiểu sự khác nhau giữa “==” và “equals”

Ví dụ, hãy xem đoạn mã sau.

String a = "apple";
String b = new String("apple");

System.out.println(a == b);       // 結果: false
System.out.println(a.equals(b));  // 結果: true

Nhiều người sẽ ngạc nhiên khi nhìn vào kết quả đầu ra của đoạn mã này. Mặc dù là cùng một chuỗi, nhưng == trả về false, trong khi equals() trả về true. Điều này là do Java xử lý “chuỗi như một kiểu tham chiếu”, và == so sánh địa chỉ tham chiếu.

Như vậy, việc so sánh chuỗi một cách chính xác liên quan trực tiếp đến độ tin cậy và khả năng đọc của chương trình. Ngược lại, nếu hiểu đúng phương pháp, bạn có thể ngăn ngừa lỗi từ đầu.

Những gì bạn sẽ học được trong bài viết này

Trong bài viết này, chúng tôi sẽ giải thích chi tiết về các phương pháp so sánh chuỗi trong Java, từ cơ bản đến nâng cao. Chúng tôi sẽ trả lời các câu hỏi sau, giải thích một cách có cấu trúc và dễ hiểu cho người mới bắt đầu.

  • Sự khác nhau giữa ==equals() là gì?
  • Làm thế nào để so sánh mà không phân biệt chữ hoa/chữ thường?
  • Làm sao để so sánh chuỗi theo thứ tự từ điển?
  • Làm thế nào để tránh ngoại lệ khi so sánh với null?

Kèm theo các ví dụ mã hữu ích trong thực tế, hãy nắm vững kiến thức so sánh chuỗi đúng.

2. Cơ bản về chuỗi trong Java

Chuỗi là “kiểu tham chiếu”

Trong Java, kiểu String không phải là kiểu nguyên thủ (int, boolean, …) mà là “kiểu tham chiếu (Reference Type)”. Điều này có nghĩa là biến String không chứa dữ liệu chuỗi thực tế mà tham chiếu tới đối tượng chuỗi tồn tại trong bộ nhớ heap.

Nói cách khác, nếu viết như sau:

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

ab tham chiếu tới cùng một chuỗi "hello" nên a == b có thể trả về true. Tuy nhiên, điều này là do cơ chế tối ưu hoá literal chuỗi (String Interning) của Java.

Sự khác nhau giữa literal chuỗi và new String()

Trong Java, khi sử dụng cùng một literal chuỗi nhiều lần, chúng sẽ được tối ưu hoá thành cùng một tham chiếu. Đây là tính năng cho phép Java chia sẻ chuỗi tại thời gian chạy để tăng hiệu quả bộ nhớ.

String s1 = "apple";
String s2 = "apple";
System.out.println(s1 == s2); // true(同じリテラルなので同一参照)

Ngược lại, khi sử dụng từ khóa new để tạo đối tượng một cách rõ ràng, một tham chiếu mới sẽ được tạo.

String s3 = new String("apple");
System.out.println(s1 == s3); // false(異なる参照)
System.out.println(s1.equals(s3)); // true(内容は同じ)

Như vậy, == kiểm tra sự trùng khớp của tham chiếu, trong khi equals() xác nhận sự trùng khớp của nội dung, do đó mục đích sử dụng rất khác nhau.

String là lớp “bất biến (immutable)”

Một đặc điểm quan trọng khác là Stringbất biến (immutable). Nghĩa là, nội dung của đối tượng String một khi được tạo ra không thể thay đổi.

Ví dụ, nếu viết như sau:

String original = "hello";
original = original + " world";

Điều này trông như đang thêm chuỗi vào original, nhưng thực tế một đối tượng String mới được tạo ra và được gán lại cho original.

Nhờ tính bất biến này, String an toàn với đa luồng và hữu ích cho bảo mật cũng như tối ưu hoá bộ nhớ đệm.

3. Các phương pháp so sánh chuỗi

So sánh tham chiếu bằng toán tử ==

== so sánh tham chiếu (địa chỉ) của đối tượng chuỗi. Nghĩa là, ngay cả khi nội dung giống nhau, nếu là các đối tượng khác nhau thì sẽ trả về false.

String a = "Java";
String b = new String("Java");

System.out.println(a == b);        // false

Trong ví dụ này, a là literal, b được tạo bằng new, vì tham chiếu khác nhau nên kết quả là false. Cần lưu ý không dùng để so sánh nội dung.

So sánh nội dung bằng phương thức equals()

equals()phương pháp đúng để so sánh nội dung chuỗi. Được khuyến nghị sử dụng phương thức này trong hầu hết các trường hợp.

String a = "Java";
String b = new String("Java");

System.out.println(a.equals(b));   // true

Như vậy, ngay cả khi tham chiếu khác nhau, nếu nội dung giống nhau thì sẽ trả về true.

Lưu ý khi so sánh với null

Đoạn mã sau có thể gây ra NullPointerException.

String input = null;
System.out.println(input.equals("test")); // 例外発生!

Để tránh, nên viết dưới dạng hằng số.equals(biến).

System.out.println("test".equals(input)); // false(安全)

So sánh không phân biệt chữ hoa/chữ thường bằng phương thức equalsIgnoreCase()

Khi muốn so sánh không phân biệt chữ hoa/chữ thường, ví dụ tên người dùng hoặc địa chỉ email, equalsIgnoreCase() rất tiện dụng.

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

System.out.println(a.equalsIgnoreCase(b)); // true

Tuy nhiên, trong một số trường hợp đặc biệt của Unicode (ví dụ ký tự “İ” trong tiếng Thổ Nhĩ Kỳ) có thể gây hành vi không mong muốn, vì vậy cần cân nhắc thêm khi làm việc đa ngôn ngữ.

So sánh theo thứ tự từ điển bằng phương thức compareTo()

compareTo() so sánh hai chuỗi theo thứ tự từ điển và trả về một số nguyên như sau:

  • 0: bằng nhau
  • Giá trị âm: chuỗi gọi hàm đứng trước (nhỏ hơn)
  • Giá trị dương: chuỗi gọi hàm đứng sau (lớn hơn)
    String a = "apple";
    String b = "banana";
    
    System.out.println(a.compareTo(b)); // 負の値("apple"は"banana"より前)
    

Thường được dùng trong sắp xếp theo thứ tự từ điển hoặc lọc, và cũng được sử dụng nội bộ trong Collections.sort() và so sánh khóa của TreeMap.

4. Ví dụ thực tế

Kiểm tra đầu vào người dùng (chức năng đăng nhập)

Một trong những trường hợp sử dụng phổ biến nhất là kiểm tra tính khớp của tên người dùng và mật khẩu.

String inputUsername = "Naohiro";
String registeredUsername = "naohiro";

if (registeredUsername.equalsIgnoreCase(inputUsername)) {
    System.out.println("ログイン成功");
} else {
    System.out.println("ユーザー名が一致しません");
}

この例のように、大文字・小文字を無視して比較したい場面では equalsIgnoreCase() を使うのが適切です。

ただし、セキュリティ的にパスワード比較では大小文字を区別すべきなので、equals() を使いましょう。

入力バリデーション(フォーム処理)

例えば、ドロップダウンやテキストボックスからの入力値のチェックにも、文字列比較が使われます。

String selectedOption = request.getParameter("plan");

if ("premium".equals(selectedOption)) {
    System.out.println("プレミアムプランを選択しました。");
} else {
    System.out.println("その他のプランです。");
}

このように、nullチェックを兼ねた安全な比較として "定数".equals(変数) の形式が実務でよく使われます。ユーザー入力は必ずしも値が存在するとは限らないため、NullPointerExceptionを防ぐための書き方です。

複数条件の分岐処理(スイッチ的に使う)

複数の文字列候補を条件分岐で扱いたい場合は、equals()を連続して使うのが一般的です。

String cmd = args[0];

if ("start".equals(cmd)) {
    startApp();
} else if ("stop".equals(cmd)) {
    stopApp();
} else {
    System.out.println("コマンドが不正です");
}

Java 14以降では、文字列に対する switch 文も正式に使えるようになっています。

switch (cmd) {
    case "start":
        startApp();
        break;
    case "stop":
        stopApp();
        break;
    default:
        System.out.println("不明なコマンドです");
}

このように、文字列比較はロジックの分岐処理に直結するため、正確な理解が求められます。

nullとの比較で起こるバグとその対策

よくある失敗例として、null値との比較でアプリがクラッシュするケースがあります。

String keyword = null;

if (keyword.equals("検索")) {
    // 例外発生:java.lang.NullPointerException
}

このようなケースでは、次のように書くことで安全に比較できます。

if ("検索".equals(keyword)) {
    System.out.println("検索実行");
}

または、より厳密なnullチェックを先に行う方法もあります。

if (keyword != null && keyword.equals("検索")) {
    System.out.println("検索実行");
}

null安全なコードは堅牢性を高めるうえで必須のスキルです。

5. パフォーマンスと最適化

文字列比較における処理コスト

equals()compareTo() は、一般に高速に動作するよう最適化されていますが、内部では1文字ずつ比較しているため、長い文字列や大量データを扱うときには影響が出ます。特に、ループの中で何度も同じ文字列と比較していると、意図しないパフォーマンス低下につながることがあります。

for (String item : items) {
    if (item.equals("keyword")) {
        // 比較回数が多い場合、注意
    }
}

String.intern() による比較高速化

JavaのString.intern()メソッドを使うと、同じ内容の文字列をJVMの「文字列プール」に登録し、参照を共有することができます。これを利用すると、==での比較も可能になるため、性能上のメリットがある場合があります。

String a = new String("hello").intern();
String b = "hello";

System.out.println(a == b); // true

ただし、文字列プールを乱用するとヒープ領域が圧迫される可能性があるため、限られた用途での使用に留めるべきです。

equalsIgnoreCase() の落とし穴と代替案

equalsIgnoreCase() は便利ですが、比較時に大文字・小文字を変換する処理が発生するため、通常のequals()よりも若干コストが高くなることがあります。性能がシビアな場面では、すでに大文字や小文字に統一した値で比較する方が高速です。

String input = userInput.toLowerCase();
if ("admin".equals(input)) {
    // 高速化された比較
}

このように事前に変換してからequals()を使うと、比較処理の効率を高められます。

StringBuilder / StringBuffer の活用

大量の文字列連結が発生するようなケースでは、String を使っていると毎回新しいオブジェクトが生成され、メモリやCPUへの負荷が高くなります。比較処理が混在する場合も含め、連結や構築には StringBuilder を使い、比較には String のまま保持するのがベストプラクティスです。

StringBuilder sb = new StringBuilder();
sb.append("user_");
sb.append("123");

String result = sb.toString();

if (result.equals("user_123")) {
    // 比較処理
}

キャッシュと事前処理で高速化する設計

同じ文字列との比較が何度も行われるような場合は、一度比較結果をキャッシュする、あるいはマップ(HashMapなど)を使って前処理することで、比較処理自体を減らすという手法も有効です。

Map<String, Runnable> commandMap = new HashMap<>();
commandMap.put("start", () -> startApp());
commandMap.put("stop", () -> stopApp());

Runnable action = commandMap.get(inputCommand);
if (action != null) {
    action.run();
}

このようにすれば、equals()による文字列比較は1回のMap検索に置き換わり、可読性とパフォーマンスの両方を向上させることができます。

6. よくある質問(FAQ)

Q1. ==equals()の違いは何ですか?

A.
==参照の比較(つまりメモリ上のアドレスの一致)を行います。一方でequals()は、文字列の中身(内容)を比較します。

String a = new String("abc");
String b = "abc";

System.out.println(a == b);        // false(参照が異なる)
System.out.println(a.equals(b));   // true(内容は同じ)

したがって、文字列の内容を比較したいときは必ずequals()を使用しましょう。

Q2. equals()を使うとき、nullが原因でエラーになるのはなぜ?

A.
nullに対してメソッドを呼び出すと、NullPointerExceptionが発生するためです。

String input = null;
System.out.println(input.equals("test")); // 例外発生!

このエラーを防ぐには、次のように定数側から比較する書き方が安全です。

System.out.println("test".equals(input)); // false(安全)

Q3. 大文字・小文字を無視して比較するにはどうすればいい?

A.
Phương thức equalsIgnoreCase() có thể được sử dụng để so sánh mà bỏ qua sự khác biệt giữa chữ hoa và chữ thường.

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

System.out.println(a.equalsIgnoreCase(b)); // true

Tuy nhiên, với các ký tự toàn chiều rộng hoặc một số ký tự Unicode đặc biệt, kết quả có thể không như mong đợi, vì vậy cần chú ý.

Q4. Nếu muốn so sánh chuỗi theo thứ tự sắp xếp thì sao?

A.
Khi muốn kiểm tra mối quan hệ trước sau theo thứ tự từ điển của chuỗi, hãy sử dụng compareTo().

String a = "apple";
String b = "banana";

System.out.println(a.compareTo(b)); // 負の値("apple"は"banana"より前)

Ý nghĩa của giá trị trả về:

  • 0 → Bằng nhau
  • Giá trị âm → Bên trái ở trước
  • Giá trị dương → Bên trái ở sau

Nó được sử dụng trong xử lý sắp xếp, v.v.

Q5. Những thực hành tốt nhất cần nhớ khi so sánh chuỗi là gì?

A.

  • Sử dụng equals() cho so sánh nội dung
  • Chú ý an toàn null bằng cách ưu tiên hình thức "hằng số".equals(biến)
  • Để bỏ qua chữ hoa/thường, sử dụng equalsIgnoreCase() hoặc xử lý toLowerCase() / toUpperCase() trước
  • Trong trường hợp so sánh hàng loạt hoặc cần tăng tốc, hãy xem xét intern() hoặc thiết kế cache
  • Luôn chú ý cân bằng giữa tính đọc và an toàn

7. Tóm tắt

So sánh chuỗi trong Java rất quan trọng là “sử dụng đúng cách phân biệt”

Qua bài viết này, chúng ta đã giải thích toàn diện từ cơ bản đến thực hành và khía cạnh hiệu suất của so sánh chuỗi trong Java. Trong Java, vì String là kiểu tham chiếu, nếu sai phương pháp so sánh, sẽ dẫn đến hành vi không mong muốn không ít.

Đặc biệt, sự khác biệt giữa ==equals() là điểm dễ gây nhầm lẫn cho người mới bắt đầu. Việc hiểu đúng và sử dụng phân biệt chúng là không thể thiếu để viết mã an toàn và đáng tin cậy.

Những gì học được từ bài viết này (Danh sách kiểm tra)

  • == là toán tử so sánh tham chiếu (địa chỉ bộ nhớ)
  • equals() là phương thức so sánh nội dung của chuỗi (an toàn nhất)
  • Sử dụng equalsIgnoreCase() để bỏ qua chữ hoa/thường
  • compareTo() cho phép so sánh theo thứ tự từ điển của chuỗi
  • So sánh an toàn null bằng thứ tự "hằng số".equals(biến)
  • Để chú ý hiệu suất, intern() hoặc thiết kế cache cũng hiệu quả

Kiến thức so sánh chuỗi hữu ích trong công việc thực tế

Xác thực đăng nhập, xác thực đầu vào, tìm kiếm cơ sở dữ liệu hoặc phân nhánh điều kiện, v.v., so sánh chuỗi là yếu tố liên quan chặt chẽ đến công việc phát triển hàng ngày. Với kiến thức này, bạn có thể ngăn chặn lỗi và thực hiện mã hoạt động theo ý định.

Trong tương lai, khi viết xử lý chuỗi, hãy tham khảo nội dung bài viết này và chọn phương pháp so sánh phù hợp với mục đích.