Java Foundations
11/39
Bài 11 / 39~13 phútCú pháp Java & Kiểu dữ liệuLộ trình · chặng 02/21Miễn phí… lượt xem

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ểuSize (bit)MinMaxDefault (field)Literal suffix
byte8−1281270(không có)
short16−32 76832 7670(không có)
int32−2 147 483 6482 147 483 6470(không có)
long64−9 223 372 036 854 775 8089 223 372 036 854 775 8070LL
Java
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 = 127
  • int (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.

Java
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ế
Java
// 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+

Java
// 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ểuSize (bit)Mantissa bitPrecision (decimal)DefaultSuffix
float3223~7 chữ số0.0ff hoặc F
double6452~15 chữ số0.0d (hay 0.0)d hoặc D (optional)
Java
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ốngDùng gìLý do
Toán học tổng quát✅ double15 chữ số thập phân, sai số ít hơn
Đồ họa, GPU, game✅ floatGPU tối ưu 32-bit, RAM giảm một nửa
Tiền tệ, tài chính❌ Cả haiDùng BigDecimal
Array hàng triệu phần tửfloatTiế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:

Java
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

Java
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ệ

Java
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
Java
// 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?

Java
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++).
Java
// 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?

Java
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ínhGiá trị
Size16 bit
Range0 đến 65 535 (unsigned)
Default (field)'\u0000' (null character)
EncodingUTF-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):

Java
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:

Thang nâng kiểu trong biểu thức: byte, short, char luôn được nâng lên int trước, rồi lên long, float, double tuỳ theo toán hạng rộng nhất trong phép tính

Java
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ểuSize (bit)MinMaxDefault (field)SuffixUse case chính
byte8−1281270—Binary data, protocol
short16−32 76832 7670—Hiếm, interop protocol
int32−2 147 483 6482 147 483 6470—Default cho số nguyên
long64−9.2 × 10¹⁸9.2 × 10¹⁸0LLTimestamp, ID lớn
float32~−3.4 × 10³⁸~3.4 × 10³⁸0.0ffGraphics, GPU
double64~−1.8 × 10³⁰⁸~1.8 × 10³⁰⁸0.0dDefault cho số thực
boolean———false—Flag, điều kiện
char16065 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ảnint
Timestamp Unix millisecondslong
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 bufferfloat
File size bytelong

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:

Java
long soLon = 10_000_000_000L; // Dung: co chu L

Pitfall 2 — Quên f cho float:

Java
float f = 3.14f; // Bat buoc suffix f vi 3.14 mac dinh la double

Pitfall 3 — Overflow trong nhân int:

Java
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ệ:

Java
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ú ý?

📚 Deep Dive Oracle — nguồn spec chính thức

Đọ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?

Tự kiểm tra
0/6 câu đã trả lời
  1. Q1
    Vì sao Integer.MAX_VALUE + 1 cho giá trị âm thay vì throw exception? Cơ chế nào gây ra điều này?
  2. Q2
    float f = 3.14; có compile không? Vì sao? Sửa thế nào?
  3. Q3
    Bạn cần lưu timestamp Unix dạng millisecond (hiện tại ~1.7 × 10¹²). Dùng int hay long? Vì sao?
  4. Q4
    0.1 + 0.2 == 0.3 cho false. Giải thích cơ chế IEEE 754 đằng sau.
  5. Q5
    char c = 65; in ra gì? Vì sao Java cho phép gán số nguyên cho char?
  6. 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.

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

Bài tiếp theo

Kiểu tham chiếu — null, NPE, wrapper class và autoboxing