1. UTF-8 — “vua” của các bảng mã hiện đại
Trong công việc, bạn sẽ gặp tệp, giao thức mạng, cơ sở dữ liệu, dịch vụ web sử dụng các bảng mã khác nhau. Nếu bạn không biết UTF-8 khác ISO-8859-1 như thế nào, rất dễ rơi vào bẫy: mã của bạn chạy “trên máy tôi thì được”, nhưng sẽ hỏng trên máy đồng nghiệp ở Pháp hoặc trên đám mây nơi hệ thống mặc định khác.
Hôm nay chúng ta sẽ phân tích ba bảng mã chính thường gặp trong thực tế của nhà phát triển Java:
- UTF-8 — tiêu chuẩn hiện đại, phổ quát.
- UTF-16 — định dạng nội bộ của chuỗi trong Java và thường gặp trong Windows.
- ISO-8859-1 (Latin-1) — bảng mã một byte cũ nhưng vẫn còn thấy trong các ngôn ngữ Tây Âu.
Hãy cùng tìm hiểu kỹ hơn!
Đó là gì?
UTF-8 (Unicode Transformation Format, 8-bit) là bảng mã có thể biểu diễn mọi ký tự trong bảng Unicode khổng lồ (bao gồm chữ Cyrillic, chữ Hán, emoji, và cả các hệ chữ cổ hiếm). Cách làm rất tinh tế: với các ký tự phổ biến nhất (chữ cái tiếng Anh, chữ số, dấu câu) chỉ dùng 1 byte. Với các ký tự khác — 2, 3 hoặc thậm chí 4 byte.
Thực tế: Hầu hết các trang web hiện đại, đa số tệp JSON và XML, và thậm chí mã nguồn Java đều mặc định dùng UTF-8.
Một ký tự cần bao nhiêu byte?
- Ký tự ASCII (chữ cái tiếng Anh, chữ số, dấu câu) — 1 byte.
- Ký tự chữ Cyrillic, ký tự châu Âu có dấu — 2 byte.
- Chữ tượng hình Trung, Nhật, Hàn — 3 byte.
- Ký tự siêu hiếm, emoji — 4 byte.
Ví dụ:
| Ký tự | Mã trong Unicode | UTF-8 (theo byte) |
|---|---|---|
| A | |
|
| Ya | |
|
| € | |
|
| 😀 | |
|
Vì sao UTF-8 tuyệt vời?
- Tương thích ngược với ASCII: nếu tệp chỉ chứa ký tự tiếng Anh, nó sẽ giống hệt như trong ASCII.
- Gọn nhẹ: với văn bản ngắn tiếng Anh, tệp UTF-8 sẽ có kích thước nhỏ nhất.
- Tính quốc tế: hỗ trợ mọi ngôn ngữ trên thế giới, kể cả emoji.
- Tiêu chuẩn cho web: HTML, CSS, JSON, XML, JavaScript — tất cả mặc định là UTF-8.
Ví dụ sử dụng trong Java
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
public class Utf8Demo {
public static void main(String[] args) throws Exception {
// Ghi chuỗi vào tệp bằng UTF-8
String text = "Xin chào, thế giới! 😀";
Files.write(Paths.get("utf8.txt"), text.getBytes(StandardCharsets.UTF_8));
// Đọc tệp bằng UTF-8
String read = Files.readString(Paths.get("utf8.txt"), StandardCharsets.UTF_8);
System.out.println(read); // Sẽ in: Xin chào, thế giới! 😀
}
}
Ở đây, trước khi ghi, chúng ta mã hóa chuỗi rõ ràng sang UTF-8, và khi đọc cũng chỉ rõ bảng mã. Nhờ vậy văn bản được lưu và khôi phục chính xác, kể cả khi có emoji hoặc ký tự từ các ngôn ngữ khác nhau.
Khi nào dùng UTF-8?
Luôn luôn, trừ khi bạn có lý do thật sự thuyết phục để chọn thứ khác. Đây là lựa chọn phổ quát cho mọi ứng dụng hiện đại.
2. UTF-16 — định dạng nội bộ của Java
UTF-16 là một bảng mã khác trong họ Unicode. Khác với UTF-8, nó thường dùng 2 byte cho mỗi ký tự. Java lưu trữ chuỗi (String) và ký tự (char) trong JVM theo cách này. Nhưng có một điểm: một số ký tự (ví dụ, các chữ tượng hình hiếm hoặc emoji) cần 4 byte (hai “cặp surrogate”).
UTF-16 dùng ở đâu?
- Bên trong Java: tất cả chuỗi và ký tự là UTF-16.
- Windows: nhiều tệp hệ thống trong Windows dùng UTF-16 (ví dụ, Notepad mặc định lưu UTF-16 LE).
- Một số giao thức và định dạng: ví dụ, XML có thể ở dạng UTF-16.
Ví dụ: chuỗi trông như thế nào trong UTF-16
| Ký tự | Mã trong Unicode | UTF-16 (byte) | Ghi chú |
|---|---|---|---|
| A | |
|
2 byte |
| Ya | |
|
2 byte |
| € | |
|
2 byte |
| 😀 | |
|
4 byte (cặp surrogate) |
Ví dụ sử dụng trong Java
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
public class Utf16Demo {
public static void main(String[] args) throws Exception {
String text = "Xin chào, thế giới! 😀";
// Ghi chuỗi vào tệp với bảng mã UTF-16
Files.write(Paths.get("utf16.txt"), text.getBytes(StandardCharsets.UTF_16));
// Đọc nội dung tệp với bảng mã UTF-16
String read = Files.readString(Paths.get("utf16.txt"), StandardCharsets.UTF_16);
System.out.println(read); // In chính xác chuỗi gốc
}
}
Điểm mấu chốt: khi ghi và đọc phải dùng cùng một bảng mã. Nếu ghi bằng UTF-16 nhưng đọc theo mặc định (ví dụ UTF-8), bạn sẽ nhận được mớ ký tự rác.
Đặc điểm của UTF-16
- Độ dài cố định cho phần lớn ký tự: 2 byte.
- Cặp surrogate: một phần ký tự (ví dụ emoji) cần 4 byte.
- Byte Order Mark (BOM): đầu tệp thường có “header” chỉ thứ tự byte (LE — little endian, BE — big endian). Java thường tự nhận BOM.
- Không tương thích với ASCII: nếu mở tệp UTF-16 trong trình soạn thảo đang mong đợi ASCII hoặc UTF-8, bạn sẽ thấy rất nhiều số 0 và ký tự lạ.
Khi nào dùng UTF-16?
- Nếu bạn làm việc với các hệ thống hay tệp đặc thù yêu cầu đúng UTF-16 (ví dụ tích hợp với chương trình Windows).
- Với ứng dụng và tệp thông thường — nên ưu tiên UTF-8.
4. ISO-8859-1 (Latin-1) — lời chào từ quá khứ
ISO-8859-1, hay Latin-1, là bảng mã một byte ra đời từ thập niên 80. Nó hỗ trợ 256 ký tự: bảng chữ cái tiếng Anh, các chữ cái Tây Âu có dấu (é, ü, ç, v.v.), dấu câu, ký tự đặc biệt.
Thực tế: Bảng mã này không có chữ Cyrillic, chữ Hy Lạp, Ả Rập, Trung Quốc và các ký tự phi Latin khác.
Gặp ISO-8859-1 ở đâu?
- Trong các tệp và chương trình cũ (đặc biệt ở châu Âu).
- Trong một số cơ sở dữ liệu và giao thức “mặc định”.
- Trong header HTTP (theo chuẩn, dữ liệu văn bản không ghi rõ bảng mã được coi là ISO-8859-1, dù thực tế hiếm gặp).
Ví dụ: chuỗi trông như thế nào trong ISO-8859-1
| Ký tự | Mã trong Unicode | ISO-8859-1 (byte) | Ghi chú |
|---|---|---|---|
| A | |
|
Giống như trong ASCII |
| é | |
|
Tiếng Pháp |
| ü | |
|
Tiếng Đức |
| Ya | |
— | Không có ký tự này! |
Ví dụ sử dụng trong Java
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.charset.Charset;
public class Latin1Demo {
public static void main(String[] args) throws Exception {
String text = "Bonjour, ça va? Café!"; // Văn bản tiếng Pháp
Files.write(Paths.get("latin1.txt"), text.getBytes(StandardCharsets.ISO_8859_1));
// Đọc tệp bằng ISO-8859-1
String read = Files.readString(Paths.get("latin1.txt"), StandardCharsets.ISO_8859_1);
System.out.println(read); // Mọi thứ được đọc chính xác!
// Thử ghi chữ Cyrillic
String cyrillic = "Xin chào, thế giới!";
try {
Files.write(Paths.get("badlatin1.txt"), cyrillic.getBytes(StandardCharsets.ISO_8859_1));
} catch (Exception e) {
System.out.println("Lỗi: không thể ghi chữ Cyrillic vào ISO-8859-1!");
}
}
}
Bảng mã ISO-8859-1 (Latin-1) chỉ hỗ trợ các ký tự Tây Âu — ví dụ, các chữ cái có dấu trong tiếng Pháp, Đức hoặc Tây Ban Nha. Vì vậy văn bản tiếng Pháp được ghi và đọc không vấn đề.
Tuy nhiên, chữ Cyrillic không có trong bảng mã này; khi cố gắng ghi sẽ xảy ra lỗi hoặc mất ký tự. Đó là lý do với văn bản đa ngôn ngữ, nên dùng UTF-8 hoặc UTF-16, vốn bao phủ bộ ký tự lớn hơn rất nhiều.
Đặc điểm của ISO-8859-1
- Giới hạn theo ngôn ngữ: chỉ các ngôn ngữ Tây Âu.
- 1 byte mỗi ký tự: gọn nhưng rất hạn chế.
- Ghi ký tự “không thuộc bảng” sẽ bị méo: nếu cố lưu chữ Cyrillic hay chữ tượng hình, chúng sẽ thành dấu hỏi hoặc rác.
Khi nào dùng ISO-8859-1?
- Chỉ khi bạn tích hợp với hệ thống hoặc cơ sở dữ liệu rất cũ yêu cầu đúng bảng mã này.
- Với nhu cầu hiện đại — không khuyến nghị.
5. Những lưu ý hữu ích
Bảng so sánh các bảng mã
| Bảng mã | Byte mỗi ký tự | Ngôn ngữ hỗ trợ | Tương thích với ASCII | Nơi sử dụng |
|---|---|---|---|---|
|
1-4 | Tất cả | Đầy đủ | Web, Java, JSON, XML, Linux |
|
2 hoặc 4 | Tất cả | Không | Bên trong Java, Windows, XML |
|
1 | Tây Âu | Đầy đủ | Ứng dụng cũ, cơ sở dữ liệu, HTTP |
Chọn bảng mã như thế nào?
- UTF-8 — lựa chọn cho 99% trường hợp. Tương thích với ASCII, hỗ trợ mọi ngôn ngữ, kích thước tối thiểu cho văn bản tiếng Anh, tiêu chuẩn cho web và Java.
- UTF-16 — chỉ dùng nếu được đặc tả yêu cầu hoặc khi tích hợp với chương trình Windows.
- ISO-8859-1 — chỉ để tương thích ngược với hệ thống cũ. Không bao giờ dùng để lưu chữ Cyrillic, Hy Lạp, Ả Rập, v.v.
Cách biết tệp đang dùng bảng mã nào?
- Mở tệp trong trình soạn thảo có hiển thị bảng mã (ví dụ, Notepad++, VS Code).
- Nếu thấy “ký tự rác”, hãy thử mở tệp với bảng mã khác.
- Trong terminal Linux, có thể dùng lệnh file tên_tệp — đôi khi nó gợi ý được bảng mã.
Cách đặt bảng mã trong Java?
Dùng các lớp trong gói java.nio.charset:
- StandardCharsets.UTF_8
- StandardCharsets.UTF_16
- StandardCharsets.ISO_8859_1
Hoặc qua tên bảng mã:
Charset windows1251 = Charset.forName("Windows-1251");
Charset utf8 = Charset.forName("UTF-8");
6. Thực hành: điều gì xảy ra nếu nhầm bảng mã?
Hãy thử một thí nghiệm nhỏ. Ghi chuỗi bằng UTF-8, rồi thử đọc nó như ISO-8859-1:
import java.nio.file.*;
import java.nio.charset.*;
public class EncodingMismatchDemo {
public static void main(String[] args) throws Exception {
String text = "Xin chào, thế giới!"; // Chữ Cyrillic (đã thay bằng tiếng Việt để minh họa)
// Ghi như UTF-8
Files.write(Paths.get("utf8demo.txt"), text.getBytes(StandardCharsets.UTF_8));
// Đọc như ISO-8859-1 (SAI!)
String wrong = Files.readString(Paths.get("utf8demo.txt"), StandardCharsets.ISO_8859_1);
System.out.println("Đọc sai: " + wrong);
// Bây giờ đọc đúng
String correct = Files.readString(Paths.get("utf8demo.txt"), StandardCharsets.UTF_8);
System.out.println("Đọc đúng: " + correct);
}
}
Kết quả:
- Trường hợp đầu bạn sẽ thấy “ký tự rác” — một loạt ký tự kỳ lạ.
- Trường hợp thứ hai — văn bản tiếng Nga bình thường.
7. Lỗi thường gặp khi làm việc với bảng mã
Lỗi số 1: Đọc tệp với bảng mã không đúng. Nếu tệp được ghi bằng UTF-8 nhưng bạn đọc nó như ISO-8859-1 hoặc Windows-1251, mọi ký tự ngoài ASCII sẽ thành rác. Đây là “kinh điển”: “trên máy tôi chạy được, còn trên máy đồng nghiệp thì không”.
Lỗi số 2: Cố ghi ký tự “không thuộc bảng” vào bảng mã một byte. Nếu bạn cố lưu chữ Cyrillic hoặc chữ tượng hình bằng ISO-8859-1, Java sẽ thay bằng dấu hỏi hoặc phát sinh lỗi. Dữ liệu sẽ mất không thể phục hồi.
Lỗi số 3: Dùng bảng mã hệ thống “mặc định”. Trong một số phương thức Java (ví dụ, new FileReader("file.txt")) bảng mã không được chỉ rõ — hệ thống mặc định sẽ được dùng. Trên một máy có thể là UTF-8, trên máy khác là Windows-1251, trên máy thứ ba — thậm chí thứ gì đó lạ. Vì vậy hãy luôn dùng phương thức cho phép chỉ rõ bảng mã.
Lỗi số 4: Mở tệp UTF-16 như UTF-8. Tệp được ghi bằng UTF-16 mà mở như UTF-8 sẽ đầy số 0 và ký tự lạ, vì byte bị diễn giải sai.
GO TO FULL VERSION