Java — Từ Zero đến Senior/Cú pháp Java & Kiểu dữ liệu/Kiểu nguyên thuỷ — 8 loại, size, range, overflow và floating-point trap
2/8
~17 phútCú pháp Java & Kiểu dữ liệu

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ể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
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. 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ể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)
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átdouble15 chữ số thập phân, sai số ít hơn
Đồ họa, GPU, gamefloatGPU 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. 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?

floatdouble 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ínhGiá trị
Size16 bit
Range0 đến 65 535 (unsigned)
Default (field)'�' (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):

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ểuSize (bit)MinMaxDefault (field)SuffixUse case chính
byte8−1281270Binary data, protocol
short16−32 76832 7670Hiếm, interop protocol
int32−2 147 483 6482 147 483 6470Default 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
booleanfalseFlag, điều kiện
char16065 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ả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. 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

13. Tự kiểm tra

  1. Vì sao int max + 1 cho giá trị âm thay vì throw exception? Cơ chế nào gây ra điều này?
  2. float f = 3.14; có compile không? Vì sao? Sửa thế nào?
  3. Bạ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?
  4. 0.1 + 0.2 == 0.3 cho false. Giải thích cơ chế IEEE 754 đằng sau.
  5. char c = 65; in ra gì? Vì sao Java cho phép gán số nguyên cho char?
  6. Đ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