Command Palette

Search for a command to run...

[Java Basics] Toán tử trong Java: số học, so sánh, logic, gán và toán tử ba ngôi

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.

Toán tử trong Java

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ómOperatorKế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 typeinstanceofboolean

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-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
ExpressionKết quảDấu lấy theo
7 % 31số bị chia 7
-7 % 3-1số bị chia -7
7 % -31số bị chia 7
-7 % -3-1số 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 double5.5 % 21.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.

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 > 1nan == 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.

Tính từ trái sang phải khiến + chuyển từ phép cộng thành nối chuỗi

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) + 2x12
"x" + (1 + 2)"x" + 3x3

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 98char đượ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à --

++-- 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.

Biến đang giữ gì so với biểu thức trả về gì, theo từng bước

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ạngGiá trị expression trả raVariable 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

&&, ||! kết hợp các giá trị boolean. Điểm đặc biệt của &&|| là chúng dừng lại ngay khi kết quả đã được quyết định.

Short-circuit evaluation trong Java hoạt động thế nào

&& 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ĩaOperand bên phải
&&logical ANDbỏ qua khi vế trái là false
||logical ORbỏ qua khi vế trái là true
&AND, không short-circuitluôn được tính
|OR, không short-circuitluôn được tính
!NOTkhông có, đây là toán tử một ngôi

&| 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, """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ý.

Trace từng bit khi -8 được dịch phải 1 bit bằng >> và bằng >>>

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ênOperatorTí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
3cast (type) exprphải sang trái
4* / %trái sang phải
5+ -trái sang phải
6<< >> >>>trái sang phải
7< > <= >= instanceoftrá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 +-, 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ớ */ mạnh hơn +-, các phép so sánh mạnh hơn &&||, 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?

% 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.

Bài viết liên quan

[Java Basics] Interface trong Java và so sánh với abstract class

Interface trong Java trên OpenJDK 21: implements khác extends thế nào, member ngầm định public static final, một class implement nhiều interface, default, static và private method, diamond problem và Interface.super, luật class-wins, functional interface, cùng bảng so sánh chi tiết với abstract class.

[Java Basics] Đệ quy trong Java: cách hoạt động và khi nào nên dùng

Đệ quy trong Java hoạt động ra sao: base case và recursive case, trace factorial theo từng frame, StackOverflowError thật khi thiếu base case, độ sâu đệ quy và -Xss, vì sao Fibonacci naive cần 2692537 lời gọi cho fib(30) trong khi memoisation chỉ cần 59, đệ quy so với vòng lặp, và vì sao JVM không tối ưu tail call.

[Java Basics] Mảng một chiều trong Java: khai báo, khởi tạo và duyệt mảng

Hướng dẫn đầy đủ về mảng một chiều trong Java - mọi cách khai báo, giá trị mặc định, field length, index và ArrayIndexOutOfBoundsException, duyệt mảng bằng for và for-each, cái bẫy reference, copy thật với Arrays.copyOf và System.arraycopy, cùng Arrays.equals, mọi ví dụ đều compile và chạy trên JDK 21.

[Java Basics] Chuỗi String trong Java: tính bất biến, String pool và các method thường dùng

String trong Java hoạt động ra sao: vì sao nó immutable, string pool ảnh hưởng thế nào tới ==, kèm bảng tra length, substring, split, replace, trim vs strip, text block và String.format.