Fix java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer

intermediate Java2026-03-24| Java 8+, mọi hệ điều hành (Windows / Linux / macOS), mọi framework trên JVM (Spring, Jakarta EE, Java thuần)

Error Message

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
#java#classcast#ép kiểu#generics#lỗi runtime

Lỗi Gặp Phải

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer

Lỗi này xuất hiện lúc runtime — thường trong code mà compiler đã cho qua mà không có cảnh báo nào. Bạn viết lệnh cast, code biên dịch được, mọi thứ trông ổn. Rồi một giá trị thực sự chạy qua và JVM phản đối ầm ĩ. Sự không tương thích kiểu dữ liệu vốn đã tồn tại từ đầu; Java chỉ không phát hiện ra cho đến khi thực thi.

Nguyên Nhân

Java kiểm tra tính tương thích của cast lúc runtime, không phải lúc compile time. Ba tình huống thường gây ra lỗi này:

  • Raw types / unchecked cast — sử dụng List không có generics, rồi cast các phần tử ra ngoài.
  • Giả định sai về kiểu đối tượng — một method trả về Object và bạn cast nó sang kiểu bạn kỳ vọng.
  • Deserialization / reflection — đọc dữ liệu từ JSON, YAML, database, hoặc file config và giả định rằng kiểu dữ liệu vẫn được giữ nguyên sau quá trình chuyển đổi.

Tái Hiện Vấn Đề

Phiên bản đơn giản nhất của lỗi:

List list = new ArrayList(); // raw type — không có generics
list.add("hello");
Integer num = (Integer) list.get(0); // ClassCastException tại đây

Compiler cảnh báo bạn bằng "unchecked cast" nhưng vẫn biên dịch. Lúc runtime, JVM thấy một String ở chỗ bạn cam kết là Integer — crash.

Map với giá trị kiểu Object cũng là thủ phạm thường gặp:

Map<String, Object> config = loadConfig();
Integer timeout = (Integer) config.get("timeout"); // lỗi nếu timeout là "30" (một String)

Pattern này xuất hiện khắp nơi trong Spring config loading và các property reader tùy chỉnh. Một giá trị hiển thị là 30 thực ra có thể được lưu dưới dạng chuỗi "30", chứ không phải số nguyên 30.

Cách Khắc Phục

Cách 1 — Dùng Generics

Chuyển từ raw types sang generics được tham số hóa đúng cách giúp compiler phát hiện lỗi kiểu dữ liệu trước khi bạn chạy bất cứ thứ gì.

// Trước (raw type)
List list = new ArrayList();
list.add("hello");
Integer num = (Integer) list.get(0); // crash lúc runtime

// Sau (generic type)
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // không cần cast, an toàn ở compile time

Không cast, không lỗi. Đó là mục tiêu cần đạt.

Cách 2 — Kiểm Tra với instanceof Trước Khi Cast

Xử lý giá trị trả về kiểu Object hoặc dữ liệu bên ngoài? Hãy luôn xác minh kiểu dữ liệu trước:

Object value = config.get("timeout");

if (value instanceof Integer) {
    Integer timeout = (Integer) value;
    // dùng timeout
} else if (value instanceof String) {
    Integer timeout = Integer.parseInt((String) value);
    // dùng timeout
} else {
    throw new IllegalArgumentException("Kiểu không mong đợi cho timeout: " + value.getClass());
}

Pattern matching của Java 16+ rút gọn điều này gọn hơn:

// Pattern matching instanceof của Java 16+
if (value instanceof Integer timeout) {
    System.out.println("Timeout: " + timeout);
} else if (value instanceof String s) {
    System.out.println("Timeout (chuỗi): " + Integer.parseInt(s));
}

Pattern matching loại bỏ hoàn toàn dòng cast thừa — biến đã có đúng kiểu ngay bên trong block.

Cách 3 — Khắc Phục Vấn Đề Deserialization và Map Type

Jackson và Gson hoạt động khác nhau tùy theo giá trị. Một số JSON như 30 được deserialize thành Integer nếu nằm trong phạm vi, Long nếu vượt quá 2.147.483.647, và Double nếu có phần thập phân. Bạn không thể lúc nào cũng đoán được kiểu nào sẽ nhận được từ Map<String, Object>.

