Giải thích ép kiểu trong Java (Từ cơ bản đến nâng cao): Ép kiểu số, ép kiểu lên/đi xuống, instanceof và các lỗi thường gặp

目次

1. Ép kiểu trong Java là gì? (Trả lời nhanh)

Trong Java, ép kiểu có nghĩa là xem một giá trị hoặc một đối tượng như một kiểu khác.
Bạn sử dụng ép kiểu khi muốn chuyển đổi các số (như double sang int) hoặc khi muốn xử lý một đối tượng như một lớp cụ thể hơn (như Animal sang Dog).

Ép kiểu mạnh mẽ, nhưng cũng có thể rủi ro:

  • Ép kiểu số có thể thay đổi giá trị thực (bằng cách cắt bỏ phần thập phân hoặc tràn).
  • Ép kiểu tham chiếu có thể gây lỗi thời gian chạy nếu kiểu thực của đối tượng không khớp.

Bài viết này giải thích về ép kiểu trong Java sao cho bạn có thể viết mã an toàn và tránh các bẫy thường gặp.

1.1 Những gì bạn sẽ học trong hướng dẫn này

Ép kiểu trong Java trở nên dễ dàng hơn rất nhiều khi bạn chia nó thành hai loại:

  • Ép kiểu số (kiểu nguyên thủy) Ép kiểu giữa các kiểu như int, long, double, v.v. Câu hỏi then chốt là việc chuyển đổi có an toàn hay mất thông tin.
  • Ép kiểu tham chiếu (lớp và giao diện) Ép kiểu các đối tượng trong một hệ thống kế thừa, như ép lên (upcasting) và ép xuống (downcasting). Câu hỏi then chốt là kiểu thực thời gian chạy của đối tượng có khớp với ép kiểu hay không.

Nếu bạn trộn lẫn hai chủ đề này quá sớm, ép kiểu sẽ gây nhầm lẫn. Vì vậy chúng ta sẽ học chúng từng bước.

1.2 Các tình huống thường gặp khi sử dụng ép kiểu

Ví dụ 1: Chuyển một số thập phân thành số nguyên

double price = 19.99;
int yen = (int) price; // 19 (decimal part is removed)

Đây là một chuyển đổi thu hẹp, vì vậy Java yêu cầu ép kiểu rõ ràng.

Ví dụ 2: Xem một tham chiếu kiểu cha như kiểu con

Animal a = new Dog();   // upcasting
Dog d = (Dog) a;        // downcasting

Điều này chỉ hoạt động nếu đối tượng thực sự là một Dog. Nếu không, chương trình sẽ bị sập tại thời gian chạy.

1.3 Hai rủi ro chính của việc ép kiểu

Ép kiểu trở nên nguy hiểm vì hai lý do chính:

1) Ép kiểu số có thể thay đổi giá trị

  • Phần thập phân bị cắt bỏ (không làm tròn)
  • Giá trị có thể tràn khi kiểu đích quá nhỏ

2) Ép kiểu tham chiếu có thể làm chương trình của bạn sập

  • Việc ép kiểu xuống sai gây ra ClassCastException

Nếu bạn nhớ hai rủi ro này, bạn sẽ tránh được hầu hết các lỗi ép kiểu.

1.4 Các thuật ngữ mà mọi người nhầm lẫn với ép kiểu

Trước khi tiếp tục, dưới đây là một vài thuật ngữ trông giống nhau nhưng có nghĩa khác nhau:

  • Chuyển đổi ngầm Java tự động chuyển đổi các kiểu trong các tình huống an toàn (thường là chuyển đổi mở rộng).
  • Ép kiểu rõ ràng Bạn viết (type) một cách thủ công khi có thể mất thông tin.
  • Boxing / unboxing Chuyển đổi tự động giữa các kiểu nguyên thủy ( int ) và các lớp bao ( Integer ). Điều này không giống với ép kiểu, nhưng thường gây ra lỗi.

