Các toán tử so sánh trong Java được giải thích: ==, !=, <, > và sự khác biệt của equals()

目次

1. Những Điều Bạn Sẽ Học Trong Bài Viết Này (Tóm Tắt Chính)

Trong Java, toán tử so sánh là các tính năng ngôn ngữ cơ bản dùng để so sánh các giá trị như số và ký tự.
Tuy nhiên, nhiều người mới bắt đầu gặp khó khăn khi so sánh các đối tượng như String hoặc Integer, đặc biệt là khi sử dụng sai toán tử ==.

Phần này tóm tắt các điểm chính ngay từ đầu, để bạn nhanh chóng hiểu khi nào việc dùng toán tử so sánh là an toàn—và khi nào không.

1.1 Toán Tử So Sánh Chia Thành Hai Loại

Các toán tử so sánh trong Java có thể được nhóm thành hai loại chính:

  • Toán tử quan hệ (so sánh thứ tự) <, <=, >, >=
  • Toán tử bằng (so sánh giá trị) ==, !=

Khi làm việc với các kiểu nguyên thủy (như int, double, hoặc char), các toán tử này hoạt động chính xác như bạn mong đợi.

int a = 10;
int b = 20;

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

1.2 Quan Trọng: == KHÔNG Phải Luôn So Sánh Giá Trị

Đây là nguồn gây nhầm lẫn phổ biến nhất trong Java.

  • Đối với các kiểu nguyên thủy, == so sánh giá trị thực tế
  • Đối với các kiểu tham chiếu (đối tượng), == so sánh liệu hai biến có trỏ tới cùng một đối tượng hay không

Sự khác biệt này rất quan trọng.

String s1 = new String("Java");
String s2 = new String("Java");

System.out.println(s1 == s2);      // false
System.out.println(s1.equals(s2)); // true

Mặc dù văn bản trông giống hệt nhau, s1s2hai đối tượng khác nhau trong bộ nhớ.

👉 Nguyên tắc chung:
Nếu bạn muốn kiểm tra hai đối tượng có cùng nội dung, hãy dùng equals().

1.3 Hướng Dẫn Quyết Định Nhanh (Cheat Sheet)

Nếu bạn chỉ nhớ một điều, hãy nhớ điều này:

  • Giá trị nguyên thủy (int, boolean, char, v.v.) → Dùng các toán tử so sánh (==, <, >, v.v.)
  • So sánh nội dung đối tượng (String, Integer, đối tượng tùy chỉnh) → Dùng equals() hoặc Objects.equals()
  • Thứ tự hoặc sắp xếp (cái nào lớn/hơn) → Dùng compareTo() hoặc một Comparator

Insight chính:
== có vẻ đơn giản, nhưng đối với đối tượng nó trả lời
“Hai biến này có cùng một đối tượng không?”, chứ không phải
“Hai biến này có cùng giá trị không?”

1.4 Những Điều Bạn Sẽ Có Thể Làm Sau Khi Đọc Bài Viết Này

Khi kết thúc bài viết, bạn sẽ có thể:

  • Sử dụng các toán tử so sánh trong Java đúng cách và an toàn
  • Tránh các lỗi phổ biến do nhầm lẫn ==equals()
  • Hiểu vì sao so sánh Integer đôi khi lại có hành vi không nhất quán
  • Chọn cách tiếp cận phù hợp khi so sánh giá trị, đối tượng, hoặc thứ tự

Trong phần tiếp theo, chúng ta sẽ bắt đầu với một cái nhìn tổng quan đầy đủ về tất cả các toán tử so sánh trong Java, trước khi đi sâu vào các bẫy thường gặp và thực tiễn tốt nhất.

2. Các Toán Tử So Sánh trong Java (Danh Sách Đầy Đủ)

Trong phần này, chúng ta sẽ sắp xếp tất cả các toán tử so sánh trong Java và làm rõ chúng có thể và không thể so sánh gì.
Mục tiêu là loại bỏ mọi mơ hồ trước khi đi vào các trường hợp phức tạp hơn.

2.1 Toán Tử So Sánh Luôn Trả Về Kiểu Boolean

Mỗi toán tử so sánh trong Java trả về một giá trị kiểu boolean:

  • true → điều kiện được thỏa mãn
  • false → điều kiện không được thỏa mãn

Đó là lý do các toán tử so sánh thường được dùng trong if, while, và các câu lệnh điều khiển khác.

int x = 5;
int y = 10;

boolean result = x < y;  // true

Các toán tử so sánh không thay đổi giá trị—chúng chỉ đánh giá các điều kiện.

2.2 Toán Tử Quan Hệ: <, <=, >, >=

Các toán tử này so sánh thứ tự hoặc độ lớn.
Chúng chủ yếu được dùng với các kiểu số và char.

OperatorMeaning
<less than
<=less than or equal to
>greater than
>=greater than or equal to

Ví dụ với số

int a = 10;
int b = 20;

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

Ví dụ với char

Các ký tự được so sánh dựa trên giá trị Unicode của chúng.

char c1 = 'A';
char c2 = 'B';

System.out.println(c1 < c2); // true

Lưu ý:
Các toán tử này không thể được sử dụng với String. Thực hiện như vậy sẽ gây ra lỗi biên dịch.

2.3 Toán tử bằng nhau: ==!=

Các toán tử bằng nhau kiểm tra xem hai toán hạng có bằng nhau hay không.

OperatorMeaning
==equal to
!=not equal to

Sử dụng an toàn với các kiểu nguyên thủy

int x = 5;
int y = 5;

System.out.println(x == y); // true
System.out.println(x != y); // false

Ở đây, Java so sánh giá trị thực tế, điều này đơn giản và an toàn.

2.4 So sánh các giá trị boolean

Các giá trị boolean cũng có thể được so sánh bằng ==!=.

boolean f1 = true;
boolean f2 = false;

System.out.println(f1 == f2); // false

Trong mã thực tế, tuy nhiên, việc viết sẽ dễ đọc hơn:

if (isEnabled) {
    // do something
}

thay vì:

if (isEnabled == true) { ... }

2.5 Các kiểu dữ liệu hoạt động tốt với các toán tử so sánh

An toàn khi sử dụng trực tiếp các toán tử so sánh:

  • int , long , double , float
  • char
  • boolean (chỉ == / !=)

Không an toàn hoặc không được phép:

  • String
  • Các lớp bao bọc ( Integer , Long , v.v.)
  • Đối tượng tùy chỉnh

Các kiểu này yêu cầu các kỹ thuật so sánh khác nhau, chúng ta sẽ đề cập tới chúng tiếp theo.

2.6 Điểm chính cần nhớ từ phần này

  • Các toán tử so sánh luôn trả về true hoặc false
  • Chúng hoạt động đáng tin cậy với các kiểu nguyên thủy
  • Sử dụng chúng với các đối tượng có thể dẫn đến lỗi hoặc lỗi biên dịch

Trong phần tiếp theo, chúng ta sẽ tập trung vào các kiểu nguyên thủy, nơi các toán tử so sánh hoạt động đúng như mong đợi.

3. Toán tử so sánh với các kiểu nguyên thủy (Vùng an toàn)

Các kiểu nguyên thủy là trường hợp an toàn và đơn giản nhất cho các toán tử so sánh.
Hiểu rõ phần này sẽ giúp bạn nhận ra khi nào mọi thứ bắt đầu trở nên phức tạp.

3.1 Kiểu nguyên thủy là gì?

Các kiểu nguyên thủy lưu trữ giá trị thực tế, không phải tham chiếu.

Các ví dụ phổ biến bao gồm:

  • Kiểu số: int , long , double , float
  • Kiểu ký tự: char
  • Kiểu boolean: boolean

Vì không có tham chiếu đối tượng nào, các phép so sánh hoạt động dự đoán được.

3.2 So sánh các giá trị intlong

int a = 100;
int b = 100;

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

Java so sánh các giá trị số một cách trực tiếp.

Các kiểu số hỗn hợp

int x = 10;
long y = 10L;

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

Java thực hiện tự động nâng kiểu trước khi so sánh.

3.3 So sánh ký tự (char)

Mặc dù char đại diện cho một ký tự, Java xử lý nó như một số bên trong.

char c1 = 'A';
char c2 = 'a';

System.out.println(c1 < c2); // true

So sánh này dựa trên giá trị Unicode, không phải quy tắc chữ cái trong ngôn ngữ con người.

3.4 So sánh các giá trị Boolean

boolean flag1 = true;
boolean flag2 = false;

System.out.println(flag1 != flag2); // true

Trong thực tế, tránh các so sánh thừa:

if (isLoggedIn) { ... }      // preferred
if (isLoggedIn == true) { } // unnecessary

3.5 Cạm bẫy so sánh số thực (double / float)

Đây là một cạm bẫy kinh điển trong Java.

double d1 = 0.1 + 0.2;
double d2 = 0.3;

System.out.println(d1 == d2); // may be false

Các số thực được lưu trữ với giới hạn độ chính xác.

Phương pháp đề xuất: sử dụng độ sai (epsilon)

double epsilon = 0.000001;

if (Math.abs(d1 - d2) < epsilon) {
    // treat as equal
}

Đối với các tính toán tài chính hoặc độ chính xác cao, hãy cân nhắc sử dụng BigDecimal.

3.6 Tóm tắt Vùng an toàn

  • Các kiểu nguyên thủy có thể được so sánh trực tiếp
  • So sánh char sử dụng giá trị Unicode
  • So sánh bằng nhau của số thực cần chú ý đặc biệt
  • Cho đến thời điểm này, các toán tử so sánh hoạt động một cách trực quan