// Jackson: deserialize vào một class có kiểu cụ thể thay vì Map thô
ObjectMapper mapper = new ObjectMapper();
MyConfig config = mapper.readValue(json, MyConfig.class); // có kiểu, an toàn

// Nếu buộc phải dùng Map<String, Object>, dùng Number làm cầu nối
Object raw = map.get("count");
int count = ((Number) raw).intValue(); // xử lý được Integer, Long, Double một cách an toàn

Number là supertype chung cho tất cả các kiểu số boxed. Cast sang nó trước, rồi gọi .intValue(), giúp tránh hoàn toàn sự mơ hồ về kiểu.

Cách 4 — Bọc Raw Collection từ API Cũ

Các API cũ đôi khi trả về List hoặc Collection thô. Có hai lựa chọn: suppress và cast, hoặc copy có kiểm tra kiểu.

// Lựa chọn A: suppress và cast (chỉ khi bạn chắc chắn về API contract)
@SuppressWarnings("unchecked")
List<String> items = (List<String>) legacyApi.getItems();

// Lựa chọn B: copy có kiểm tra kiểu (an toàn hơn khi API không đáng tin)
List<String> items = new ArrayList<>();
for (Object obj : legacyApi.getItems()) {
    if (obj instanceof String) {
        items.add((String) obj);
    }
    // bỏ qua hoặc log các kiểu không mong đợi
}

Lựa chọn B chậm hơn nhưng không bao giờ ném exception. Dùng khi nguồn dữ liệu là bên ngoài hoặc không đáng tin cậy.

Đọc Stack Trace

Đừng bỏ qua thông báo exception — nó rất chính xác:

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
    at com.example.MyService.processConfig(MyService.java:42)
    at com.example.MyService.init(MyService.java:18)

  • Dòng 42 trong MyService.java — đó là nơi cast thất bại. Bắt đầu điều tra từ đây.
  • Thông báo cho bạn biết kiểu bạn thực sự có (String) so với kiểu bạn yêu cầu (Integer).
  • Truy ngược từ dòng đó để tìm nơi giá trị đi vào collection hoặc được trả về bởi một method. Đó là nơi kiểu dữ liệu bị sai.

Kiểm Tra Sau Khi Sửa

Ba bước kiểm tra trước khi kết luận đã xong:

  • Biên dịch với javac -Xlint:unchecked MyClass.java — không có cảnh báo unchecked cast nào nghĩa là bạn đã sạch.
  • Chạy unit test với các input biên: chuỗi ở chỗ cần số nguyên, null, và collection có kiểu hỗn hợp.
  • Nếu lỗi nằm trong data pipeline, hãy viết test truyền dữ liệu thô thực tế — không phải mock, mà là dữ liệu thực — qua đường dẫn parsing.
@Test
void testConfigParsing() {
    Map<String, Object> config = Map.of("timeout", "30"); // String, không phải Integer
    assertDoesNotThrow(() -> MyService.parseTimeout(config));
    assertEquals(30, MyService.parseTimeout(config));
}

Mẹo Nhanh

  • Bật cảnh báo compiler: javac -Xlint:all, hoặc cấu hình IDE để đánh dấu các unchecked cast. Đừng coi chúng là nhiễu — mỗi cảnh báo là một crash tiềm năng lúc runtime.
  • Chỉ trả về Object khi không còn lựa chọn nào khác: nếu một method trả về Object, đó là tín hiệu thiết kế đáng xem xét lại. Generics hoặc sealed interface thường giải quyết vấn đề này gọn gàng hơn.
  • Dùng Number làm kiểu cầu nối: khi bạn không biết một giá trị số là Integer, Long, hay Double, hãy cast sang Number trước, rồi gọi .intValue(). Bao phủ cả ba trường hợp.
  • Cân nhắc dùng container có kiểu cho map hỗn hợp: nếu bạn thực sự cần các giá trị không đồng nhất trong một map, ClassToInstanceMap của Guava được tạo ra đúng cho mục đích này. Nó đảm bảo type safety ở cấp độ container thay vì tại từng điểm get.

Related Error Notes