2. Ép kiểu trong Java có Hai Loại: Số và Tham chiếu

Để hiểu đúng về ép kiểu trong Java, bạn phải chia nó thành:

  • Ép kiểu số (kiểu nguyên thủy)
  • Ép kiểu tham chiếu (đối tượng)

Hai loại này tuân theo các quy tắc khác nhau và gây ra các vấn đề khác nhau.

2.1 Ép kiểu số (Kiểu Nguyên Thủy)

Ép kiểu số chuyển đổi một kiểu số nguyên thủy sang kiểu khác, chẳng hạn:

  • Số nguyên: byte, short, int, long
  • Số thực: float, double
  • Ký tự: char (nội bộ là số)

Ví dụ: Ép kiểu số

double d = 10.5;
int i = (int) d; // 10

Đây là một chuyển đổi thu hẹp, vì vậy ép kiểu rõ ràng là bắt buộc.

Chuyển đổi Mở rộng vs Thu hẹp (Quan trọng)

Các chuyển đổi số được nhóm thành:

  • Chuyển đổi mở rộng (khoảng nhỏ → lớn) Ví dụ: int → long, int → double Thông thường an toàn, vì vậy Java cho phép chuyển đổi ngầm.
  • Chuyển đổi thu hẹp (khoảng lớn → nhỏ) Ví dụ: double → int, long → short Rủi ro, vì vậy Java yêu cầu ép kiểu rõ ràng.

2.2 Ép kiểu tham chiếu (Lớp và Giao diện)

Ép kiểu tham chiếu thay đổi cách một đối tượng được xem xét trong một hệ thống kế thừa.

Không phải thay đổi đối tượng, mà là thay đổi kiểu của tham chiếu.

Ví dụ về hệ thống kế thừa:

  • Animal (parent)
  • Dog (child)
    Animal a = new Dog(); // upcasting
    Dog d = (Dog) a;      // downcasting
    

Reference casting is tightly connected to polymorphism in object-oriented programming.

2.3 “Danger” Means Different Things in Numeric vs Reference Casting

This is the most important mental model:

Numeric casting risk

  • The code runs, but the value may change unexpectedly.

Reference casting risk

  • The code compiles, but the program may crash at runtime.

2.4 Key Takeaway (Memorize This)

If you ever feel stuck, come back to this:

  • Numeric casting → “You can convert, but the value might change.”
  • Reference casting → “You can cast, but a wrong cast can crash.”

3. Numeric Casting in Java: Implicit vs Explicit Casting

Numeric casting in Java becomes easy once you understand one simple rule:

  • Widening conversions are usually automatic (implicit).
  • Narrowing conversions require manual casting (explicit) because data may be lost.

This section explains the difference with practical examples and common mistakes.

3.1 Implicit Casting (Widening Conversion) Examples

Java allows implicit conversion when the target type can safely represent the original value range.

Example: intdouble

int i = 10;
double d = i;

System.out.println(d); // 10.0

This is safe because double can represent a much larger range than int.

Example: byteint

byte b = 100;
int i = b;

System.out.println(i); // 100

Since int has a wider range than byte, Java converts automatically.

3.2 Explicit Casting (Narrowing Conversion) and Truncation Behavior

When converting to a smaller or more limited type, Java requires explicit casting.

Example: doubleint

double d = 10.9;
int i = (int) d;

System.out.println(i); // 10

Important detail:

  • Casting to int does not round .
  • It truncates the decimal part.

So:

  • 10.9 becomes 10
  • 10.1 becomes 10
  • -10.9 becomes -10 (moves toward zero)

Example: longint

long l = 100L;
int i = (int) l;

System.out.println(i); // 100

This looks safe, but it becomes dangerous when the long value exceeds the int range.

3.3 Overflow and Underflow: The Hidden Danger

Narrowing conversions are risky not only because decimals get removed, but also because values can overflow.

Example: Casting a large long into int

long l = 3_000_000_000L; // 3 billion (too large for int)
int i = (int) l;

