Bài 1 đã giới thiệu int, double, boolean như những kiểu quen dùng nhất và giải thích vì sao primitive nằm thẳng trên stack. Bài này đi sâu hơn: 8 kiểu có gì khác nhau, chọn nhầm kiểu thì xảy ra gì, và tại sao floating-point hay gây lỗi tài chính.
1. Analogy — Cốc đo nước
Hãy nghĩ 8 kiểu primitive như bộ cốc đo trong bếp:
- Cốc 1 ml (
byte) — dùng đo muối, cực nhỏ, tràn dễ - Cốc 250 ml (
short) — dùng ít trong code Java hiện đại - Ca 1 lít (
int) — cốc mặc định, dùng 80% trường hợp - Xô 4 lít (
long) — cần khi số rất lớn (timestamp, ID database) - Bình chia độ nhựa (
float) — chính xác đến một mức, rẻ bộ nhớ - Bình chia độ thuỷ tinh (
double) — chính xác hơn, chuẩn công nghiệp - Công tắc on/off (
boolean) — chỉ 2 trạng thái - Ô nhớ ký tự (
char) — 1 ký tự Unicode 16-bit
Chọn cốc quá nhỏ → tràn (overflow). Chọn cốc quá to → lãng phí memory trong array hàng triệu phần tử. Chọn bình nhựa thay thuỷ tinh cho đo tài chính → sai số tích luỹ.
2. Integer family — 4 kiểu số nguyên
Java có 4 kiểu số nguyên có dấu, tất cả dùng two's complement:
| Kiểu | Size (bit) | Min | Max | Default (field) | Literal suffix |
|---|---|---|---|---|---|
byte | 8 | −128 | 127 | 0 | (không có) |
short | 16 | −32 768 | 32 767 | 0 | (không có) |
int | 32 | −2 147 483 648 | 2 147 483 647 | 0 | (không có) |
long | 64 | −9 223 372 036 854 775 808 | 9 223 372 036 854 775 807 | 0L | L |
byte b = 127;
short s = 32_767;
int i = 2_147_483_647; // Integer.MAX_VALUE
long l = 9_223_372_036_854_775_807L; // phai co chu L
💡 💡 Cách nhớ — công thức range
Kiểu n-bit có dấu chứa từ −2^(n−1) đến 2^(n−1) − 1.
byte(8 bit): −2^7 = −128 đến 2^7 − 1 = 127int(32 bit): −2^31 = −2 147 483 648 đến 2^31 − 1 = 2 147 483 647
Hằng số sẵn: Byte.MIN_VALUE, Integer.MAX_VALUE, Long.MAX_VALUE...
2.1 Khi nào dùng kiểu nào?
int: mặc định cho mọi số nguyên — đếm vòng lặp, chỉ số mảng, ID nhỏ.long: timestamp Unix (milli/nano), ID database auto-increment lớn, file size byte.byte: binary data raw (đọc file, network packet). Không dùng để tiết kiệm memory trong biến đơn lẻ — JVM vẫn cấp 32 bit cho stack slot.short: hiếm dùng trong code Java application; chủ yếu xuất hiện khi giao tiếp giao thức nhị phân cố định-width.
3. Overflow — khi cốc bị tràn
Java không throw exception khi integer overflow. Giá trị quay vòng (wrap around) theo two's complement.
int max = Integer.MAX_VALUE; // 2_147_483_647
int overflow = max + 1; // -2_147_483_648 (Integer.MIN_VALUE!)
System.out.println(overflow); // in: -2147483648
int min = Integer.MIN_VALUE; // -2_147_483_648
int underflow = min - 1; // 2_147_483_647 (MAX_VALUE!)
System.out.println(underflow);
Tại sao lại thế? Binary của 2 147 483 647 là 0111...1111 (31 bit 1). Cộng 1 → 1000...0000 → trong two's complement = −2 147 483 648.
⚠️ Pitfall — overflow ngầm trong thực tế
// Bug kinh dien: dem so nguoi dung, mong nho hon 2 ty
int daiLy = 50_000;
int soGiaoDich = 100_000;
int tongDoanhThu = daiLy * soGiaoDich;
// 50_000 * 100_000 = 5_000_000_000 vuot int range!
// Ket qua wrap: 705_032_704 -- sai hoan toan
long tongDung = (long) daiLy * soGiaoDich; // ep kieu truoc khi nhan
Khi nhân hai int có thể vượt 2 147 483 647, ép một bên sang long trước phép tính.
3.1 Kiểm tra overflow an toàn — Java 8+
// Math.addExact / multiplyExact nem ArithmeticException khi overflow
try {
int result = Math.addExact(Integer.MAX_VALUE, 1);
} catch (ArithmeticException e) {
System.out.println("Overflow bi phat hien!");
}
4. Floating-point family — float và double
| Kiểu | Size (bit) | Mantissa bit | Precision (decimal) | Default | Suffix |
|---|---|---|---|---|---|
float | 32 | 23 | ~7 chữ số | 0.0f | f hoặc F |
double | 64 | 52 | ~15 chữ số | 0.0d (hay 0.0) | d hoặc D (optional) |
float f = 3.14f; // bat buoc chu f, khong co se bi loi compile
double d = 3.14; // mac dinh la double
double d2 = 3.14d; // chu d optional nhung ro rang hon
✅/❌ Khi nào dùng float vs double?
| Tình huống | Dùng gì | Lý do |
|---|---|---|
| Toán học tổng quát | ✅ double | 15 chữ số thập phân, sai số ít hơn |
| Đồ họa, GPU, game | ✅ float | GPU tối ưu 32-bit, RAM giảm một nửa |
| Tiền tệ, tài chính | ❌ Cả hai | Dùng BigDecimal |
| Array hàng triệu phần tử | float | Tiết kiệm 50% RAM so với double |
Quy tắc đơn giản: Gần như luôn dùng double. Chỉ dùng float khi có lý do memory/GPU rõ ràng.
5. Floating-point precision trap — 0.1 + 0.2 ≠ 0.3
Đây là một trong những surprise phổ biến nhất với người mới học lập trình:
double a = 0.1;
double b = 0.2;
System.out.println(a + b); // 0.30000000000000004 -- khong phai 0.3!
System.out.println(a + b == 0.3); // false
Vì sao vậy?
float và double dùng chuẩn IEEE 754 binary floating-point — số được biểu diễn trong cơ số 2 (binary), không phải cơ số 10.
Tương tự như 1/3 không biểu diễn chính xác trong decimal (0.333...), 0.1 không biểu diễn chính xác trong binary:
0.1 (decimal) = 0.0001100110011... (binary, lap vo han)
Máy tính phải làm tròn → tích luỹ sai số sau nhiều phép tính.
💡 💡 Cách nhớ — binary fraction
Giống 1/3 = 0.333... không kết thúc trong decimal, 1/10 = 0.0001100110011... không kết thúc trong binary.
Không có cách nào lưu chính xác 0.1 trong 32 hay 64 bit.
So sánh floating-point đúng cách
double x = 0.1 + 0.2;
double expected = 0.3;
// Sai: so sanh truc tiep
if (x == expected) { ... } // false, du mat
// Dung: so sanh voi epsilon
double EPSILON = 1e-9;
if (Math.abs(x - expected) < EPSILON) {
System.out.println("Bang nhau trong nguong sai so");
}
5.1 Dùng BigDecimal cho tiền tệ
import java.math.BigDecimal;
BigDecimal gia = new BigDecimal("19.99"); // PHAI dung String literal!
BigDecimal soLuong = new BigDecimal("3");
BigDecimal tongCong = gia.multiply(soLuong);
System.out.println(tongCong); // 59.97 -- chinh xac
⚠️ BigDecimal gotcha
// SAI -- float/double da mat do chinh xac truoc khi vao BigDecimal
new BigDecimal(0.1) // = 0.1000000000000000055511151231257827021181583404541015625
// DUNG -- String literal giu chinh xac
new BigDecimal("0.1") // = 0.1
BigDecimal chậm hơn double đáng kể vì dùng arithmetic phần mềm. Chỉ dùng khi cần chính xác tuyệt đối (tiền tệ, thuế, kế toán). Chi tiết BigDecimal sẽ có ở Module xử lý số.
6. Boolean — đơn giản nhất
boolean daDangNhap = false;
boolean isActive = true;
- Size: JVM spec không yêu cầu 1 bit — thường chiếm 1 byte trong biến đơn, 1 bit trong
boolean[]. - Default (field):
false - Chỉ nhận 2 giá trị:
true/false. Không thể ép từint(khác C/C++).
// Sai -- Java khong cho phep nay
if (count) { ... } // compile error
// Dung
if (count > 0) { ... }
7. char — 16-bit Unicode, KHÔNG phải ASCII
char c1 = 'A'; // ky tu ASCII
char c2 = 'à'; // a voi accent (a grave) -- Unicode escap
char c3 = 65; // so nguyen -- 'A' (char co the nhan int literal)
| Thuộc tính | Giá trị |
|---|---|
| Size | 16 bit |
| Range | 0 đến 65 535 (unsigned) |
| Default (field) | '�' (null character) |
| Encoding | UTF-16 code unit |
7.1 char và Unicode — điều hay bị hiểu nhầm
char là một UTF-16 code unit, đủ cho hầu hết ký tự (Basic Multilingual Plane, U+0000 đến U+FFFF). Tuy nhiên, ký tự ngoài BMP — ví dụ emoji 🚀 (U+1F680) — cần 2 char (surrogate pair):
String rocket = "🚀"; // emoji Rocket = 2 char (surrogate pair)
System.out.println(rocket.length()); // 2 -- khong phai 1!
System.out.println(rocket.codePointCount(0, rocket.length())); // 1
Thực tế: Khi làm việc với text cần hỗ trợ emoji hoặc ký tự ngoài BMP, dùng codePointAt() thay charAt(), hoặc xử lý qua String API cao hơn.
8. Integer promotion — biểu thức tự nâng kiểu
Khi tính toán với byte, short, char, Java tự động nâng lên int trước khi thực hiện phép tính:
flowchart LR byte["byte or short or char"] int["int (auto-promoted)"] long["long"] float["float"] double["double"] byte -->|"widening"| int int -->|"if one operand is long"| long long -->|"if one operand is float"| float float -->|"if one operand is double"| double
byte x = 10;
byte y = 20;
// byte z = x + y; // COMPILE ERROR -- ket qua phep tinh la int
int z = x + y; // OK: x va y duoc promote len int truoc khi cong
byte z2 = (byte)(x + y); // OK: ket qua ep lai ve byte
Đây là lý do byte + byte cho int, trông kỳ lạ lúc đầu nhưng có nguyên nhân rõ ràng: tránh overflow ngầm trong intermediate calculation.
9. Bảng đầy đủ 8 kiểu primitive
| Kiểu | Size (bit) | Min | Max | Default (field) | Suffix | Use case chính |
|---|---|---|---|---|---|---|
byte | 8 | −128 | 127 | 0 | — | Binary data, protocol |
short | 16 | −32 768 | 32 767 | 0 | — | Hiếm, interop protocol |
int | 32 | −2 147 483 648 | 2 147 483 647 | 0 | — | Default cho số nguyên |
long | 64 | −9.2 × 10¹⁸ | 9.2 × 10¹⁸ | 0L | L | Timestamp, ID lớn |
float | 32 | ~−3.4 × 10³⁸ | ~3.4 × 10³⁸ | 0.0f | f | Graphics, GPU |
double | 64 | ~−1.8 × 10³⁰⁸ | ~1.8 × 10³⁰⁸ | 0.0 | d | Default cho số thực |
boolean | — | — | — | false | — | Flag, điều kiện |
char | 16 | 0 | 65 535 | '�' | — | Ký tự Unicode đơn |
10. Khi nào dùng int / long / double / BigDecimal?
| Tình huống cụ thể | Kiểu |
|---|---|
| Chỉ số vòng lặp, đếm đơn giản | int |
| Timestamp Unix milliseconds | long |
| ID database (> 2 tỷ dòng) | long |
| Tính toán khoa học, vật lý | double |
| Giá tiền, tỷ giá, % thuế | BigDecimal |
| Đồ họa 3D, vertex buffer | float |
| File size byte | long |
11. Pitfall tổng hợp
Pitfall 1 — Quên L cho long literal:
long lớn = 10_000_000_000; // COMPILE ERROR -- int literal vuot range
long dung = 10_000_000_000L; // OK
Pitfall 2 — Quên f cho float:
float f = 3.14; // COMPILE ERROR -- 3.14 la double literal
float f2 = 3.14f; // OK
Pitfall 3 — Overflow trong nhân int:
int a = 1_000_000;
int b = 1_000_000;
long c = a * b; // SAI: nhan int overflow truoc khi convert sang long
long d = (long) a * b; // DUNG: ep kieu truoc
Pitfall 4 — Dùng double cho tiền tệ:
double gia = 0.1 + 0.2; // 0.30000000000000004
// Dung: BigDecimal("0.1").add(new BigDecimal("0.2"))
12. Deep Dive Oracle
ℹ️ 📚 Deep Dive Oracle — nguồn spec chính thức
- JLS §4.2 — Primitive Types and Values: định nghĩa 8 kiểu, range, two's complement.
- JLS §4.2.3 — Floating-Point Types, Formats, and Values: IEEE 754, special values (NaN, Infinity).
- JLS §5.6 — Numeric Contexts: integer promotion rules.
- IEEE 754 Standard: binary floating-point đằng sau
float/double. - Math.addExact API: overflow-safe arithmetic.
- BigDecimal API: arbitrary-precision decimal.
13. Tự kiểm tra
- Vì sao
int max + 1cho giá trị âm thay vì throw exception? Cơ chế nào gây ra điều này? float f = 3.14;có compile không? Vì sao? Sửa thế nào?- Bạn cần lưu số giây kể từ epoch Unix (hiện tại ~1.7 × 10¹²). Dùng
inthaylong? Vì sao? 0.1 + 0.2 == 0.3chofalse. Giải thích cơ chế IEEE 754 đằng sau.char c = 65;in ra gì? Vì sao Java cho phép gán số nguyên chochar?- Đoạn nào dưới đây không compile? Vì sao?
byte a = 10; byte b = 20; byte c = a + b; // (A) byte a = 10; byte b = 20; int c = a + b; // (B)
Bài tiếp theo: Kiểu tham chiếu — null, NPE, wrapper class và autoboxing