Java — Từ Zero đến Senior/Cú pháp Java & Kiểu dữ liệu/Ép kiểu — widening, narrowing, autoboxing và safe cast patterns
5/8
~16 phútCú pháp Java & Kiểu dữ liệu

É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

  1. Vì sao int i = 100; float f = i; là widening tự động nhưng float f = 100; int i = f; là compile error?
  2. (byte) 130 cho kết quả gì? Giải thích theo bit representation.
  3. Đoạn sau có lỗi gì?
    Integer score = null;
    int s = score;
    
  4. 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)?
  5. Đoạn sau có ClassCastException không? Vì sao?
    Object obj = "Hello";
    String s = (String) obj;
    
  6. 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);
    }
    
  7. (int) 3.9 cho 3 hay 4? Nếu muốn 4, viết thế nào?

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