CodeGym /Các khóa học /JAVA 25 SELF /Các vấn đề lệch mã hóa, lỗi thường gặp

Các vấn đề lệch mã hóa, lỗi thường gặp

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Triệu chứng lỗi

Trong thế giới lý tưởng, lập trình viên luôn biết tệp được lưu bằng mã hóa nào và chỉ định đúng khi đọc. Nhưng thực tế — tệp di chuyển giữa Windows, Linux, máy chủ, trình soạn thảo, và mỗi bên lại diễn giải byte theo cách riêng. Kết quả là ta gặp các triệu chứng sau:

  • “Mojibake” — thay vì văn bản mong đợi, ta thấy ký tự lạ, dấu hỏi, ô vuông hoặc một chuỗi chữ cái không giống bất kỳ ngôn ngữ nào.
  • Mất ký tự — một phần văn bản biến mất hoặc bị thay bằng ?.
  • Ngoại lệ — ví dụ MalformedInputException, khi Java không thể “tiêu hóa” các byte theo mã hóa đã chọn.
  • Lỗi khi phân tích cú pháp — chương trình không thể xử lý đúng tệp vì từ khóa hoặc cấu trúc bị hỏng do văn bản bị méo.

Đây là ví dụ kinh điển về “mojibake” khi đọc tệp chữ Cyrillic bằng sai mã hóa:

Kỳ vọng:  Privet, mir!
Nhận được: Привет, мир

Đây không phải là ngôn ngữ mới, mà là kết quả của việc các byte bị diễn giải bằng “từ điển” không đúng.

2. Vì sao xảy ra lỗi: căn nguyên

Tệp được ghi bằng một mã hóa nhưng đọc bằng mã hóa khác

Giả sử ai đó lưu tệp ở Windows-1251, còn bạn mở nó bằng UTF-8. Java sẽ cố giải mã byte theo quy tắc của UTF-8, nhưng sẽ thành vô nghĩa vì giá trị byte không khớp với kỳ vọng.

Dùng mã hóa hệ thống “mặc định”

Nếu bạn không chỉ định mã hóa tường minh, Java sẽ dùng mã hóa hệ thống — tức mã đang được cài trên máy của bạn. Trên Windows với locale tiếng Nga có thể là Windows-1251, trên Linux — UTF-8, trên Mac — cũng UTF-8. Một tệp mở rất ổn trên máy bạn có thể trở nên không đọc được trên máy đồng nghiệp dùng hệ điều hành khác.

Dùng các constructor lỗi thời

Trong các phiên bản Java cũ (và một số giáo trình) thường xuất hiện các mẫu dùng FileReader/FileWriter, vốn sử dụng mã hóa hệ thống và không cho bạn quyền kiểm soát — đây là cái bẫy và là nguồn gốc của “mojibake”.

FileReader reader = new FileReader("file.txt");
FileWriter writer = new FileWriter("file.txt");

Có hoặc không có BOM (Byte Order Mark)

Một số mã hóa (ví dụ, UTF-8 với BOM hoặc UTF-16) thêm các byte đặc biệt vào đầu tệp để báo hiệu bản chất của chúng. Nếu chương trình không mong đợi BOM hoặc ngược lại — trông đợi nhưng không thấy, có thể phát sinh vấn đề: hoặc các ký tự đầu tệp bị méo, hoặc tệp không được nhận diện.

3. Lỗi biểu hiện như thế nào: phân tích thực tế

Ví dụ 1: Tệp chứa Cyrillic, ghi bằng Windows-1251, được đọc như UTF-8

import java.nio.file.*;
import java.nio.charset.*;

public class EncodingDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("russian.txt");
        // Tệp ghi bằng Windows-1251, đọc như UTF-8 — sẽ bị mojibake!
        try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
            System.out.println(reader.readLine());
        }
    }
}

Kết quả, thay vì “Privet, mir!”, bạn sẽ thấy một loạt ký tự lạ.

