Toán tử số học và so sánh — phép chia nguyên, modulo âm và bẫy ==
Arithmetic, comparison và assignment trong Java: vì sao 5/2 ra 2 chứ không phải 2.5, modulo âm giữ dấu dividend, compound assignment ngầm narrowing cast, và vì sao không bao giờ so sánh String/Integer bằng ==.
TL;DR: Toán tử số học, so sánh và gán là nhóm bạn gõ nhiều nhất mỗi ngày — và cũng là nhóm chứa nhiều bẫy im lặng nhất. / giữa hai số nguyên là integer division: truncate về phía 0, không làm tròn. % với số âm giữ dấu của số bị chia — khác modulo toán học. == trên String/Integer so sánh địa chỉ chứ không phải giá trị. Compound assignment (+=, -=...) ngầm narrowing cast về kiểu vế trái — byte b += 100 có thể overflow mà không một lời cảnh báo. Bài này mổ từng bẫy theo spec JLS, kèm cách viết đúng.
Bạn đã biết khai báo biến (bài 1), chọn đúng kiểu (bài 2), hiểu reference vs primitive (bài 3). Bây giờ đến lúc làm việc với giá trị — đó là nhiệm vụ của toán tử.
Hệ thống toán tử Java chia làm hai nhóm lớn. Bài này đi nhóm thứ nhất — số học, so sánh và gán — những phép tính dùng hằng ngày kèm các pitfall ẩn của chúng. Nhóm thứ hai — logic/short-circuit, bitwise, ternary và thứ tự ưu tiên — nằm ở bài kế tiếp.
1. Analogy — Máy tính văn phòng và hộp táo
Nhìn nhóm toán tử của bài này qua 2 hình ảnh quen thuộc:
- Máy tính văn phòng — cộng, trừ, nhân, chia, chia lấy dư. Bạn bấm số, bấm phép tính, bấm
=ra kết quả. Đó là arithmetic operators. - Hộp có X quả táo — "hộp A có nhiều hơn hộp B không?" — đó là comparison operators, luôn trả
true/false.
| Đời thường | Concept |
|---|---|
| Máy tính văn phòng | Arithmetic: + - * / % |
| So đếm hai hộp táo | Comparison: == != < > <= >= |
| Ghi đè số mới lên bảng | Assignment: = += -=... |
2. Arithmetic operators — phép tính cơ bản
| Operator | Ý nghĩa | Operand type | Ví dụ | Kết quả |
|---|---|---|---|---|
+ | Cộng (hoặc nối chuỗi) | numeric / String | 3 + 4 | 7 |
- | Trừ | numeric | 10 - 3 | 7 |
* | Nhân | numeric | 4 * 5 | 20 |
/ | Chia | numeric | 10 / 3 | 3 (nguyên!) |
% | Chia lấy dư (modulo) | numeric | 10 % 3 | 1 |
int a = 10;
int b = 3;
System.out.println(a + b); // 13
System.out.println(a - b); // 7
System.out.println(a * b); // 30
System.out.println(a / b); // 3 -- integer division: phan nguyen, bo phan du
System.out.println(a % b); // 1 -- phan du cua phep chia
2.1 String concatenation với +
Khi một trong hai operand là String, + trở thành nối chuỗi:
String ten = "An";
int tuoi = 25;
System.out.println("Xin chao, " + ten + ", " + tuoi + " tuoi.");
// Xin chao, An, 25 tuoi.
// Canh giac: + tinh tu trai sang phai
System.out.println("Tong: " + 1 + 2); // "Tong: 12" -- khong phai "Tong: 3"!
System.out.println("Tong: " + (1 + 2)); // "Tong: 3" -- dung ngoac
3. Pitfall — integer division và modulo âm
3.1 Integer division: kết quả bị truncate
Khi cả hai operand là kiểu nguyên (int, long), phép / cho kết quả nguyên — phần thập phân bị bỏ hoàn toàn (truncate toward zero, không phải làm tròn):
System.out.println(5 / 2); // 2 -- KHONG phai 2.5!
System.out.println(-5 / 2); // -2 -- truncate ve phia 0 (khong phai -3)
System.out.println(7 / 10); // 0 -- nho hon 1, bo het
flowchart LR e1["5 / 2 -- ca hai la int"] d1["integer division<br/>truncate toward zero"] r1["ket qua: 2"] e2["5.0 / 2 -- mot ve la double"] d2["floating-point division"] r2["ket qua: 2.5"] e1 --> d1 --> r1 e2 --> d2 --> r2
Muốn kết quả thực, ít nhất một operand phải là double hoặc float:
System.out.println(5.0 / 2); // 2.5
System.out.println(5 / 2.0); // 2.5
System.out.println((double) 5 / 2); // 2.5 -- ep kieu truoc khi chia
// SAI: ca hai la int, chia nguyen truoc roi moi cong -> mat do chinh xac
int a = 1, b = 2;
double trungBinh = (a + b) / 2; // = 1.0 (khong phai 1.5!)
// DUNG: ep kieu de lam phep chia thuc
double trungBinhDung = (a + b) / 2.0; // = 1.5
double trungBinhDung2 = (double)(a + b) / 2; // = 1.5
3.2 Modulo với số âm
Java định nghĩa % theo công thức: (a / b) * b + a % b == a. Kết quả % giữ dấu của dividend (số bị chia):
System.out.println( 7 % 3); // 1 (7 = 2*3 + 1)
System.out.println(-7 % 3); // -1 (KHONG phai 2 nhu toan hoc!)
System.out.println( 7 % -3); // 1 (dau la dau cua 7 -- duong)
System.out.println(-7 % -3); // -1
Nếu cần kết quả modulo luôn dương (như trong vòng lặp index):
int n = -7, m = 3;
int modDuong = ((n % m) + m) % m; // 2 -- modulo duong
3.3 So sánh String và Integer bằng == — sai!
Như bài 1 và 3 đã giải thích:
==so sánh địa chỉ (reference), không phải giá trị.- ✅ So sánh chuỗi: dùng
.equals()hoặcObjects.equals(). - ✅ So sánh wrapper
Integer,Long...: dùng.equals().
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false -- khac object
System.out.println(s1.equals(s2)); // true -- cung noi dung
Integer a = 200;
Integer b = 200;
System.out.println(a == b); // false -- ngoai Integer Cache
System.out.println(a.equals(b)); // true
4. Comparison operators — so sánh trả boolean
| Operator | Ý nghĩa | Ví dụ | Kết quả |
|---|---|---|---|
== | Bằng nhau | 5 == 5 | true |
!= | Khác nhau | 5 != 3 | true |
< | Nhỏ hơn | 3 < 5 | true |
> | Lớn hơn | 5 > 3 | true |
<= | Nhỏ hơn hoặc bằng | 5 <= 5 | true |
>= | Lớn hơn hoặc bằng | 4 >= 5 | false |
Kết quả luôn là boolean. Dùng trong if, while, for, và làm giá trị gán cho biến boolean.
int diem = 75;
boolean dat = diem >= 50; // true
boolean xuatSac = diem >= 90; // false
if (diem >= 50 && diem < 90) {
System.out.println("Dat -- chua xuat sac");
}
5. Assignment operators — gán và compound assignment
| Operator | Tương đương | Ví dụ |
|---|---|---|
= | (gán trực tiếp) | x = 5 |
+= | x = x + n | x += 3 |
-= | x = x - n | x -= 2 |
*= | x = x * n | x *= 4 |
/= | x = x / n | x /= 2 |
%= | x = x % n | x %= 3 |
int x = 10;
x += 5; // x = 15
x -= 3; // x = 12
x *= 2; // x = 24
x /= 4; // x = 6
x %= 4; // x = 2
5.1 Increment và Decrement
| Operator | Ý nghĩa | Ghi chú |
|---|---|---|
x++ | Tăng x lên 1 (post-increment) | Trả giá trị cũ trước khi tăng |
++x | Tăng x lên 1 (pre-increment) | Tăng rồi trả giá trị mới |
x-- | Giảm x xuống 1 (post-decrement) | Trả giá trị cũ trước khi giảm |
--x | Giảm x xuống 1 (pre-decrement) | Giảm rồi trả giá trị mới |
int i = 5;
System.out.println(i++); // in 5 (gia tri cu), sau do i = 6
System.out.println(i); // 6
System.out.println(++i); // tang truoc: i = 7, in 7
System.out.println(i); // 7
6. Compound assignment có casting ngầm
Đây là pitfall ít người biết:
byte b = 10;
// SAI -- COMPILE ERROR
// b = b + 1;
// Ly do: b + 1 la int (byte duoc promote len int theo integer promotion)
// int khong tu dong thu hep xuong byte
// DUNG -- compound assignment ngam cast ket qua ve kieu goc
b += 1; // OK -- tuong duong (byte)(b + 1)
b++; // OK -- tuong tu
Compound assignment operators (+=, -=, *=, /=, %=) ngầm thực hiện narrowing cast về kiểu của vế trái. Điều này có thể gây data loss ngầm:
byte b = 100;
b += 100; // 100 + 100 = 200, nhung byte chi chua den 127!
System.out.println(b); // -56 -- overflow ngam, khong bao loi!
// Tuong tu voi short
short s = 30_000;
s += 10_000; // 40_000 vuot short range (32_767) -- overflow ngam
System.out.println(s); // -25536
b += 1 ngầm giống b = (byte)(b + 1). Nếu kết quả vượt range của kiểu, overflow xảy ra im lặng — không exception, không warning từ compiler.
Khi dùng byte/short trong vòng lặp cộng dồn, hãy cẩn thận overflow ngầm này.
7. Code example — demo tổng hợp
public class ArithmeticDemo {
public static void main(String[] args) {
// --- Integer division pitfall ---
System.out.println("5 / 2 = " + (5 / 2)); // 2
System.out.println("5.0 / 2 = " + (5.0 / 2)); // 2.5
// --- Modulo am ---
System.out.println("-7 % 3 = " + (-7 % 3)); // -1 (dau cua dividend)
// --- String concat tinh trai sang phai ---
System.out.println("Tong: " + 1 + 2); // Tong: 12
System.out.println("Tong: " + (1 + 2)); // Tong: 3
// --- Comparison tra boolean ---
int score = 75;
boolean passed = score >= 50;
System.out.println("Passed: " + passed); // true
// --- Increment: post vs pre ---
int i = 5;
System.out.println(i++); // 5 -- tra gia tri cu roi moi tang
System.out.println(++i); // 7 -- tang truoc roi tra gia tri moi
// --- Compound assignment va byte overflow ngam ---
byte b = 100;
b += 100; // (byte)(200) = -56
System.out.println("byte b = " + b); // -56
}
}
8. Bảng tổng hợp nhóm toán tử bài này
| Nhóm | Operators | Operand | Trả về | Ghi chú |
|---|---|---|---|---|
| Arithmetic | + - * / % | numeric | numeric | / là integer division nếu cả hai int/long |
| String concat | + | String + any | String | Tính từ trái sang phải |
| Comparison | == != < > <= >= | numeric/reference | boolean | == so sánh reference với object |
| Assignment | = += -= *= /= %= | matching type | (side effect) | Compound ngầm narrow cast |
| Increment | ++ -- | numeric | numeric | Pre vs post khác thứ tự |
9. Deep Dive Oracle
Spec chính thức Java 21:
- JLS §15 — Expressions: toàn bộ quy tắc evaluate biểu thức.
- JLS §15.17 — Multiplicative Operators: integer division truncate toward zero, modulo definition.
- JLS §15.18.1 — String Concatenation Operator +: quy tắc nối chuỗi khi một operand là String.
- JLS §15.26.2 — Compound Assignment Operators: quy tắc cast ngầm trong
+=,-=... — lý dobyte b += 1hợp lệ nhưngbyte b = b + 1thì không.
Diễn giải đơn giản: JLS §15.17 spec rõ ràng rằng integer division "truncates toward zero" — đây là nguyên nhân 5/2=2, -5/2=-2. §15.26.2 quy định compound assignment thực hiện narrowing cast ngầm về kiểu vế trái — lý do byte b += 1 được compile trong khi b = b + 1 thì lỗi.
10. Tóm tắt
- Arithmetic
/giữa hai nguyên → integer division (truncate toward zero). Muốn kết quả thực: một operand phải làdouble. - Modulo
%với số âm: kết quả giữ dấu dividend — không phải modulo toán học. Cần modulo luôn dương:Math.floorMod(). +với một operandStringlà nối chuỗi, tính từ trái sang phải —"Tong: " + 1 + 2raTong: 12.- Comparison luôn trả
boolean. Không dùng==để so sánhStringhay wrapper nhưInteger— dùng.equals(). x++trả giá trị cũ rồi tăng;++xtăng rồi trả giá trị mới.- Compound assignment (
+=,-=...) ngầm cast về kiểu vế trái —byte b += 1OK,byte b = b + 1không; overflow xảy ra im lặng.
11. Tự kiểm tra
Q1Vì sao 7 / 2 = 3 chứ không phải 3.5? Sửa thế nào để ra 3.5?▸
7 / 2 = 3 chứ không phải 3.5? Sửa thế nào để ra 3.5?int → / thực hiện integer division: truncate phần thập phân về 0 (JLS §15.17.2). 7/2 = 3.5 → cắt → 3. Sửa: đảm bảo ít nhất một operand là floating: 7 / 2.0, 7.0 / 2, hoặc (double) 7 / 2 → 3.5.Q2-7 % 3 cho kết quả gì? Giải thích theo công thức JLS.▸
-7 % 3 cho kết quả gì? Giải thích theo công thức JLS.-7 % 3 = -1. JLS §15.17.3: a % b = a - (a / b) * b. -7 / 3 = -2 (integer division truncate toward 0, không phải floor). -2 * 3 = -6. -7 - (-6) = -1. Java modulo giữ dấu của dividend — khác modulo toán học (kết quả luôn ≥ 0). Muốn kết quả luôn dương: Math.floorMod(-7, 3) = 2.Q3Đoạn System.out.println("Tong: " + 1 + 2); in gì? Vì sao không phải Tong: 3?▸
System.out.println("Tong: " + 1 + 2); in gì? Vì sao không phải Tong: 3?Tong: 12. + cùng mức precedence evaluate từ trái sang phải: "Tong: " + 1 có một operand là String nên là nối chuỗi → "Tong: 1". Tiếp tục "Tong: 1" + 2 → lại nối chuỗi → "Tong: 12". Phép cộng số không bao giờ xảy ra. Muốn cộng trước: thêm ngoặc "Tong: " + (1 + 2) — ngoặc ép Java tính 1 + 2 = 3 dạng int trước khi đụng tới String.Q4int i = 5; rồi System.out.println(i++); và System.out.println(++i); in lần lượt gì? Giải thích cơ chế post/pre-increment.▸
int i = 5; rồi System.out.println(i++); và System.out.println(++i); in lần lượt gì? Giải thích cơ chế post/pre-increment.5 rồi 7. i++ (post-increment) trả về giá trị cũ trước, rồi mới tăng — nên in 5, sau lệnh đó i = 6. ++i (pre-increment) tăng trước rồi trả giá trị mới — i từ 6 lên 7, in 7. Cả hai đều tăng biến lên 1; khác nhau duy nhất ở giá trị mà biểu thức trả về. Khi đứng một mình như statement (i++;), hai dạng tương đương — nhưng tránh nhét ++ vào giữa biểu thức phức tạp vì khó đọc.Q5byte b = 100; b += 100; — kết quả là gì? Có exception không? Vì sao?▸
byte b = 100; b += 100; — kết quả là gì? Có exception không? Vì sao?b = -56, không exception. JLS §15.26.2: compound assignment b += 100 có ngầm cast về kiểu vế trái — tương đương b = (byte)(b + 100). 100 + 100 = 200, vượt range byte (−128..127) → narrowing wrap quanh: 200 - 256 = -56. Nếu viết b = b + 100 thì compile error (vì b + 100 là int, không tự narrow vào byte).Q6Vì sao Integer a = 200; Integer b = 200; a == b cho false nhưng a.equals(b) cho true?▸
Integer a = 200; Integer b = 200; a == b cho false nhưng a.equals(b) cho true?-128..127. 200 ngoài range → Integer.valueOf(200) tạo object mới mỗi lần → a và b là 2 reference khác nhau. == so reference → false. .equals() so giá trị intValue() → true. Bài học: luôn dùng .equals() cho wrapper.Bài tiếp theo: Toán tử logic và bitwise — short-circuit, cờ bit và thứ tự ưu tiên
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên