Java Foundations/Ép kiểu — widening, narrowing, autoboxing và safe cast patterns
12/35
Bài 12 / 35~16 phútCú pháp Java & Kiểu dữ liệuMiễn phí lượt xem

Ép kiểu — widening, narrowing, autoboxing và safe cast patterns

Hiểu toàn bộ cơ chế type casting trong Java: widening tự động không mất data, narrowing cần cast tường minh có thể tràn, autoboxing/unboxing, upcasting/downcasting reference, ClassCastException, và pattern matching instanceof từ Java 16.

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

intfloatlongfloat/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/longdouble thường an toàn (double có 52-bit mantissa). longfloat 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:

  • 130 trong binary (8 bit): 10000010
  • Byte là signed — bit cao nhất là sign bit.
  • 10000010 trong 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ứcKết quảLý do
(int) 3.93Truncate về 0, không round
(int) -3.9-3Truncate về 0
(byte) 127127Đúng max byte
(byte) 128-128Overflow: wrap theo two's complement
(byte) 130-126130 - 256 = -126
(byte) 2560256 mod 256 = 0
(int) 1e18-14866186241e18 vượt Integer.MAX_VALUE
(short) 40_000-2553640000 - 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().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ừSangCáchRisk
intlonglong l = i;Widening tự độngKhông
intdoubledouble d = i;Widening tự độngKhông
longfloatfloat f = l;Widening tự độngCó thể mất precision
doubleintint i = (int) d;Narrowing tường minhTruncate phần thập phân
longintint i = (int) l;Narrowing tường minhOverflow nếu > MAX_VALUE
intIntegerInteger w = i;Autoboxing tự độngKhông (trừ unbox null)
Integerintint i = w;Unboxing tự độngNPE nếu w == null
StringintInteger.parseInt(s)Parse methodNumberFormatException
intStringString.valueOf(i)Convert methodKhông
longint safeMath.toIntExact(l)Safe methodArithmeticException nếu overflow
doubleint round(int) Math.round(d)Round rồi castKhô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
doubleint, chấp nhận truncate(int) d
doubleint, muốn làm tròn(int) Math.round(d)(int) d (không round)
longint, muốn fail-fast nếu overflowMath.toIntExact(l)(int) l (wrap ngầm)
longint, chắc chắn giá trị nhỏ(int) l
Downcast reference an toànif (x instanceof Foo f) { use(f); }(Foo) x không kiểm tra
StringintInteger.parseInt(s)(int) s (compile error)
intStringString.valueOf(i)(String) i (compile error)
Unbox wrapper có thể nullKiểm tra null trướcUnbox 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

PitfallCode saiHệ 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ặngMath.toIntExact() hoặc kiểm tra range
Downcast không kiểm tra(Dog) animalClassCastExceptioninstanceof trước
Unbox nullint x = nullableIntegerNPEKiểm tra null trước
Cast String-to-int(int) "42"Compile errorInteger.parseInt("42")
Widening long → float mất precisionfloat f = 123_456_789;1.23456792E8Dùng double hoặc BigDecimal

12. Deep Dive Oracle

📚 Deep Dive Oracle (optional)

Spec chính thức Java 21:

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/longfloat/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 → float có 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ùng instanceof (Java 16+: pattern matching) để kiểm tra trước.
  • Autoboxing/Unboxing: tự động nhưng unbox null → NPE.
  • Stringint: không cast được — dùng Integer.parseInt() / String.valueOf().

14. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao int i = 100; float f = i; là widening tự động nhưng float f = 100; int i = f; là compile error?
int → float là widening: float có range rộng hơn nhiều (~3.4 × 10³⁸) → không overflow. Cho phép tự động dù có thể mất precision (float chỉ ~7 chữ số significant). float → int là narrowing: float có phần thập phân và range lớn hơn integer — chuyển sang int gây truncate phần thập phân + có thể overflow. Rủi ro data loss cao nên JLS buộc developer viết tường minh (int) f để thể hiện mình biết.
Q2
(byte) 130 cho kết quả gì? Giải thích theo bit representation.
Kết quả -126. byte là 8-bit two's complement, range −128..127. 130 trong binary = 10000010. Narrowing int → byte = "lấy 8 bit thấp" (JLS §5.1.3). Bit cao = 1 → giá trị âm theo two's complement: -(256 - 130) = -126. Đây là wrap-around im lặng, không exception. Muốn fail-fast: dùng Math.toIntExact()-like check hoặc tự validate range.
Q3
Đoạn sau có lỗi gì?
Integer score = null;
int s = score;
Runtime NullPointerException ở dòng 2. Gán Integer (reference) vào int (primitive) cần unbox: compiler sinh score.intValue(). Gọi method trên null → NPE. Fix: kiểm tra null trước (if (score != null) int s = score;), hoặc giá trị mặc định (int s = score != null ? score : 0;), hoặc chuyển sang Optional<Integer>.
Q4
Làm thế nào để convert long l = 5_000_000_000L; về int một cách an toàn (throw exception nếu overflow)?

Dùng Math.toIntExact():

long l = 5_000_000_000L;
int i = Math.toIntExact(l);  // ArithmeticException: integer overflow

Nếu l nằm trong range int → trả giá trị int bình thường. Nếu ngoài range → throw ArithmeticException ngay tại thời điểm convert — fail-fast thay vì im lặng wrap như (int) l.

Q5
Đoạn sau có ClassCastException không? Vì sao?
Object obj = "Hello";
String s = (String) obj;
Không có exception. obj thực sự đang trỏ tới instance String ("Hello"). Downcast (String) ở runtime kiểm tra instance type: match → OK, trả về reference cùng type mới. ClassCastException chỉ xảy ra khi runtime type không tương thích, ví dụ Object obj = 42; String s = (String) obj;objInteger, cast sang String → fail.
Q6
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);
}
if (obj instanceof Integer i) {
    System.out.println(i * 2);
}

JEP 394 (Java 16) bổ sung pattern variable: nếu obj match Integer, compiler tự bind ra biến i kiểu Integer, có scope trong nhánh if. Loại bỏ redundant cast, tránh bug "check rồi cast sai kiểu".

Q7
(int) 3.9 cho 3 hay 4? Nếu muốn 4, viết thế nào?
Cho 3. Cast float/double → int là truncate toward 0 (cắt phần thập phân), không round. 3.9 → bỏ .93. Tương tự (int) -3.9 = -3 (không phải -4). Muốn round: (int) Math.round(3.9)4, hoặc (int) Math.ceil(3.9)4, hoặc (int) Math.floor(3.9)3 tùy ý định.

Bài tiếp theo: Câu lệnh điều kiện — if, else, switch

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