Mọi expression trong Java có tính toán đều dùng tới operator. + để cộng, == để so sánh, && để nối các điều kiện. Java có khoảng bốn mươi operator, chia thành vài nhóm, và mỗi nhóm đều có quy tắc rõ ràng khi bạn đã nắm được.
Điều gây vấp lại không nằm ở những chỗ hiển nhiên. 7 / 2 cho 3, 0.1 + 0.2 == 0.3 cho false, i = i++ không làm i thay đổi, còn b += 5 thì compile được trong khi b = b + 5 thì không. Mọi kết quả bên dưới đều được compile và chạy thật trên OpenJDK 21.0.6, nên những gì bạn đọc chính là những gì JVM đã in ra.
![]()
Bài viết đi qua từng nhóm theo đúng thứ tự bạn gặp chúng — số học, hai vai trò của +, tăng giảm, gán, so sánh, logic, bitwise và ba ngôi — rồi kết lại bằng bảng độ ưu tiên nối tất cả với nhau.
Operator trong Java là gì?
Một operator nhận một, hai hoặc ba operand rồi tạo ra một giá trị. -x có một operand, a + b có hai, còn cond ? a : b là operator ba operand duy nhất trong ngôn ngữ.
Điều đáng nhớ nhất là mỗi expression không chỉ có giá trị mà còn có type, và type đó được quyết định ngay lúc compile. 7 / 2 là expression kiểu int, 7 / 2.0 là expression kiểu double, a == b là expression kiểu boolean. Gần như mọi bất ngờ trong bài này đều xuất phát từ type chứ không phải từ phép tính.
| Nhóm | Operator | Kết quả |
|---|---|---|
| Số học | + - * / % | một số |
| Tăng / giảm | ++ -- | một số |
| Gán | = += -= *= /= %= | giá trị vừa gán |
| Quan hệ | < > <= >= | boolean |
| Bằng nhau | == != | boolean |
| Logic | && || ! | boolean |
| Bitwise và dịch bit | & | ^ ~ << >> >>> | một số, hoặc boolean với & | ^ |
| Điều kiện (ba ngôi) | ? : | type của hai nhánh |
| So sánh type | instanceof | boolean |
instanceof là trường hợp riêng: nó so một reference với một type chứ không so hai giá trị. Nó vẫn thuộc nhóm so sánh, và một bài sau về class và object sẽ nói kỹ về nó.
Toán tử số học: + - * / %
Năm operator số học hoạt động với mọi primitive kiểu số.
public class Arithmetic {
public static void main(String[] args) {
int a = 17, b = 5;
System.out.println("a + b = " + (a + b));
System.out.println("a - b = " + (a - b));
System.out.println("a * b = " + (a * b));
System.out.println("a / b = " + (a / b));
System.out.println("a % b = " + (a % b));
}
}
a + b = 22
a - b = 12
a * b = 85
a / b = 3
a % b = 2
Lưu ý rằng tên chính thức của % là toán tử remainder (số dư), không phải "modulo". Khác biệt đó vô hình cho tới khi xuất hiện một số âm, và lúc đó nó rất quan trọng.
Phép chia số nguyên cắt bỏ phần thập phân
Khi cả hai operand đều là kiểu số nguyên, / là phép chia nguyên: kết quả là int (hoặc long) và phần thập phân bị bỏ đi chứ không được làm tròn.
System.out.println(7 / 2);
System.out.println(5 / 2.0);
System.out.println(7 / 2.0);
System.out.println((double) 7 / 2);
System.out.println(-7 / 2);
3
2.5
3.5
3.5
-3
7 / 2 cho 3 vì cả expression là expression kiểu int — không có chỗ nào để chứa phần .5. Chỉ cần một operand là số thực thì cả expression thành double, nên 5 / 2.0 cho 2.5. Phần dư bị cắt về phía 0, vì vậy -7 / 2 là -3 chứ không phải -4.
⚠️ Lỗi kinh điển là
int average = total / count;trong khi bạn muốn một số thập phân. Không có cảnh báo nào cả — kết quả chỉ đơn giản là sai, sai tới gần một đơn vị.
% làm gì với số âm
Phần lớn tài liệu nói ngược quy tắc này. Java định nghĩa % sao cho (a / b) * b + (a % b) bằng a, và vì / cắt về phía 0 nên dấu của số dư đi theo operand bên trái — số bị chia. Dấu của số chia không ảnh hưởng gì.
System.out.println(7 % 3);
System.out.println(-7 % 3);
System.out.println(7 % -3);
System.out.println(-7 % -3);
System.out.println(5.5 % 2);
1
-1
1
-1
1.5
| Expression | Kết quả | Dấu lấy theo |
|---|---|---|
7 % 3 | 1 | số bị chia 7 |
-7 % 3 | -1 | số bị chia -7 |
7 % -3 | 1 | số bị chia 7 |
-7 % -3 | -1 | số bị chia -7 |
Nếu bạn cần modulo theo nghĩa toán học, giá trị không bao giờ âm khi số chia dương, hãy dùng Math.floorMod: Math.floorMod(-7, 3) trả về 2. Và như dòng cuối cho thấy, % cũng áp dụng được cho double — 5.5 % 2 là 1.5.
Chia cho 0 không chỉ có một kiểu hành vi
Chia số nguyên cho 0 thì ném exception. Chia số thực cho 0 thì không — nó tạo ra một trong các giá trị đặc biệt của IEEE 754.
System.out.println(1.0 / 0);
System.out.println(-1.0 / 0);
System.out.println(0.0 / 0.0);
double nan = 0.0 / 0.0;
System.out.println("nan == nan -> " + (nan == nan));
System.out.println("nan != nan -> " + (nan != nan));
System.out.println("Double.isNaN(nan) -> " + Double.isNaN(nan));
System.out.println(1 / 0);
Infinity
-Infinity
NaN
nan == nan -> false
nan != nan -> true
Double.isNaN(nan) -> true
Exception in thread "main" java.lang.ArithmeticException: / by zero
at DivZero.main(DivZero.java:10)
Có ba điều cần nhớ. 1 / 0 với số nguyên ném ArithmeticException: / by zero và làm chết chương trình. 1.0 / 0 lặng lẽ cho Infinity, còn 0.0 / 0.0 cho NaN, nên một chuỗi tính toán số thực có thể mang theo giá trị hỏng đi rất xa trước khi ai đó nhận ra.
Và NaN không bằng bất cứ thứ gì, kể cả chính nó: nan == nan cho false còn nan != nan cho true. Điều đó khiến == vô dụng khi cần phát hiện nó — cách kiểm tra đúng là Double.isNaN(x). NaN cũng thua mọi phép so sánh quan hệ, nên nan < 1, nan > 1 và nan == 1 cùng cho false một lượt.
Toán tử + có hai vai trò
Nếu một trong hai operand là String thì + là phép nối chuỗi. Ngược lại nó là phép cộng số. + kết hợp từ trái sang phải, nên một chuỗi phép cộng được tính từng cặp một từ bên trái — và ý nghĩa có thể đổi ngay giữa chừng.

Rút gọn từng toán tử một là thấy ngay khác biệt:
System.out.println(1 + 2 + "x");
System.out.println("x" + 1 + 2);
System.out.println("x" + (1 + 2));
System.out.println('a' + 1);
System.out.println((char) ('a' + 1));
System.out.println("" + 'a' + 1);
System.out.println('a' + 'b');
3x
x12
x3
98
b
a1
195
| Expression | Được hiểu là | Kết quả |
|---|---|---|
1 + 2 + "x" | (1 + 2) + "x" | 3x |
"x" + 1 + 2 | ("x" + 1) + 2 | x12 |
"x" + (1 + 2) | "x" + 3 | x3 |
Trong 1 + 2 + "x", phép + đầu tiên thấy hai operand int nên nó cộng số; chỉ phép + thứ hai mới gặp String. Trong "x" + 1 + 2, phép + đầu tiên đã tạo ra String rồi, nên phép thứ hai cũng nối chuỗi. Đây là lỗi log phổ biến nhất trong Java: "score: " + score + 1 in ra score: 101 khi score bằng 10.
'a' + 1 in ra 98 vì char được promote lên int trước khi cộng, và kết quả là expression kiểu int. Cast lại bằng (char) để có b, hoặc bắt đầu chuỗi bằng "" để ép sang nhánh nối chuỗi. Cũng vì lý do đó mà 'a' + 'b' cho 195 chứ không phải ab.
Quy tắc thực dụng: khi một chuỗi + trộn lẫn số và text, hãy đặt ngoặc quanh phần tính toán.
Toán tử tăng giảm: ++ và --
++ và -- cộng hoặc trừ 1 ngay tại chỗ. Dạng prefix và postfix thay đổi variable y hệt nhau; chúng chỉ khác nhau ở giá trị mà expression trả ra.

Cái bẫy nằm ở khoảng cách giữa giá trị được lưu và giá trị được trả về:
int i = 5;
System.out.println("i++ yields " + (i++) + ", i is now " + i);
int j = 5;
System.out.println("++j yields " + (++j) + ", j is now " + j);
int k = 5;
int a = k++;
int b = ++k;
System.out.println("k=" + k + " a=" + a + " b=" + b);
i++ yields 5, i is now 6
++j yields 6, j is now 6
k=7 a=5 b=7
| Dạng | Giá trị expression trả ra | Variable sau đó |
|---|---|---|
i++ (postfix) | giá trị trước khi tăng | đã tăng |
++i (prefix) | giá trị sau khi tăng | đã tăng |
Lần theo khối thứ ba: k bắt đầu bằng 5, k++ trả ra 5 và để k thành 6, rồi ++k nâng k lên 7 và trả ra 7.
Vì sao i = i++ không làm i thay đổi
int n = 5;
n = n++;
System.out.println("after n = n++; n = " + n);
int m = 5;
m = ++m;
System.out.println("after m = ++m; m = " + m);
int c = 0;
c = c++ + ++c;
System.out.println("c = c++ + ++c; c = " + c);
after n = n++; n = 5
after m = ++m; m = 6
c = c++ + ++c; c = 2
Hãy đọc n = n++; đúng theo thứ tự Java thực hiện. Vế phải chạy trước: n++ trả ra giá trị cũ là 5 và, như một tác dụng phụ, đặt n thành 6. Sau đó phép gán ghi giá trị vừa trả ra — 5 — ngược trở lại vào n. Phép tăng đã thực sự xảy ra, rồi bị chính phép gán ghi đè. m = ++m; trông có vẻ chạy đúng chỉ vì prefix trả ra giá trị mới.
Dòng cuối là cùng cái bẫy đó nhân đôi: c++ trả ra 0 và làm c thành 1, rồi ++c làm c thành 2 và trả ra 2, nên tổng là 2 và đó là giá trị rơi vào c. Đừng bao giờ trộn phép tăng với phép gán lên cùng một variable — hãy viết n++; thành một dòng riêng.
Toán tử gán và gán kết hợp
Bản thân = cũng là một expression, giá trị của nó là giá trị vừa được gán, và nó kết hợp từ phải sang trái, nên a = b = 0 chạy được. Các dạng kết hợp gộp một phép tính vào trong phép gán.
int x = 10;
x += 5; System.out.println("x += 5 -> " + x);
x -= 3; System.out.println("x -= 3 -> " + x);
x *= 4; System.out.println("x *= 4 -> " + x);
x /= 6; System.out.println("x /= 6 -> " + x);
x %= 5; System.out.println("x %= 5 -> " + x);
x += 5 -> 15
x -= 3 -> 12
x *= 4 -> 48
x /= 6 -> 8
x %= 5 -> 3
x /= 6 trên 48 cho 8 chứ không phải 8.0, vì phép chia nguyên vẫn được áp dụng bên trong dạng kết hợp. += cũng nối chuỗi khi vế trái là String.
Cast ẩn bên trong +=
Đây là một cặp trông y hệt nhau nhưng không hề giống nhau.
byte b = 10;
b += 5;
System.out.println("byte b after b += 5 -> " + b);
byte b after b += 5 -> 15
byte b = 10;
b = b + 5;
CompoundBad.java:4: error: incompatible types: possible lossy conversion from int to byte
b = b + 5;
^
1 error
b + 5 promote b lên int, nên vế phải là expression kiểu int, và một int không nhét vừa vào byte nếu không cast tường minh. Còn operator kết hợp thì có sẵn một phép cast trong định nghĩa của nó: b += 5 nghĩa là b = (byte) (b + 5).
Sự tiện lợi đó che giấu việc cắt bớt giá trị. Cùng phép cast ngầm ấy khiến int i = 5; i += 3.5; compile được và cho 8, và khiến một byte quay vòng:
byte big = 100;
big += 100;
System.out.println(big);
-56
Bài 6 giải thích vì sao byte quay vòng ở mốc 127. Bài 8 trình bày đầy đủ các quy tắc widening và narrowing conversion — ở đây nó chỉ đơn giản là một hành vi của operator kết hợp mà bạn nên biết.
So sánh giá trị: toán tử quan hệ và bằng nhau
< > <= >= == != đều cho ra boolean.
int a = 7, b = 5;
System.out.println("a == b -> " + (a == b));
System.out.println("a != b -> " + (a != b));
System.out.println("a > b -> " + (a > b));
System.out.println("a <= b -> " + (a <= b));
a == b -> false
a != b -> true
a > b -> true
a <= b -> false
Với primitive thì mọi thứ đúng như bạn nhìn thấy. Có hai trường hợp thì không.
Vì sao 0.1 + 0.2 == 0.3 là false
System.out.println(0.1 + 0.2);
System.out.println(0.1 + 0.2 == 0.3);
double sum = 0.1 + 0.2;
System.out.println(Math.abs(sum - 0.3) < 1e-9);
System.out.println(Math.abs(sum - 0.3));
0.30000000000000004
false
true
5.551115123125783E-17
double là số thực dấu phẩy động nhị phân, và 0.1, 0.2, 0.3 đều không biểu diễn chính xác được trong hệ nhị phân. Giá trị được lưu rất gần với con số bạn gõ, nhưng tổng của hai số đầu không trùng từng bit với số thứ ba. == vẫn làm đúng việc của nó; chỉ là các operand không phải những con số như trên giấy.
Hãy so sánh với một sai số cho phép — Math.abs(a - b) < epsilon — và chọn epsilon phù hợp với độ lớn của dữ liệu. Với tiền bạc, hãy dùng BigDecimal hoặc số nguyên đơn vị nhỏ nhất và tránh double hoàn toàn.
== trên reference so sánh danh tính, không so sánh nội dung
String a = new String("hi");
String b = new String("hi");
System.out.println("a == b -> " + (a == b));
System.out.println("a.equals(b) -> " + a.equals(b));
a == b -> false
a.equals(b) -> true
Với một reference type, == hỏi "hai cái này có phải cùng một object không?" còn equals hỏi "hai cái này có cùng nội dung không?" Hai lần gọi new String("hi") tạo ra hai object khác nhau, nên == cho false dù text giống hệt. Bài 9 đi sâu hơn vào String và string pool; quy tắc dùng được ngay lúc này là == cho primitive, equals cho object.
Toán tử logic và short-circuit evaluation
&&, || và ! kết hợp các giá trị boolean. Điểm đặc biệt của && và || là chúng dừng lại ngay khi kết quả đã được quyết định.