System.out.println(i); // unexpected result

This is one of the worst kinds of bugs because:

  • The code compiles
  • The program runs
  • But the value becomes incorrect silently

3.4 Common Compiler Error: “Possible Lossy Conversion”

If you try narrowing conversion without an explicit cast, Java stops you with a compiler error.

Example: double to int without casting

double d = 1.5;
int i = d; // compile-time error

The message usually includes something like:

  • possible lossy conversion

Meaning:

  • “This conversion may lose information.”
  • “You must write the cast manually if you really want it.”

Example: long to int without casting

long l = 100L;
int i = l; // compile-time error

Even though 100 is safe, Java blocks it because long could hold much larger values.

3.5 Unexpected Casting Behavior Inside Expressions

A common beginner mistake is assuming Java automatically produces decimals in division.

Example: Integer division truncates results

int a = 5;
int b = 2;

System.out.println(a / b); // 2

Because both operands are int, the result is also int.

Fix: Cast one operand to double

int a = 5;
int b = 2;

System.out.println((double) a / b); // 2.5

3.6 Nguyên tắc thực tế (Cho các dự án thực tế)

Để tránh các lỗi chuyển đổi kiểu số, hãy tuân theo các quy tắc sau:

  • Chuyển đổi mở rộng đủ an toàn cho việc chuyển đổi ngầm.
  • Chuyển đổi thu hẹp phải được thực hiện một cách rõ ràng, và bạn phải chấp nhận rằng các giá trị có thể thay đổi.
  • Luôn cẩn thận khi chuyển đổi: wp:list /wp:list
    • floating-point → integer (truncation)
    • large type → smaller type (overflow)

4. Chuyển đổi tham chiếu: Upcasting vs Downcasting

Chuyển đổi tham chiếu liên quan đến đối tượng, không phải các giá trị số.
Thay vì chuyển đổi một số, bạn đang thay đổi cách Java xử lý một tham chiếu đối tượng trong một hệ thống kế thừa.

Quy tắc quan trọng nhất là:

Trong chuyển đổi tham chiếu, điều quan trọng là kiểu thực thi thời gian chạy của đối tượng, không phải kiểu biến.

4.1 Upcasting (Con → Cha) là an toàn và thường ngầm định

Upcasting có nghĩa là xem một đối tượng lớp con như là kiểu của lớp cha.
Điều này rất phổ biến trong Java và thường an toàn.

Ví dụ: Upcasting một Dog thành Animal

class Animal {
    void eat() {
        System.out.println("eat");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("bark");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();

        Animal a = dog; // upcasting (implicit)
        a.eat();        // OK
    }
}

Điều này hoạt động vì mỗi Dog đều là một Animal.

4.2 Những gì bạn mất khi Upcast

Upcasting an toàn, nhưng nó thay đổi các phương thức nào có thể nhìn thấy thông qua kiểu tham chiếu.

Animal a = new Dog();
a.bark(); // compile-time error

Mặc dù đối tượng thực tế là một Dog, kiểu biến là Animal, vì vậy Java chỉ cho phép các phương thức được định nghĩa trong Animal.

4.3 Downcasting (Cha → Con) là rủi ro và yêu cầu chuyển đổi rõ ràng

Downcasting có nghĩa là chuyển đổi một tham chiếu kiểu cha trở lại thành kiểu con.

Animal a = new Dog();
Dog d = (Dog) a; // downcasting (explicit)
d.bark();        // OK

Điều này chỉ hoạt động nếu đối tượng thực tế thực sự là một Dog.

4.4 Tại sao Downcasting nguy hiểm: ClassCastException

Nếu đối tượng thực tế không phải là kiểu mục tiêu, chương trình sẽ bị sập tại thời gian chạy.

Animal a = new Animal();
Dog d = (Dog) a; // ClassCastException at runtime

Mã này biên dịch được vì Java nhận thấy một mối quan hệ kế thừa có thể, nhưng tại thời gian chạy đối tượng không thể trở thành một Dog.