Tiếp theo, chúng ta sẽ chuyển sang vùng nguy hiểm:
tại sao việc sử dụng == với các đối tượng lại dẫn đến kết quả không mong đợi.

4. Tại sao việc sử dụng == với các đối tượng gây ra vấn đề

Đây là nơi mà nhiều người mới học Java — và thậm chí các nhà phát triển trung cấp — gặp rắc rối.
Hành vi của == thay đổi khi bạn bắt đầu làm việc với kiểu tham chiếu (đối tượng).

4.1 == So sánh Tham chiếu Đối tượng, Không phải Nội dung

Đối với các đối tượng, toán tử == kiểm tra xem cả hai biến có trỏ tới cùng một đối tượng trong bộ nhớ hay không.

String s1 = new String("Java");
String s2 = new String("Java");

System.out.println(s1 == s2); // false

Mặc dù cả hai chuỗi trông giống nhau, chúng là các đối tượng khác nhau, vì vậy phép so sánh sẽ thất bại.

4.2 equals() So sánh Nội dung Đối tượng

Phương thức equals() được thiết kế để so sánh sự bằng nhau logic, nghĩa là nội dung thực tế của các đối tượng.

System.out.println(s1.equals(s2)); // true

Lớp String ghi đè equals() để nó so sánh chuỗi ký tự, không phải địa chỉ bộ nhớ.

Quy tắc vàng:

  • Cùng một đối tượng? → ==
  • Cùng giá trị/nội dung? → equals()

4.3 Tại sao == Đôi khi Hoạt động với Các Literal Chuỗi

Ví dụ này làm nhiều nhà phát triển bối rối:

String a = "Java";
String b = "Java";

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

Điều này xảy ra do String Pool.

  • Các literal chuỗi được lưu trong một pool chung
  • Các literal giống nhau có thể tham chiếu tới cùng một đối tượng

Tuy nhiên, hành vi này là một chi tiết triển khai, không phải thứ bạn nên dựa vào.

String x = "Java";
String y = new String("Java");

System.out.println(x == y);      // false
System.out.println(x.equals(y)); // true

👉 Luôn sử dụng equals() để so sánh nội dung String.

4.4 Null và equals() — Một Cạm Bẫy Thường Gặp Khác

Gọi equals() trên một tham chiếu null sẽ gây lỗi thời gian chạy.

String str = null;
str.equals("Java"); // NullPointerException

Mẫu an toàn 1: Gọi equals() trên hằng số

if ("Java".equals(str)) {
    // safe
}

Mẫu an toàn 2: Sử dụng Objects.equals()

if (Objects.equals(str, "Java")) {
    // safe and clean
}

5. Tóm tắt Phần Này

  • == so sánh tham chiếu đối tượng
  • equals() so sánh nội dung
  • Hành vi String Pool có thể ẩn lỗi
  • Luôn cân nhắc an toàn null

Tiếp theo, chúng ta sẽ xem một cạm bẫy tinh tế khác:
so sánh các lớp wrapper như IntegerLong.

5. So sánh Các Lớp Wrapper (Integer, Long, v.v.)

Các lớp wrapper trông giống số, nhưng chúng vẫn là đối tượng.

5.1 Các Lớp Wrapper là gì?

Các lớp wrapper cho phép các giá trị nguyên thủy được xử lý như đối tượng.

PrimitiveWrapper
intInteger
longLong
doubleDouble
booleanBoolean

5.2 Tại sao == Tạo ra Kết quả Không nhất quán

Integer a = 100;
Integer b = 100;

System.out.println(a == b); // true
Integer x = 1000;
Integer y = 1000;

System.out.println(x == y); // false

Điều này xảy ra do caching Integer (thường từ -128 đến 127).

Kết quả của == phụ thuộc vào hành vi nội bộ JVM, không phải sự bằng nhau về giá trị.

5.3 Cách Đúng Để So sánh Giá trị Wrapper

Sử dụng equals() để so sánh giá trị.

System.out.println(x.equals(y)); // true

5.4 Các Cạm Bẫy Autoboxing và Unboxing

Integer a = 100;
int b = 100;

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

Điều này hoạt động do unboxing tự động, nhưng:

  • Nếu a là null → NullPointerException
  • Ý định của mã trở nên không rõ ràng

So sánh một cách rõ ràng sẽ an toàn hơn.

5.5 Các Mẫu So sánh Được Đề xuất

  • So sánh giá trị → equals() / Objects.equals()
  • So sánh an toàn null → Objects.equals()
  • So sánh tham chiếu → == (hiếm và có mục đích)