Ví dụ 2: Tệp ghi bằng UTF-8, đọc như ISO-8859-1

try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.ISO_8859_1)) {
    System.out.println(reader.readLine());
}

Kết quả: Mọi ký tự không phải ASCII sẽ biến thành rác hoặc bị thay bằng ?.

Ví dụ 3: Ngoại lệ khi đọc tệp

Nếu các byte không phù hợp với quy tắc của mã hóa đã chọn, Java có thể ném ngoại lệ:

Exception in thread "main" java.nio.charset.MalformedInputException: Input length = 1
    at java.base/sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
    ...

Điều này có nghĩa Java đã gặp một byte không thể được diễn giải hợp lệ theo mã hóa đã chọn.

4. Chẩn đoán: làm sao biết có vấn đề với mã hóa

Kiểm tra mã hóa của tệp

  • Trong các trình soạn thảo như Notepad++, VS Code hoặc Sublime Text, bạn thường có thể xem hoặc đổi mã hóa của tệp (thường ở thanh dưới).
  • Trên Linux, lệnh sau có thể gợi ý về mã hóa (nhưng không phải lúc nào cũng chính xác 100%):
file imya_faila.txt

Kiểm tra mã hóa hệ thống của Java

In ra console giá trị thuộc tính file.encoding:

System.out.println(System.getProperty("file.encoding"));

Sử dụng dữ liệu thử nghiệm

Tạo một tệp nhỏ với nhiều loại ký tự (Cyrillic, Latin, ký tự đặc biệt, emoji), thử đọc nó với các mã hóa khác nhau và xem khi nào kết quả khớp với kỳ vọng.

Luôn chỉ định mã hóa một cách tường minh

Ngay khi bạn thấy trong mã có thao tác đọc/ghi tệp mà không chỉ định mã hóa — hãy cảnh giác. Ví dụ, dùng Files.newBufferedReader(..., StandardCharsets.UTF_8) thay vì dựa vào “mặc định”.

5. Best practices: làm sao không rơi vào bẫy

Quy tắc №1:
LUÔN LUÔN chỉ định rõ mã hóa khi làm việc với tệp, đặc biệt nếu tệp sẽ được dùng trên nhiều máy, nhiều hệ điều hành hoặc gửi qua mạng.

Quy tắc №2:
Hãy dùng các mã hóa hiện đại, “dễ hiểu” — trước hết là UTF-8 (StandardCharsets.UTF_8). Chỉ khi có yêu cầu đặc biệt (ví dụ tích hợp với hệ thống cũ) mới sử dụng các mã hóa khác.

Quy tắc №3:
Tránh dùng các lớp FileReaderFileWriter (chúng không cho phép chỉ định mã hóa), thay vào đó dùng InputStreamReader, OutputStreamWriter hoặc các phương thức trong Files với Charset tường minh.

Quy tắc №4:
Hãy kiểm tra kết quả! Mở các tệp đã ghi trong trình soạn thảo hỗ trợ nhiều mã hóa để bảo đảm văn bản hiển thị đúng.

6. Đặc thù và lưu ý: BOM, XML, JSON và các trường hợp “oái oăm” khác

BOM (Byte Order Mark): đôi khi tệp UTF-8 bắt đầu bằng các byte “vô hình” (EF BB BF). Phần lớn chương trình hiện đại bỏ qua chúng, nhưng một số có thể hiển thị “mojibake” ở đầu dòng hoặc không chấp nhận tệp (ví dụ, các bộ phân tích XML/JSON cũ).

XML/HTML: đôi khi đầu tệp có dòng như <?xml version="1.0" encoding="UTF-8"?>. Dòng này cho chương trình biết nên kỳ vọng mã hóa nào. Nhưng nếu mã hóa thực tế không khớp với khai báo — lại “mojibake”.

JSON: theo tiêu chuẩn phải là UTF-8, nhưng nếu tệp được tạo bằng Windows-1251, bộ phân tích sẽ báo lỗi hoặc cho dữ liệu bị méo.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION