Java Foundations/Kiểu nguyên thuỷ — 8 loại, size, range, overflow và floating-point trap
9/35
Bài 9 / 35~17 phútCú pháp Java & Kiểu dữ liệuMiễn phí lượt xem

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

Tự kiểm tra
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?
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.
Q2
float f = 3.14; có compile không? Vì sao? Sửa thế nào?
Không compile. Literal 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).
Q3
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?
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.
Q4
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.10.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.
Q5
char c = 65; in ra gì? Vì sao Java cho phép gán số nguyên cho char?
In ra '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)
(A) không compile. JLS §5.6.2 "binary numeric promotion" — với +, 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ì cint, 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

Đặt 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