Kiểu nguyên thuỷ — 8 loại, size, range, overflow và floating-point trap
Đi sâu vào 8 kiểu primitive của Java: bit width, min/max range, default value, overflow wrap-around và vì sao 0.1 + 0.2 không bằng 0.3 trong Java (và mọi ngôn ngữ dùng IEEE 754).
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
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.
// 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.
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
// 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
- 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
Q1Vì sao Integer.MAX_VALUE + 1 cho giá trị âm thay vì throw exception? Cơ chế nào gây ra điều này?▸
Integer.MAX_VALUE + 1 cho giá trị âm thay vì throw exception? Cơ chế nào gây ra điều này?int là 32-bit two's complement. Integer.MAX_VALUE = 0x7FFFFFFF. Cộng 1 → 0x80000000 — bit dấu = 1 → giá trị âm nhất Integer.MIN_VALUE = -2147483648. JVM (và CPU) không kiểm tra overflow cho +/-/* trên int/long — im lặng wrap quanh. Muốn fail-fast: dùng Math.addExact(), Math.multiplyExact() — throw ArithmeticException khi overflow.Q2float f = 3.14; có compile không? Vì sao? Sửa thế nào?▸
float f = 3.14; có compile không? Vì sao? Sửa thế nào?3.14 mặc định là double (64-bit); gán double vào float là narrowing → phải tường minh. Sửa: float f = 3.14f; (suffix f → literal là float) hoặc float f = (float) 3.14; (ép kiểu).Q3Bạn cần lưu số giây kể từ epoch Unix (hiện tại ~1.7 × 10¹²). Dùng int hay long? Vì sao?▸
int hay long? Vì sao?long. Integer.MAX_VALUE ≈ 2.1 × 10⁹ (~2 tỷ) — đủ cho Unix giây cho tới năm 2038 ("Y2038 problem"), không đủ cho millisecond hiện tại (~1.7 × 10¹²). long (64-bit) hỗ trợ tới ~9.2 × 10¹⁸ — thừa thãi cho timestamp mọi granularity. API chuẩn (System.currentTimeMillis(), Instant.getEpochSecond()) đều trả long.Q40.1 + 0.2 == 0.3 cho false. Giải thích cơ chế IEEE 754 đằng sau.▸
0.1 + 0.2 == 0.3 cho false. Giải thích cơ chế IEEE 754 đằng sau.double dùng IEEE 754 binary floating-point. 0.1 và 0.2 là phân số thập phân không biểu diễn chính xác trong hệ nhị phân (giống như 1/3 = 0.333... trong hệ thập phân). Mỗi giá trị được round về xấp xỉ 53-bit gần nhất. Tổng 0.1 + 0.2 ra xấp xỉ 0.30000000000000004. 0.3 literal cũng là xấp xỉ nhưng giá trị khác. So sánh bit-by-bit → false. Fix: Math.abs(a - b) < 1e-9, hoặc dùng BigDecimal cho tính toán tài chính.Q5char c = 65; in ra gì? Vì sao Java cho phép gán số nguyên cho char?▸
char c = 65; in ra gì? Vì sao Java cho phép gán số nguyên cho char?'A'. char trong Java là số unsigned 16-bit (0–65535), đồng thời là code point Unicode (UTF-16 code unit). 65 là code point của 'A' theo ASCII/Unicode. JLS cho phép gán int literal vào char nếu giá trị nằm trong range hợp lệ (compile-time constant trong 0..65535), vì char bản chất là số.Q6Đ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)
▸
byte a = 10; byte b = 20; byte c = a + b; // (A)
byte a = 10; byte b = 20; int c = a + b; // (B)+, cả hai byte được promote lên int trước khi cộng, kết quả là int. Gán int vào byte là narrowing → phải tường minh. Sửa: byte c = (byte)(a + b);. (B) OK vì c là int, nhận int bình thường.Bài tiếp theo: Kiểu tham chiếu — null, NPE, wrapper class và autoboxing
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