4.5 Kiểu biến vs Kiểu thời gian chạy (Khái niệm then chốt)

Animal a = new Dog();

Trong trường hợp này:

  • Kiểu biến (thời gian biên dịch): Animal
  • Kiểu thời gian chạy (đối tượng thực tế): Dog

Java cho phép điều này nhờ đa hình, nhưng downcasting phải khớp với kiểu thời gian chạy.

5. Sử dụng instanceof trước Downcasting (Mẫu an toàn)

Để ngăn chặn ClassCastException, bạn nên kiểm tra kiểu thời gian chạy trước.

5.1 instanceof làm gì

instanceof kiểm tra xem một đối tượng có phải là một thể hiện của kiểu đã cho hay không.

if (obj instanceof Dog) {
    // obj can be treated as a Dog safely
}

Điều này kiểm tra kiểu thời gian chạy thực tế, không phải kiểu biến đã khai báo.

5.2 Mẫu Downcasting an toàn tiêu chuẩn

Animal a = new Dog();

if (a instanceof Dog) {
    Dog d = (Dog) a;
    d.bark();
}

Điều này ngăn ngừa sự cố bằng cách đảm bảo việc chuyển đổi chỉ diễn ra khi hợp lệ.

5.3 Điều gì xảy ra khi không có instanceof (Sự cố thường gặp)

Animal a = new Animal();
Dog d = (Dog) a; // runtime crash

Đó là lý do tại sao instanceof được coi là công cụ an toàn mặc định.

5.4 Pattern Matching cho instanceof (Cú pháp hiện đại sạch hơn)

Các phiên bản Java mới hơn hỗ trợ một dạng dễ đọc hơn:

Kiểu truyền thống

if (a instanceof Dog) {
    Dog d = (Dog) a;
    d.bark();
}

Kiểu pattern matching

if (a instanceof Dog d) {
    d.bark();
}

Lợi ích:

  • Không cần chuyển đổi thủ công
  • Biến d chỉ tồn tại trong khối if
  • Mã sạch hơn và an toàn hơn

5.5 Khi instanceof Trở Thành Một Mùi Hôi Thiết Kế

Nếu bạn bắt đầu viết mã như thế này ở khắp mọi nơi:

if (a instanceof Dog) {
    ...
} else if (a instanceof Cat) {
    ...
} else if (a instanceof Bird) {
    ...
}

Điều này có thể có nghĩa là thiết kế của bạn thiếu tính đa hình hoặc một cấu trúc giao diện tốt hơn.

Trong phần tiếp theo, chúng ta sẽ thảo luận cách giảm việc ép kiểu bằng cách cải thiện thiết kế.

6. Cách Giảm Việc Ép Kiểu Trong Thiết Kế Java Thực Tế

Ép kiểu đôi khi là cần thiết, nhưng trong các codebase chuyên nghiệp, việc ép kiểu thường xuyên thường là dấu hiệu của một vấn đề thiết kế.

Nếu bạn thấy rất nhiều:

  • các kiểm tra instanceof
  • các ép kiểu hạ cấp lặp lại
  • chuỗi if/else dài dựa trên kiểu

…thường có nghĩa là mã đang đấu tranh chống lại thiết kế hướng đối tượng thay vì sử dụng nó.

6.1 Mẫu “Bùng Nổ Ép Kiểu” Cổ Điển

Đây là một anti-pattern phổ biến:

void process(Animal a) {
    if (a instanceof Dog) {
        Dog d = (Dog) a;
        d.bark();
    } else if (a instanceof Cat) {
        Cat c = (Cat) a;
        c.meow();
    }
}

Cách này hoạt động, nhưng nó tạo ra các vấn đề lâu dài:

  • Thêm một subtype mới buộc bạn phải sửa đổi process()
  • Phương thức cần biết mọi subtype
  • Mã trở nên khó bảo trì và mở rộng hơn

6.2 Sử Dụng Đa Hình Thay Thế Cho Ép Kiểu