5.6 Tóm tắt Phần

  • Các lớp wrapper là kiểu tham chiếu
  • == không đáng tin cậy cho so sánh giá trị
  • Sử dụng equals() một cách nhất quán

Tiếp theo, chúng ta sẽ tập trung vào các kỹ thuật so sánh an toàn null.

6. Các Kỹ Thuật So sánh An Toàn Null

Các lỗi liên quan đến null là rất phổ biến trong các ứng dụng Java.

6.1 Các Quy tắc Cơ bản với null

  • null == null → true
  • Gọi một phương thức trên null → lỗi thời gian chạy
  • Toán tử quan hệ (<, >) với null → lỗi biên dịch

6.2 Mẫu nguy hiểm

str.equals("Java"); // unsafe

6.3 Mẫu an toàn #1: Hằng số trước

"Java".equals(str);

6.4 Mẫu an toàn #2: Objects.equals()

Objects.equals(str, "Java");

Điều này xử lý tất cả các trường hợp null nội bộ.

6.5 Khi nào nên sử dụng Objects.equals()

  • So sánh các biến
  • Giá trị có thể null
  • Logic điều kiện sạch hơn

6.6 Mẹo thiết kế: Giảm việc sử dụng null

  • Khởi tạo giá trị sớm
  • Sử dụng Optional khi thích hợp
  • Ít null hơn → so sánh đơn giản hơn

6.7 Tóm tắt phần

  • Không bao giờ gọi phương thức trên tham chiếu có thể null
  • Ưu tiên các tiện ích so sánh an toàn với null
  • Thiết kế để giảm thiểu việc sử dụng null

Tiếp theo, chúng ta sẽ đề cập đến so sánh thứ tự bằng compareTo().

7. So sánh thứ tự với compareTo()

Các toán tử so sánh không thể xác định thứ tự cho các đối tượng.

7.1 compareTo() là gì?

compareTo() so sánh thứ tự và trả về:

  • Âm → nhỏ hơn
  • Zero → bằng
  • Dương → lớn hơn

7.2 Ví dụ sắp xếp chuỗi

String a = "Apple";
String b = "Banana";

if (a.compareTo(b) < 0) {
    System.out.println("Apple comes first");
}

7.3 Các lớp Wrapper cũng hỗ trợ compareTo()

Integer x = 10;
Integer y = 20;

System.out.println(x.compareTo(y)); // negative

7.4 equals() vs compareTo()

  • Kiểm tra bằng nhau → equals()
  • Thứ tự/sắp xếp → compareTo()

7.5 Liên kết với việc sắp xếp

Các phương thức như Collections.sort() dựa vào compareTo() bên trong.

7.6 Tóm tắt phần

  • Các toán tử so sánh không thể so sánh thứ tự của đối tượng
  • compareTo() là công cụ đúng
  • Cần thiết cho việc sắp xếp và các collection có thứ tự

8. Những sai lầm thường gặp (Danh sách nhanh)

8.1 Sử dụng == với Strings

str1 == str2
str1.equals(str2)

8.2 Sử dụng == với các lớp Wrapper

Integer a == b
a.equals(b)

8.3 So sánh giá trị floating-point trực tiếp

a == b
✅ sử dụng độ sai số hoặc BigDecimal

8.4 Quên kiểm tra null

obj.equals(x)
Objects.equals(obj, x)

8.5 Sử dụng < với Objects

str1 < str2
str1.compareTo(str2)

9. Tổng kết cuối cùng: Cách chọn so sánh phù hợp

9.1 Hướng dẫn quyết định

  • Kiểu nguyên thủy → toán tử so sánh
  • Nội dung đối tượngequals() / Objects.equals()
  • Thứ tự và sắp xếpcompareTo() / Comparator

9.2 Thực hành tốt nhất

  • Hiểu == thực sự có nghĩa gì
  • Luôn cân nhắc an toàn null
  • Tránh phụ thuộc vào nội bộ JVM

9.3 Những gì nên học tiếp

  • Toán tử logic (&&, ||)
  • Câu lệnh ifswitch
  • Comparator để sắp xếp tùy chỉnh
  • Cài đặt đúng equals() / hashCode()

Câu hỏi thường gặp

H1. Sự khác biệt giữa ==equals() trong Java là gì?

== so sánh tham chiếu của các đối tượng, trong khi equals() so sánh nội dung.

H2. Tại sao == đôi khi hoạt động với Strings?

Do có String Pool. Hành vi này không nên được dựa vào.

H3. Cách an toàn nhất để so sánh các giá trị có thể null là gì?

Sử dụng Objects.equals(a, b).

H4. Làm thế nào để so sánh Strings theo thứ tự chữ cái?

Sử dụng compareTo().

H5. Các toán tử so sánh có đủ trong Java không?

Chúng đủ cho các kiểu nguyên thủy, nhưng các đối tượng cần equals()compareTo().