Backend

99 Ngày Java — Ngày 05: Ép kiểu (casting)

SSite Admin
1 tháng 08, 2026 5 phút đọc 100 lượt xem
99 Ngày Java — Ngày 05: Ép kiểu (casting)

Hôm qua ta để lại một câu hỏi treo: chuyện gì xảy ra khi các kiểu khác nhau gặp nhau trong một biểu thức? Hôm nay trả lời trọn vẹn bằng ép kiểu (casting): khi nào Java tự đổi kiểu giúp bạn (widening), khi nào bạn phải tự tay ép và tự chịu rủi ro mất dữ liệu (narrowing), và mối quan hệ đặc biệt giữa char với int.

Sketchnote Ngày 05: Ép kiểu widening và narrowing trong Java

Widening — nước chảy chỗ trũng, tự động và an toàn

Đổ nước từ ly nhỏ sang ly to thì không bao giờ tràn — đưa giá trị từ kiểu hẹp sang kiểu rộng hơn cũng vậy, nên Java làm tự động, không cần hỏi:

int i = 100;
long l = i;       // int → long: tự động, luôn an toàn
double d = l;     // long → double: tự động

// Chuỗi mở rộng đầy đủ:
// byte → short → int → long → float → double
//         char → int

Đây chính là lý do 7.0 / 2 hôm qua ra 3.5: thấy một vế là double, Java lặng lẽ thăng cấp (promote) vế còn lại lên double trước khi tính. Mọi phép toán số học đều thăng cấp toán hạng lên ít nhất int — chi tiết này sẽ quay lại ở phần bẫy bên dưới.

Narrowing — phải cast tường minh, và có giá

Chiều ngược lại — ly to đổ vào ly nhỏ — có thể tràn, nên Java bắt bạn ký giấy cam kết: cú pháp (kiểu_đích) giá_trị. Chữ ký đó nghĩa là: "tôi hiểu dữ liệu có thể mất, cứ làm đi":

double price = 9.99;
int rounded = (int) price;     // 9 — phần thập phân bị CẮT, không làm tròn!

long big = 3_000_000_000L;
int oops = (int) big;          // -1294967296 — tràn số lặng lẽ!

byte tiny = (byte) 130;        // -126 — vượt phạm vi byte là quay vòng
  • doubleint: phần thập phân bị cắt bỏ (truncate), không phải làm tròn — muốn làm tròn dùng Math.round().

  • longint, intbyte: giá trị vượt phạm vi thì quay vòng như overflow Ngày 03 — không hề có cảnh báo runtime.

  • Cast chỉ đổi cách đọc bit, không kiểm tra tính hợp lệ — trách nhiệm nằm ở người viết.

char ↔ int — ký tự cũng chỉ là con số

char letter = 'A';
int code = letter;             // 65 — char lên int: tự động
System.out.println('A' + 1);   // 66 — phép cộng đẩy char thành int!

char next = (char) ('A' + 1);  // 'B' — muốn quay về char phải cast
char viet = 'ế';
System.out.println((int) viet); // 7871 — Unicode tiếng Việt vẫn là số

Mỗi char là một mã Unicode 16-bit, nên char lên int là widening tự nhiên. Thủ thuật đáng nhớ: 'z' - 'a' ra khoảng cách 25 giữa hai chữ cái, (char) (c + 1) ra ký tự kế tiếp — nền tảng của các bài toán xử lý chuỗi mà ta sẽ gặp từ Ngày 09.

Dòng code trên màn hình — nơi các kiểu dữ liệu liên tục biến hình

Ba cái bẫy gặp hoài trong code thật

// Bẫy 1: chia nguyên rồi mới ép — quá muộn!
int total = 7, count = 2;
double avg1 = total / count;          // 3.0  — 7/2 đã ra 3 trước khi lên double
double avg2 = (double) total / count; // 3.5  — ép 1 vế TRƯỚC khi chia ✓

// Bẫy 2: byte + byte là... int
byte a = 10, b = 20;
// byte sum = a + b;                  // ❌ lỗi compile: a + b là int
byte sum = (byte) (a + b);            // phải tự cast — và tự chịu overflow

// Bẫy 3: float nhận long ngon lành (widening!) nhưng mất độ chính xác
long exact = 123_456_789_012_345_678L;
float f = exact;                      // 1.23456788E17 — trôi mất đuôi!

Bẫy 1 là vua của mọi bug tính trung bình: cast phải xảy ra trước phép chia, không phải sau. Bẫy 2 đến từ quy tắc thăng cấp lên int — và cũng giải thích chuyện hôm qua: b += 1 chạy được vì toán tử gán kết hợp tự cast ngầm hộ bạn. Bẫy 3 tinh vi nhất: longfloat là widening hợp lệ nhưng float chỉ có ~7 chữ số chính xác — số lớn sẽ trôi mất đuôi mà compiler không nói một lời.

Bài tập nhỏ

  • Đoán rồi chạy: (int) 9.99, (byte) 200, (char) 66, 'A' + 2.

  • Viết công thức tính điểm trung bình của int soLanThi = 3int tongDiem = 25 sao cho ra 8.33 chứ không phải 8.0.

  • In bảng chữ cái A–Z bằng một vòng lặp duy nhất dùng char (gợi ý: Ngày 07 học vòng lặp, hôm nay cứ thử while theo trực giác).

  • Giải thích vì sao float f = 3_000_000_000L; compile được nhưng int i = 3_000_000_000L; thì không.

Kết luận

Bạn đã phân biệt widening tự động với narrowing phải ký cam kết, biết cast cắt chứ không làm tròn, hiểu char chỉ là mã Unicode và thuộc lòng 3 cái bẫy kinh điển. Ngày 06, code bắt đầu biết rẽ nhánh: if–elseswitch — gồm cả switch expression đời mới của Java. Hẹn gặp lại!

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Spring — Ngày 14: Bean Validation — @Valid & bộ constraint chuẩn

Ngày 14 của 99 Ngày Spring: Bean Validation khai báo luật ngay trên DTO — @Valid kích hoạt, bộ constraint chuẩn (@NotBlank vs @NotEmpty vs @NotNull, @Size, @Email, @Min/@Max), bẫy int vs Integer, validate object lồng nhau không tự lan, và lỗi 400 gom một lượt.

10 thg 8, 20265 phút0
99 Ngày Java — Ngày 14: Kế thừa (Inheritance)

Ngày 14 của 99 Ngày Java: extends trao gia tài từ lớp cha, override với @Override và super., thứ tự khai sinh cha trước con sau qua super(...), protected trả nợ Ngày 13, và phép thử is-a vs has-a — khi nào nên composition thay vì kế thừa.

10 thg 8, 20266 phút0
99 Ngày Spring — Ngày 13: @RequestBody & DTO — đừng công khai entity ra API

Ngày 13 của 99 Ngày Spring: @RequestBody + Jackson deserialize JSON thành object, DTO là hợp đồng API — chặn lỗ hổng mass assignment chiều vào và rò rỉ dữ liệu chiều ra, request/response DTO khác nhau là bình thường, và Jackson annotations tinh chỉnh hợp đồng JSON.

9 thg 8, 20266 phút9