Bài 3 giới thiệu wrapper class và autoboxing. Bài 4 đề cập đến integer division cần ép kiểu. Bài này đi đúng vào trọng tâm: toàn bộ cơ chế chuyển đổi kiểu — khi nào Java làm tự động, khi nào bạn phải viết tường minh, và khi nào có thể mất data hoặc crash runtime.
1. Analogy — Đổ nước giữa các chai có kích thước khác nhau
Hãy nghĩ casting như đổ nước giữa các chai:
- Đổ chai 100 ml vào chai 500 ml → tự nhiên, nước không mất giọt nào, không cần suy nghĩ. Đây là widening (tự động).
- Đổ chai 500 ml vào chai 100 ml → phải cẩn thận, có thể tràn ra ngoài mất nước. Java yêu cầu bạn viết lệnh ép kiểu tường minh để xác nhận "tôi biết rủi ro này". Đây là narrowing (phải khai báo rõ).
2. Widening — mở rộng tự động, không mất data
Widening primitive conversion xảy ra tự động khi bạn gán sang kiểu rộng hơn:
byte b = 42;
short s = b; // byte -> short: OK, tu dong
int i = s; // short -> int: OK, tu dong
long l = i; // int -> long: OK, tu dong
float f = l; // long -> float: OK, tu dong (xem chu y ve float precision)
double d = f; // float -> double: OK, tu dong
Thứ tự widening giữa các primitive:
flowchart LR byte["byte<br/>8-bit"] short["short<br/>16-bit"] int["int<br/>32-bit"] long["long<br/>64-bit"] float["float<br/>32-bit"] double["double<br/>64-bit"] char["char<br/>16-bit"] byte -->|"implicit"| short short -->|"implicit"| int char -->|"implicit"| int int -->|"implicit"| long long -->|"implicit"| float float -->|"implicit"| double
char là kiểu unsigned 16-bit (0–65535) có thể widening sang int — giá trị Unicode code point của ký tự:
char c = 'A';
int code = c; // 65 -- Unicode code point cua 'A'
System.out.println(code); // 65
2.1 Widening trong biểu thức — integer promotion
Khi tính toán, byte, short, char tự động nâng lên int trước khi thực hiện phép tính (đã giải thích chi tiết ở bài 2):
byte x = 10;
byte y = 20;
// byte z = x + y; // COMPILE ERROR -- x + y la int, khong tu thu hep
int z = x + y; // OK: x va y duoc promote len int truoc khi cong
2.2 Widening và floating-point precision
int → float và long → float/double là widening nhưng có thể mất precision (không mất range, chỉ mất độ chính xác):
int precise = 123_456_789;
float f = precise; // widening tu dong
System.out.println(precise); // 123456789
System.out.println(f); // 1.23456792E8 -- precision bi mat!
System.out.println((int) f); // 123456792 -- khac voi gia tri goc!
float chỉ có ~7 chữ số decimal. Số nguyên 9 chữ số không fit chính xác vào mantissa 23-bit.
💡 💡 Cách nhớ — widening không phải luôn an toàn 100%
Widening đảm bảo không mất range (giá trị vẫn trong range kiểu mới). Nhưng khi int/long đổi sang float/double, precision (độ chính xác của từng chữ số) có thể giảm.
Nguyên tắc: int/long → double thường an toàn (double có 52-bit mantissa). long → float mới thực sự mất precision.
3. Narrowing — thu hẹp phải khai báo tường minh
Narrowing primitive conversion yêu cầu bạn viết (targetType) tường minh — đây là cách bạn báo cho compiler "tôi biết có thể mất data, tôi chấp nhận":
double d = 9.99;
int i = (int) d; // phai co (int) -- truncate, khong round
System.out.println(i); // 9 -- phan thap phan bi bo, khong phai 10!
long l = 1_000_000L;
int i2 = (int) l; // phai co (int)
System.out.println(i2); // 1000000 -- OK neu gia tri nam trong int range
3.1 Truncate, không phải round
Narrowing từ floating-point sang integer truncate về phía 0 (bỏ phần thập phân), không làm tròn:
System.out.println((int) 3.9); // 3 -- khong phai 4
System.out.println((int) 3.1); // 3
System.out.println((int) -3.9); // -3 -- truncate ve phia 0 (khong phai -4)
System.out.println((int) -3.1); // -3
Nếu muốn làm tròn trước khi ép kiểu:
double d = 3.9;
int rounded = (int) Math.round(d); // Math.round tra long, ep ve int
System.out.println(rounded); // 4
3.2 Overflow khi narrowing integer
Khi giá trị vượt range của kiểu đích, kết quả wrap around theo bit representation (two's complement) — không exception:
System.out.println((byte) 130); // -126 -- 130 vuot byte range (max 127)
System.out.println((byte) 256); // 0 -- 256 = 0x100, byte chi lay 8 bit thap = 0x00
System.out.println((byte) 257); // 1 -- 257 = 0x101, 8 bit thap = 0x01
System.out.println((int) 1e18); // -1486618624 -- long 10^18 vuot int range, wrap
Giải thích (byte) 130:
130trong binary (8 bit):10000010- Byte là signed — bit cao nhất là sign bit.
10000010trong two's complement =−128 + 2 = −126.
⚠️ Pitfall — narrowing overflow im lặng
Java không throw exception khi narrowing gây overflow — compiler cũng không cảnh báo. Kết quả sai hoàn toàn nhưng code vẫn chạy.
Đây là lý do cần các phương pháp safe narrowing — xem mục 6.
4. Narrowing trong vòng lặp — bảng pitfall
| Biểu thức | Kết quả | Lý do |
|---|---|---|
(int) 3.9 | 3 | Truncate về 0, không round |
(int) -3.9 | -3 | Truncate về 0 |
(byte) 127 | 127 | Đúng max byte |
(byte) 128 | -128 | Overflow: wrap theo two's complement |
(byte) 130 | -126 | 130 - 256 = -126 |
(byte) 256 | 0 | 256 mod 256 = 0 |
(int) 1e18 | -1486618624 | 1e18 vượt Integer.MAX_VALUE |
(short) 40_000 | -25536 | 40000 - 65536 = -25536 |
5. Reference type casting — upcasting và downcasting
Casting cho reference type hoạt động theo quan hệ kế thừa (is-a):
5.1 Upcasting — con → cha (tự động, luôn an toàn)
// Integer la con cua Number, Number la con cua Object
Integer i = 42;
Number n = i; // upcasting tu dong -- Integer IS-A Number
Object o = i; // upcasting tu dong -- Integer IS-A Object
// String la con cua Object
String s = "hello";
Object obj = s; // upcasting tu dong
Upcasting luôn an toàn vì subtype luôn có tất cả những gì supertype có. Không cần viết cast.
5.2 Downcasting — cha → con (phải tường minh, có thể ClassCastException)
Number n = Integer.valueOf(42); // n thuc ra la Integer
// Phai viet (Integer) tuong minh
Integer i = (Integer) n; // OK runtime: n thuc su la Integer
System.out.println(i); // 42
// Loi khi cast sai loai
Number m = Double.valueOf(3.14); // m la Double
Integer bad = (Integer) m; // ClassCastException: Double cannot be cast to Integer
ClassCastException xảy ra tại runtime khi object thực sự không phải kiểu bạn cast sang. JVM kiểm tra tại runtime, không phải lúc compile.
5.3 Kiểm tra trước khi downcast — instanceof
Cách cũ (Java < 16):
Number n = layGiaTriTuDauDo();
if (n instanceof Integer) {
Integer i = (Integer) n; // an toan: da kiem tra
System.out.println("Integer: " + i);
} else if (n instanceof Double) {
Double d = (Double) n;
System.out.println("Double: " + d);
}
Cách mới — Pattern matching for instanceof (Java 16+):
Number n = layGiaTriTuDauDo();
if (n instanceof Integer i) {
// i tu dong duoc khai bao va cast -- khong can viet (Integer) n them lan nua
System.out.println("Integer: " + i);
} else if (n instanceof Double d) {
System.out.println("Double: " + d);
} else {
System.out.println("Kieu khac: " + n.getClass().getName());
}
Pattern matching instanceof (Java 16, JEP 394) kết hợp kiểm tra kiểu và khai báo biến trong một bước — ngắn gọn hơn, ít lỗi hơn vì không phải cast thủ công.
flowchart TD
obj["Object at runtime"]
check{"instanceof TargetType?"}
safe["Cast safe -- use typed variable"]
skip["Skip -- handle other type or default"]
obj --> check
check -->|"yes"| safe
check -->|"no"| skip💡 💡 Cách nhớ — upcasting vs downcasting
- Upcasting (con → cha): như nói "chó là động vật" — luôn đúng, tự động.
- Downcasting (cha → con): như nói "động vật này là chó" — có thể sai nếu thực ra là mèo. Phải kiểm tra (
instanceof) trước.
6. Autoboxing và Unboxing — nhắc lại + liên kết casting
Bài 3 đã giải thích chi tiết autoboxing/unboxing. Liên hệ với casting:
// Autoboxing: primitive -> wrapper (khong phai cast, la boxing)
int primitiveInt = 42;
Integer boxed = primitiveInt; // tu dong: Integer.valueOf(42)
// Unboxing: wrapper -> primitive
int unboxed = boxed; // tu dong: boxed.intValue()
// NGUY HIEM: unbox null -> NPE
Integer maybeNull = null;
int x = maybeNull; // NullPointerException!
Autoboxing không phải widening/narrowing — đây là chuyển đổi giữa primitive và wrapper object, được compiler tạo ra tự động qua Integer.valueOf() và .intValue().
6.1 Không thể cast trực tiếp giữa unrelated wrapper types
Integer i = 42;
// Long l = (Long) i; // COMPILE ERROR -- Integer khong phai Long
// Phai qua unboxing roi re-boxing:
long l = i; // unbox Integer -> int, widening int -> long
Long boxedL = l; // boxing long -> Long
7. Conversion map — khi nào dùng gì?
| Từ | Sang | Cách | Risk |
|---|---|---|---|
int → long | long l = i; | Widening tự động | Không |
int → double | double d = i; | Widening tự động | Không |
long → float | float f = l; | Widening tự động | Có thể mất precision |
double → int | int i = (int) d; | Narrowing tường minh | Truncate phần thập phân |
long → int | int i = (int) l; | Narrowing tường minh | Overflow nếu > MAX_VALUE |
int → Integer | Integer w = i; | Autoboxing tự động | Không (trừ unbox null) |
Integer → int | int i = w; | Unboxing tự động | NPE nếu w == null |
String → int | Integer.parseInt(s) | Parse method | NumberFormatException |
int → String | String.valueOf(i) | Convert method | Không |
long → int safe | Math.toIntExact(l) | Safe method | ArithmeticException nếu overflow |
double → int round | (int) Math.round(d) | Round rồi cast | Không (với range hợp lệ) |
⚠️ String ↔ int: KHÔNG dùng casting
// SAI -- compile error
int i = (int) "42"; // String khong phai numeric type
String s = (String) 42; // int khong phai String
// DUNG
int i = Integer.parseInt("42"); // parse, nem NumberFormatException neu sai format
String s = String.valueOf(42); // convert, luon thanh cong
String s2 = Integer.toString(42); // alternative
8. Safe cast patterns
8.1 Math.toIntExact — narrowing an toàn với fail-fast
long bigNumber = 3_000_000_000L; // vuot int range
// Nguy hiem: wrap around ngam
int bad = (int) bigNumber; // -1294967296 -- sai!
// An toan: nem exception thay vi tra ket qua sai
try {
int safe = Math.toIntExact(bigNumber);
} catch (ArithmeticException e) {
System.out.println("Overflow: " + e.getMessage());
// integer overflow
}
// Gia tri nho hon: OK
long small = 1_000_000L;
int ok = Math.toIntExact(small); // 1000000 -- khong overflow
8.2 Làm tròn trước khi cast float → int
double d = 3.7;
int truncated = (int) d; // 3 -- bo phan thap phan
int rounded = (int) Math.round(d); // 4 -- lam tron ngan nhat
int floored = (int) Math.floor(d); // 3 -- lam tron xuong
int ceiled = (int) Math.ceil(d); // 4 -- lam tron len
8.3 Pattern matching instanceof — safe downcast
Object obj = "Hello, Java!";
// Java 16+ pattern matching
if (obj instanceof String s) {
System.out.println("Do dai: " + s.length()); // s da duoc cast va khai bao
System.out.println("In hoa: " + s.toUpperCase());
}
// Ket hop voi guard condition (Java 21 -- pattern matching + guard)
if (obj instanceof String s && s.length() > 5) {
System.out.println("String dai hon 5 ky tu: " + s);
}
8.4 Integer.parseInt — String → int an toàn
String input = "42abc"; // input tu user, co the sai dinh dang
try {
int value = Integer.parseInt(input);
System.out.println("Gia tri: " + value);
} catch (NumberFormatException e) {
System.out.println("Dinh dang sai: " + input);
}
// Cac method parse khac
long l = Long.parseLong("123456789012345");
double d = Double.parseDouble("3.14");
9. ✅/❌ Khi nào dùng cách nào?
| Tình huống | ✅ Dùng | ❌ Tránh |
|---|---|---|
double → int, chấp nhận truncate | (int) d | — |
double → int, muốn làm tròn | (int) Math.round(d) | (int) d (không round) |
long → int, muốn fail-fast nếu overflow | Math.toIntExact(l) | (int) l (wrap ngầm) |
long → int, chắc chắn giá trị nhỏ | (int) l | — |
| Downcast reference an toàn | if (x instanceof Foo f) { use(f); } | (Foo) x không kiểm tra |
String → int | Integer.parseInt(s) | (int) s (compile error) |
int → String | String.valueOf(i) | (String) i (compile error) |
| Unbox wrapper có thể null | Kiểm tra null trước | Unbox trực tiếp (NPE) |
10. Code example — demo đầy đủ
import java.util.Objects;
public class EpKieuDemo {
public static void main(String[] args) {
// --- Widening tu dong ---
int i = 42;
long l = i; // int -> long: tu dong
double d = i; // int -> double: tu dong
System.out.println("Widening: int=" + i + " long=" + l + " double=" + d);
// --- Narrowing voi data loss ---
double pi = 3.14159;
int truncated = (int) pi; // 3 -- bo phan thap phan
int rounded = (int) Math.round(pi); // 3 -- 3.14 lam tron xuong
System.out.println("3.14159 truncated=" + truncated + " rounded=" + rounded);
// --- Overflow khi narrowing ---
System.out.println("(byte) 130 = " + (byte) 130); // -126
System.out.println("(byte) 256 = " + (byte) 256); // 0
// --- Math.toIntExact fail-fast ---
long big = 3_000_000_000L;
try {
int safe = Math.toIntExact(big);
} catch (ArithmeticException e) {
System.out.println("toIntExact overflow: " + big + " > Integer.MAX_VALUE");
}
// --- Pattern matching instanceof ---
Object obj = "Xin chao";
if (obj instanceof String s) {
System.out.println("String do dai " + s.length() + ": " + s.toUpperCase());
}
// --- Unbox null NPE ---
Integer maybeNull = null;
try {
int x = maybeNull; // unbox null -> NPE
} catch (NullPointerException e) {
System.out.println("NPE khi unbox null Integer");
}
// --- String <-> int ---
String str = "123";
int parsed = Integer.parseInt(str);
String back = String.valueOf(parsed);
System.out.println("Parse: " + parsed + " | Back: " + back);
// --- Widening precision loss ---
int big2 = 123_456_789;
float f = big2; // mat precision
System.out.println("int=" + big2 + " float=" + f + " cast back=" + (int)f);
}
}
Kết quả:
Widening: int=42 long=42 double=42.0
3.14159 truncated=3 rounded=3
(byte) 130 = -126
(byte) 256 = 0
toIntExact overflow: 3000000000 > Integer.MAX_VALUE
String do dai 8: XIN CHAO
NPE khi unbox null Integer
Parse: 123 | Back: 123
int=123456789 float=1.23456792E8 cast back=123456792
11. Bảng pitfall phổ biến với ép kiểu
| Pitfall | Code sai | Hệ quả | Code đúng |
|---|---|---|---|
| Truncate thay vì round | (int) 3.9 mong ra 4 | Được 3 | (int) Math.round(3.9) |
| Narrowing overflow ngầm | (byte) 130 mong giá trị đúng | -126 im lặng | Math.toIntExact() hoặc kiểm tra range |
| Downcast không kiểm tra | (Dog) animal | ClassCastException | instanceof trước |
| Unbox null | int x = nullableInteger | NPE | Kiểm tra null trước |
| Cast String-to-int | (int) "42" | Compile error | Integer.parseInt("42") |
| Widening long → float mất precision | float f = 123_456_789; | 1.23456792E8 | Dùng double hoặc BigDecimal |
12. Deep Dive Oracle
ℹ️ 📚 Deep Dive Oracle (optional)
Spec chính thức Java 21:
- JLS §5.1.2 — Widening Primitive Conversion: danh sách widening tự động, quy tắc precision với float.
- JLS §5.1.3 — Narrowing Primitive Conversion: quy tắc truncate toward zero, wrap-around integer, đặc biệt
(byte) 130 = -126. - JLS §5.5 — Casting Conversion: toàn bộ quy tắc ép kiểu tường minh, khi nào compile error, khi nào ClassCastException runtime.
- JEP 394 — Pattern Matching for instanceof: Java 16, loại bỏ cần cast tường minh sau
instanceof, biến binding trong if-body. - Math.toIntExact API: fail-fast narrowing
long → int. - Integer.parseInt API: parse String thành int, NumberFormatException spec.
Diễn giải đơn giản: JLS §5.1.3 quy định rõ narrowing integer → "phần thấp n bit" (modulo 2^n), đây là lý do (byte) 130 = -126. §5.1.2 cho phép precision loss khi int/long → float/double. JEP 394 giải thích design pattern matching để tránh redundant cast sau instanceof.
13. Tóm tắt
- Widening tự động: nhỏ → rộng hơn (byte → short → int → long → float → double). An toàn về range, nhưng
long → floatcó thể mất precision. - Narrowing tường minh: phải viết
(type). Từ float→int: truncate. Từ int/long→byte/short/int: wrap-around nếu overflow — im lặng, không exception. - Safe narrowing:
Math.toIntExact(long)— fail-fast thay vì wrap. - Upcasting reference (con → cha): tự động, luôn an toàn.
- Downcasting reference (cha → con): phải tường minh, có thể
ClassCastException. Dùnginstanceof(Java 16+: pattern matching) để kiểm tra trước. - Autoboxing/Unboxing: tự động nhưng unbox
null→ NPE. String↔int: không cast được — dùngInteger.parseInt()/String.valueOf().
14. Tự kiểm tra
- Vì sao
int i = 100; float f = i;là widening tự động nhưngfloat f = 100; int i = f;là compile error? (byte) 130cho kết quả gì? Giải thích theo bit representation.- Đoạn sau có lỗi gì?
Integer score = null; int s = score; - Làm thế nào để convert
long l = 5_000_000_000L;vềintmột cách an toàn (throw exception nếu overflow)? - Đoạn sau có
ClassCastExceptionkhông? Vì sao?Object obj = "Hello"; String s = (String) obj; - Viết lại đoạn sau dùng pattern matching
instanceof(Java 16+):if (obj instanceof Integer) { Integer i = (Integer) obj; System.out.println(i * 2); } (int) 3.9cho3hay4? Nếu muốn4, viết thế nào?
Bài tiếp theo: Câu lệnh điều kiện — if, else, switch