&& so với &, || so với |
Cách dễ nhất để thấy short-circuit là bắt mỗi operand tự lên tiếng.
public class ShortCircuit {
static boolean a() {
System.out.println(" a() ran");
return false;
}
static boolean b() {
System.out.println(" b() ran");
return true;
}
public static void main(String[] args) {
System.out.println("a() && b()");
System.out.println(" result = " + (a() && b()));
System.out.println("a() & b()");
System.out.println(" result = " + (a() & b()));
System.out.println("b() || a()");
System.out.println(" result = " + (b() || a()));
System.out.println("b() | a()");
System.out.println(" result = " + (b() | a()));
}
}
a() && b()
a() ran
result = false
a() & b()
a() ran
b() ran
result = false
b() || a()
b() ran
result = true
b() | a()
b() ran
a() ran
result = true
b() không hề in ra gì trong khối đầu tiên. Ngay khi a() trả về false, giá trị của a() && b() đã được biết, nên vế phải bị bỏ qua hoàn toàn. Điều tương tự xảy ra với || khi vế trái đã là true.
| Operator | Ý nghĩa | Operand bên phải |
|---|---|---|
&& | logical AND | bỏ qua khi vế trái là false |
|| | logical OR | bỏ qua khi vế trái là true |
& | AND, không short-circuit | luôn được tính |
| | OR, không short-circuit | luôn được tính |
! | NOT | không có, đây là toán tử một ngôi |
& và | dùng được cho cả boolean lẫn số nguyên, và với boolean thì chúng tính cả hai vế bất kể thế nào. Chỉ dùng chúng khi bạn thực sự muốn cả hai tác dụng phụ đều xảy ra.
Null guard mà short-circuit tạo ra
Đây mới là lý do hằng ngày khiến short-circuit trở nên quan trọng.
static boolean isNotEmpty(String s) {
return s != null && s.length() > 0;
}
false
false
true
Gọi lần lượt với null, "" và "java", kết quả là false, false, true. Trường hợp null an toàn chính vì s != null cho false, nên s.length() không bao giờ được chạy tới. Đổi thứ tự thì lớp bảo vệ đó biến mất:
static boolean isNotEmpty(String s) {
return s.length() > 0 && s != null;
}
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null
at NullGuardBad.isNotEmpty(NullGuardBad.java:3)
at NullGuardBad.main(NullGuardBad.java:7)
Viết s != null & s.length() > 0 với một dấu & cũng ném đúng exception đó, vì & vẫn tính operand bên phải kể cả khi vế trái đã nói không. Thứ tự điều kiện quan trọng, và chọn đúng operator short-circuit cũng quan trọng không kém.
Khi compile bằng javac thuần, thông báo trên hiện là because "<parameter1>" is null thay vì because "s" is null — tên variable cục bộ chỉ vào được class file khi bạn compile với -g, thứ mà IDE và Maven bật sẵn.
Toán tử bitwise và dịch bit
Nhóm này làm việc trên từng bit của một kiểu số nguyên. Ở mức cơ bản, bạn chủ yếu cần nhận ra chúng.
int a = 12; // 1100
int b = 10; // 1010
System.out.println("a & b = " + (a & b));
System.out.println("a | b = " + (a | b));
System.out.println("a ^ b = " + (a ^ b));
System.out.println("~a = " + (~a));
System.out.println("5 << 1 = " + (5 << 1));
System.out.println("20 >> 2 = " + (20 >> 2));
int neg = -8;
System.out.println("-8 >> 1 = " + (neg >> 1));
System.out.println("-8 >>> 1 = " + (neg >>> 1));
System.out.println("binary of -8 = " + Integer.toBinaryString(neg));
System.out.println("binary of -8 >> 1 = " + Integer.toBinaryString(neg >> 1));
System.out.println("binary of -8 >>>1 = " + Integer.toBinaryString(neg >>> 1));
a & b = 8
a | b = 14
a ^ b = 6
~a = -13
5 << 1 = 10
20 >> 2 = 5
-8 >> 1 = -4
-8 >>> 1 = 2147483644
binary of -8 = 11111111111111111111111111111000
binary of -8 >> 1 = 11111111111111111111111111111100
binary of -8 >>>1 = 1111111111111111111111111111100
<< dịch trái, tức là nhân đôi; >> dịch phải và chép bit dấu vào, tức là chia đôi mà vẫn giữ dấu. >>> dịch phải nhưng đẩy bit 0 vào bất kể dấu là gì, nên -8 >>> 1 cho ra số dương khổng lồ 2147483644 — dãy bit 1 ở đầu đã bị thay bằng 0. Dùng >> khi tính toán trên số có dấu, và chỉ dùng >>> khi bạn coi giá trị đó là bit thuần tuý.