Giải pháp sạch sẽ là di chuyển hành vi vào trong hệ thống lớp.

class Animal {
    void sound() {
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("bark");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("meow");
    }
}

Bây giờ logic của bạn trở nên đơn giản và không cần ép kiểu:

void process(Animal a) {
    a.sound(); // no casting needed
}

Đây là ý tưởng cốt lõi của đa hình:

  • Lời gọi phương thức vẫn giữ nguyên
  • Kiểu thời gian chạy quyết định hành vi

6.3 Định Nghĩa Hành Vi Cần Thiết Trong Kiểu Cha Hoặc Giao Diện

Nhiều lần ép kiểu xảy ra vì một lý do:

“Tôi cần một phương thức riêng cho con, nhưng kiểu cha không định nghĩa nó.”

Nếu hành vi thực sự cần thiết, hãy định nghĩa nó ở cấp cao nhất.

Ví dụ: Thiết kế dựa trên giao diện

interface Speaker {
    void speak();
}

class Dog implements Speaker {
    public void speak() {
        System.out.println("bark");
    }
}

class Cat implements Speaker {
    public void speak() {
        System.out.println("meow");
    }
}

Bây giờ bạn có thể viết:

void process(Speaker s) {
    s.speak(); // no downcast needed
}

6.4 Khi Việc Downcasting Thực Sự Hợp Lý

Downcasting không phải lúc nào cũng xấu. Nó có thể chấp nhận được khi:

  • một framework trả về các kiểu generic như Object
  • bạn đang tích hợp các API legacy
  • bạn đang xử lý các sự kiện hoặc callback với các kiểu cơ sở chung

Ngay cả khi đó, hãy giữ nó an toàn:

  • kiểm tra bằng instanceof
  • giữ việc ép kiểu trong một khu vực nhỏ, được kiểm soát
  • tránh lan truyền các ép kiểu khắp mã của bạn

7. Boxing/Unboxing vs Ép Kiểu (Sự Nhầm Lẫn Thông Thường)

Java có hai thế giới của các kiểu số:

  • primitives : int, double, v.v.
  • wrapper classes : Integer, Double, v.v.

Quá trình chuyển đổi tự động giữa chúng được gọi là:

  • boxing : primitive → wrapper
  • unboxing : wrapper → primitive

Điều này không giống như ép kiểu, nhưng nó có thể gây ra lỗi thời gian chạy.

7.1 Ví Dụ Autoboxing (intInteger)

int x = 10;
Integer y = x; // autoboxing
System.out.println(y); // 10

7.2 Ví Dụ Auto-unboxing (Integerint)

Integer x = 10;
int y = x; // auto-unboxing
System.out.println(y); // 10

7.3 Trường Hợp Nguy Hiểm: null + Unboxing = Crash

Integer x = null;
int y = x; // NullPointerException

Điều này trông giống như một phép gán bình thường, nhưng unboxing yêu cầu một đối tượng thực.

7.4 Cạm Bẫy So Sánh Wrapper: Đừng Dùng == cho Giá Trị

Integer a = 1000;
Integer b = 1000;

System.out.println(a == b); // may be false

Sử dụng equals() để so sánh giá trị:

System.out.println(a.equals(b)); // true

8. Generics và Ép Kiểu: Hiểu Cảnh Báo unchecked Warnings

Trong các dự án thực tế, bạn thường thấy các cảnh báo như:

  • unchecked cast
  • unchecked conversion

Những cảnh báo này có nghĩa là:

“Trình biên dịch không thể chứng minh việc ép kiểu này là an toàn về kiểu dữ liệu.”

8.1 Nguyên Nhân Thường Gặp: Raw Types

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List list = new ArrayList(); // raw type
        list.add("Hello");

        List<Integer> numbers = (List<Integer>) list; // unchecked warning
        Integer x = numbers.get(0); // may crash
        System.out.println(x);
    }
}

Điều này không an toàn vì danh sách thực tế chứa một String.

