Kiểu nguyên thuỷ trong Java: 8 loại, size và overflow
Đi sâu vào 8 kiểu primitive của Java: bit width, min/max range, default value, overflow wrap-around và hiện tượng sai số số thực IEEE 754.
TL;DR: Java cung cấp 8 kiểu dữ liệu nguyên thủy (primitive) được lưu trực tiếp trên Stack. Bốn kiểu số nguyên (byte, short, int, long) dùng bù 2 (two's complement) và sẽ âm thầm tràn số (wrap-around) khi vượt ngưỡng thay vì ném exception. Số thực (float, double) tuân theo chuẩn IEEE 754 nên không thể biểu diễn chính xác các số thập phân hữu hạn như 0.1, bắt buộc dùng BigDecimal cho dữ liệu tài chính. Kiểu char là số nguyên không dấu 16-bit lưu mã UTF-16, và boolean chỉ nhận true hoặc false.
Bài 1: Biến và khai báo đã 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 mô tả kích thước các kiểu dữ liệu như thế nào?
Hãy nghĩ 8 kiểu primitive như bộ cốc đo trong bếp:
- Cốc 1 ml (
byte): dùng đo lượng cực nhỏ, rất dễ tràn. - Cốc 250 ml (
short): ít dùng trong ứng dụng thực tế. - Ca 1 lít (
int): cốc mặc định, dùng trong phần lớn trường hợp. - Xô 4 lít (
long): dùng khi số rất lớn như timestamp hay ID database. - Bình chia độ nhựa (
float): chính xác vừa phải, tiết kiệm bộ nhớ. - Bình chia độ thuỷ tinh (
double): độ chính xác cao, chuẩn công nghiệp. - Công tắc on/off (
boolean): biểu diễn 2 trạng thái true hoặc false. - Ô nhớ ký tự (
char): lưu đúng 1 ký tự Unicode 16-bit.
Chọn cốc quá nhỏ sẽ dẫn tới tràn số (overflow). Chọn cốc quá to gây lãng phí bộ nhớ trong mảng lớn. Chọn bình nhựa thay thuỷ tinh cho tính toán tài chính sẽ làm tích lũy sai số.
2. Bốn kiểu số nguyên trong Java khác biệt nhau ra sao?
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. Hiện tượng overflow xảy ra khi nào và tại sao lại nguy hiểm?
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!");
}
Thử ngẫmmột cột đếm trong hệ thống của bạn khai int và cộng dồn mỗi ngày — cần bao lâu để nó chạm ngưỡng tràn, và ai phát hiện ra trước khi số liệu sai?
4. Float và double hoạt động như thế nào trong bộ nhớ?
| 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. Tại sao phép tính 0.1 + 0.2 không bằng 0.3 trong Java?
Đâ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ố.
Thử ngẫmnếu một tính năng bạn đang làm phải cộng hàng nghìn khoản tiền nhỏ, sai số double tích luỹ đó lớn tới mức nào, và bạn phát hiện ra ở bước nào của quy trình?
6. Kiểu boolean lưu trữ và xử lý trạng thái đúng sai ra sao?
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. Kiểu char trong Java là 16-bit Unicode khác gì so với ASCII?
char c1 = 'A'; // ky tu ASCII
char c2 = '\u00E0'; // ky tu 'a' co dau huyen (a grave) -- Unicode escape
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) | '\u0000' (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.
Thử ngẫmmột trường tên người dùng trong ứng dụng của bạn cho phép nhập emoji — nếu bạn giới hạn độ dài bằng .length(), điều gì xảy ra với input chứa 🚀?
8. Cơ chế integer promotion tự động nâng kiểu trong biểu thức như thế nào?
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:

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 tổng hợp 8 kiểu primitive trong Java gồm những thông số nào?
| 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 | '\u0000' | — | Ký tự Unicode đơn |
10. Khi nào nên dùng int, long, double hay 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. Các pitfall phổ biến cần tránh với kiểu nguyên thuỷ là gì?
Pitfall 1 — Quên L cho long literal:
long soLon = 10_000_000_000L; // Dung: co chu L
Pitfall 2 — Quên f cho float:
float f = 3.14f; // Bat buoc suffix f vi 3.14 mac dinh la double
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. Tài liệu Deep Dive Oracle và chuẩn IEEE 754 có gì đáng chú ý?
- 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.
Đọc thêm: Số nguyên và tràn số — vì sao overflow "vòng lại" đúng theo cơ chế two's complement, và Số thực và IEEE 754 — cơ chế bit đằng sau 0.1 + 0.2 != 0.3 mà bài này chỉ nêu hiện tượng.
13. Tự kiểm tra kiến thức về kiểu nguyên thuỷ như thế nào?
- Q1Vì sao
Integer.MAX_VALUE + 1cho giá trị âm thay vì throw exception? Cơ chế nào gây ra điều này? - Q2
float f = 3.14;có compile không? Vì sao? Sửa thế nào? - Q3Bạn cần lưu timestamp Unix dạng millisecond (hiện tại ~1.7 × 10¹²). Dùng
inthaylong? Vì sao? - Q4
0.1 + 0.2 == 0.3chofalse. Giải thích cơ chế IEEE 754 đằng sau. - Q5
char c = 65;in ra gì? Vì sao Java cho phép gán số nguyên chochar? - 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)
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?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Biến và khai báo — cái tên gắn với một giá trị
Bài tiếpKiểu tham chiếu — null, NPE, wrapper class và autoboxing
Lộ trình Junior Java Backend Developer
Chặng 2/21 · Lập trình từ số 0 với Java — bài 11/32 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này