Hai dòng kết quả chỉ khác nhau đúng một bit: bit đi vào từ bên trái.
Trường hợp dùng thật sự phổ biến là gói một tập flag vào trong một int:
static final int READ = 1; // 0001
static final int WRITE = 2; // 0010
static final int EXECUTE = 4; // 0100
int perms = READ | WRITE;
System.out.println("perms = " + perms);
System.out.println("can read? " + ((perms & READ) != 0));
System.out.println("can execute? " + ((perms & EXECUTE) != 0));
perms = perms | EXECUTE;
System.out.println("after adding EXECUTE: " + perms + " -> " + Integer.toBinaryString(perms));
perms = 3
can read? true
can execute? false
after adding EXECUTE: 7 -> 111
| bật một flag, & kiểm tra một flag, ^ đảo một flag. Ngoài tập flag, hashing và code protocol mức thấp, bạn sẽ hiếm khi cần tới nhóm này.
Toán tử ba ngôi ?:
Operator ba operand duy nhất: condition ? valueIfTrue : valueIfFalse. Khác với if, nó là một expression, nên nó tạo ra giá trị mà bạn có thể gán hoặc truyền đi.
int age = 20;
String status = age >= 18 ? "adult" : "minor";
System.out.println(status);
int a = 7, b = 5;
System.out.println("max = " + (a > b ? a : b));
int score = 72;
String grade = score >= 80 ? "A" : score >= 60 ? "B" : "C";
System.out.println("grade = " + grade);
adult
max = 7
grade = B
Dạng lồng nhau chạy được vì ?: kết hợp từ phải sang trái — dòng thứ ba được hiểu là score >= 80 ? "A" : (score >= 60 ? "B" : "C"). Lồng một tầng thì vẫn đọc được; lồng hai tầng là lúc nó hết đọc nổi. Cũng lưu ý rằng ba ngôi có độ ưu tiên thấp hơn +, nên nó cần ngoặc khi nằm trong một phép nối chuỗi, như ở dòng max phía trên.
NPE do autoboxing ẩn trong toán tử ba ngôi
Integer boxed = null;
boolean flag = false;
Integer result = flag ? 1 : boxed;
System.out.println(result);
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "boxed" is null
at TernaryNpe.main(TernaryNpe.java:5)
Không có dòng nào ở đây gọi method trên object cả, vậy mà nó vẫn ném exception. Khi một nhánh là int còn nhánh kia là Integer, type của toàn bộ expression điều kiện là int, nên compiler unbox nhánh nào được chọn — và unbox null nghĩa là gọi intValue() trên nó. Giữ hai nhánh cùng một type thì vấn đề biến mất:
Integer result = flag ? Integer.valueOf(1) : boxed;
System.out.println(result);
null
Độ ưu tiên và tính kết hợp của toán tử
Độ ưu tiên quyết định operator nào được gắn kết trước; tính kết hợp quyết định thứ tự giữa các operator cùng độ ưu tiên.
| Độ ưu tiên | Operator | Tính kết hợp |
|---|---|---|
| 1 (cao nhất) | expr++ expr-- | trái sang phải |
| 2 | ++expr --expr +expr -expr ~ ! | phải sang trái |
| 3 | cast (type) expr | phải sang trái |
| 4 | * / % | trái sang phải |
| 5 | + - | trái sang phải |
| 6 | << >> >>> | trái sang phải |
| 7 | < > <= >= instanceof | trái sang phải |
| 8 | == != | trái sang phải |
| 9 | & | trái sang phải |
| 10 | ^ | trái sang phải |
| 11 | | | trái sang phải |
| 12 | && | trái sang phải |
| 13 | || | trái sang phải |
| 14 | ? : | phải sang trái |
| 15 (thấp nhất) | = += -= *= /= %= | phải sang trái |
Hai dòng trong bảng đó là nơi code thật hay sai: các phép dịch bit nằm dưới + và -, còn các operator bitwise nằm dưới ==.
System.out.println(1 << 2 + 3);
System.out.println((1 << 2) + 3);
System.out.println(true || false && false);
System.out.println((true || false) && false);
32
7
true
false
1 << 2 + 3 không phải là (1 << 2) + 3. Phép cộng gắn kết chặt hơn phép dịch, nên nó là 1 << 5, tức 32. Còn true || false && false không được tính từ trái sang phải: && gắn kết chặt hơn ||, nên nó là true || (false && false), cho true — ngược hẳn với cách đọc từ trái sang phải.
Cũng chính quy tắc đó biến một phép kiểm tra bit thành lỗi compile:
int flags = 6, MASK = 2;
if (flags & MASK != 0) {
System.out.println("set");
}
PrecedenceBad.java:4: error: bad operand types for binary operator '&'
if (flags & MASK != 0) {
^
first type: int
second type: boolean
1 error
!= gắn kết chặt hơn &, nên compiler nhìn thấy flags & (MASK != 0) — một int và một boolean. Cách sửa là (flags & MASK) != 0.
Lời khuyên thực dụng rất ngắn: đừng học thuộc cả bảng. Chỉ cần nhớ * và / mạnh hơn + và -, các phép so sánh mạnh hơn && và ||, còn mọi thứ khác thì cho vào ngoặc. Ngoặc chẳng tốn gì và nó còn giúp người đọc code sau bạn.
FAQ
Java có toán tử luỹ thừa không?
Không. ^ là bitwise XOR, nên 2 ^ 10 cho 8 chứ không phải 1024. Hãy dùng Math.pow(2, 10), hàm này trả về 1024.0 kiểu double.
++i có nhanh hơn i++ không?
Không. Khi đứng riêng thành một câu lệnh, cả hai compile ra bytecode giống hệt nhau — javap -c cho thấy đúng một instruction iinc ở cả hai. Hãy chọn dạng nào dựa trên giá trị mà expression cần trả ra, không phải dựa trên hiệu năng.
Khi nào nên dùng & thay cho &&?
Khi các operand là số nguyên và bạn thực sự muốn phép AND theo bit, hoặc khi bạn muốn cả hai vế đều được chạy để lấy tác dụng phụ. Với điều kiện boolean thông thường, && mới là thứ bạn cần, và nó cũng là thứ khiến null guard an toàn.
Vì sao -7 % 3 bằng -1 mà không phải 2?
Vì % là số dư gắn liền với phép chia cắt về phía 0, nên dấu đi theo số bị chia. Nếu bạn cần modulo theo nghĩa toán học, Math.floorMod(-7, 3) trả về 2.
Java có cho phép overload toán tử không?
Không. Java cố ý không hỗ trợ operator overloading. Operator duy nhất mang hai nghĩa, + cho số và cho String, được cài sẵn trong ngôn ngữ chứ không do class nào định nghĩa.
Kết luận
Operator rất dễ đọc và cũng rất dễ dùng sai, và gần như mọi cái bẫy trong bài này đều quy về một điều: type của expression, được quyết định ngay lúc compile. 7 / 2 cắt phần thập phân vì nó là expression kiểu int. 1 + 2 + "x" khác "x" + 1 + 2 vì phép + đầu tiên tạo ra type khác nhau ở hai bên. b += 5 compile được vì operator kết hợp giấu sẵn một phép cast. flag ? 1 : boxed ném exception vì trộn int với Integer buộc phải unbox.
Vì thế chủ đề tiếp theo tự nhiên là chuyển đổi kiểu. Bài 8 nói về type casting trong Java: implicit widening, explicit narrowing, cái giá của từng loại, và khi nào compiler bắt bạn phải tự viết cast ra.