8.2 Giải Pháp: Sử Dụng Kiểu Generic Đúng

List<String> list = new ArrayList<>();
list.add("Hello");

String s = list.get(0);
System.out.println(s);

Không cần ép kiểu, và trình biên dịch sẽ bảo vệ bạn.

8.3 Nếu Bạn Phải Đè Lên Cảnh Báo, Hãy Giữ Nó Tối Thiểu

Đôi khi bạn không thể tránh việc ép kiểu (API cũ, thư viện bên ngoài).
Trong những trường hợp đó:

  • ép kiểu ở một vị trí nhỏ
  • ghi chú lý do nó an toàn
  • đè cảnh báo chỉ trong phạm vi nhỏ nhất

Ví dụ:

@SuppressWarnings("unchecked")
List<String> list = (List<String>) obj;

9. Các Sai Lầm Thường Gặp Khi Ép Kiểu (Ví Dụ Ngắn Copy-Paste)

Ép kiểu dễ “hiểu về lý thuyết” nhưng vẫn dễ gây lỗi trong code thực tế.
Phần này trình bày những sai lầm phổ biến nhất kèm theo các dụ ngắn mà bạn có thể nhanh chóng sao chép và chạy.

Cách tốt nhất để học ép kiểu là:

  • nhìn thấy lỗi
  • hiểu vì sao nó xảy ra
  • học cách sửa an toàn

9.1 Ép Kiểu Số: Mong Đợi Là Làm Tròn Thay Vì Cắt Bớt

Sai Lầm: Nghĩ rằng (int) sẽ làm tròn giá trị

double x = 9.9;
int y = (int) x;

System.out.println(y); // 9

Java không làm tròn ở đây. Nó cắt bỏ phần thập phân.

9.2 Ép Kiểu Số: Ngạc Nhiên Khi Chia Nguyên

Sai Lầm: Mong đợi 2.5 nhưng nhận được 2

int a = 5;
int b = 2;

System.out.println(a / b); // 2

Vì cả hai toán hạng đều là int, kết quả cũng là int.

Giải Pháp: Ép một toán hạng sang double

int a = 5;
int b = 2;

System.out.println((double) a / b); // 2.5

9.3 Ép Kiểu Số: Tràn Khi Ép Số Lớn

Sai Lầm: Ép một long lớn sang int

long l = 3_000_000_000L;
int i = (int) l;

System.out.println(i); // unexpected value

Mã này biên dịch và chạy, nhưng giá trị sẽ sai do tràn.

9.4 Ép Kiểu Tham Chiếu: Downcasting Gây Crash (ClassCastException)

Sai Lầm: Ép một đối tượng sang subtype sai

class Animal {}
class Dog extends Animal {}

public class Main {
    public static void main(String[] args) {
        Animal a = new Animal();
        Dog d = (Dog) a; // ClassCastException
    }
}

Giải Pháp: Sử dụng instanceof

Animal a = new Animal();

if (a instanceof Dog) {
    Dog d = (Dog) a;
    // safe usage
} else {
    System.out.println("Not a Dog, so no cast.");
}

9.5 Ép Kiểu Tham Chiếu: Mất Các Phương Thức Con Sau Khi Upcasting

Sai Lầm: “Đó là một Dog, tại sao tôi không thể gọi bark()?”

class Animal {}
class Dog extends Animal {
    void bark() {
        System.out.println("bark");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog();
        // a.bark(); // compile-time error
    }
}

Kiểu của biến là Animal, vì vậy Java chỉ cho phép các phương thức được khai báo trong Animal.

9.6 Generics: Bỏ Qua unchecked cast và Gây Lỗi Khi Chạy

Sai Lầm: Raw type + ép kiểu không an toàn

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List list = new ArrayList(); // raw type
        list.add("Hello");

        List<Integer> numbers = (List<Integer>) list; // unchecked warning
        Integer x = numbers.get(0); // may crash
        System.out.println(x);
    }
}

Giải Pháp: Sử dụng generic đúng cách

List<String> list = new ArrayList<>();
list.add("Hello");

String s = list.get(0);
System.out.println(s);

9.7 Bẫy So sánh Wrapper: Sử dụng == Thay vì equals()

Lỗi: So sánh giá trị wrapper bằng ==

Integer a = 1000;
Integer b = 1000;

System.out.println(a == b); // may be false

Cách khắc phục: Sử dụng equals()

System.out.println(a.equals(b)); // true

9.8 Sự cố Unboxing Null (NullPointerException)

Lỗi: Unboxing null

Integer x = null;
int y = x; // NullPointerException

Cách khắc phục: Kiểm tra null (hoặc sử dụng các kiểu nguyên thủy khi có thể)

Integer x = null;

if (x != null) {
    int y = x;
    System.out.println(y);
} else {
    System.out.println("x is null");
}

10. Câu hỏi thường gặp (Các câu hỏi về ép kiểu Java)

10.1 Ép kiểu (chuyển đổi kiểu) trong Java là gì?

Ép kiểu có nghĩa là xem một giá trị hoặc một đối tượng như một kiểu khác.

Java có hai loại ép kiểu chính:

  • ép kiểu số (các kiểu nguyên thủy)
  • ép kiểu tham chiếu (đối tượng)

10.2 Sự khác biệt giữa chuyển đổi ngầm và ép kiểu rõ ràng là gì?

  • Chuyển đổi ngầm xảy ra tự động trong các tình huống an toàn (chủ yếu là chuyển đổi mở rộng).
  • Ép kiểu rõ ràng yêu cầu (type) và cần thiết khi dữ liệu có thể bị mất (chuyển đổi thu hẹp).

10.3 (int) 3.9 có làm tròn số không?

Không. Nó cắt bỏ phần thập phân.

System.out.println((int) 3.9); // 3

10.4 Tại sao ép kiểu từ double sang int lại rủi ro?

Bởi vì nó loại bỏ phần thập phân (mất dữ liệu).
Ngoài ra, các giá trị lớn có thể tràn khi được ép sang các kiểu số nhỏ hơn.

10.5 Sự khác biệt giữa upcasting và downcasting là gì?

  • Upcasting (con → cha) là an toàn và thường là ngầm.
  • Downcasting (cha → con) là rủi ro và yêu cầu ép kiểu rõ ràng.

Downcasting sai có thể gây ra ClassCastException.

10.6 Nguyên nhân gây ra ClassCastException là gì, và làm sao khắc phục?

Nó xảy ra khi kiểu đối tượng thực tế tại thời gian chạy không khớp với kiểu mục tiêu của phép ép.

Khắc phục bằng cách kiểm tra với instanceof trước khi ép.

10.7 Tôi có nên luôn sử dụng instanceof trước khi downcasting không?

Nếu có bất kỳ khả năng nào kiểu thời gian chạy không khớp, thì có.
Đây là cách tiếp cận an toàn tiêu chuẩn.

Java hiện đại cũng hỗ trợ pattern matching:

if (a instanceof Dog d) {
    d.bark();
}

10.8 Có nên bỏ qua cảnh báo unchecked cast không?

Thường thì không.

Hầu hết các cảnh báo unchecked xuất phát từ các kiểu raw hoặc các phép ép không an toàn.
Khắc phục nguyên nhân gốc bằng cách sử dụng generic đúng cách.

Nếu thực sự không thể tránh được (API cũ), hãy cô lập phép ép và tắt cảnh báo trong phạm vi nhỏ nhất có thể.

10.9 Làm sao thiết kế mã để tránh quá nhiều phép ép?

Sử dụng các tính năng thiết kế hướng đối tượng như:

  • đa hình (ghi đè hành vi trong các lớp con)
  • giao diện (định nghĩa hành vi yêu cầu trong một kiểu chung)

Nếu bạn liên tục viết các chuỗi instanceof, đó có thể là dấu hiệu thiết kế cần cải thiện.