Thay vì phải chuyển đổi kiểu dữ liệu và nối nhiều phần lại với nhau, Java String Format cung cấp một cú pháp gọn gàng, an toàn hơn về kiểu dữ liệu (type-safe), và dễ đọc hơn để chèn (insert) và điều chỉnh cách hiển thị (format) các giá trị vào vị trí được đánh dấu sẵn trong chuỗi.
Đọc bài viết để hiểu thêm về:
- Phương pháp cổ điển và phổ biến
- Định dạng cho I/O:
System.out.printf() - Định dạng hiện đại (Java 15+): Text Blocks
- Phương pháp hiệu suất cao:
MessageFormat
Đọc chi tiết: Java là gì? Tất cả những điều bạn cần biết về ngôn ngữ Java
Java String Format là gì?
Về cơ bản, Java String Format là một cơ chế cho phép bạn tạo ra một chuỗi mới (String) dựa trên một chuỗi định dạng (format string) cụ thể và một tập hợp các đối số đầu vào.
Cơ chế này được thiết kế để giải quyết vấn đề cố hữu của việc nối chuỗi phức tạp và thủ công (ví dụ: sử dụng toán tử $`+`$), đặc biệt khi bạn cần chèn nhiều biến có kiểu dữ liệu khác nhau (như số nguyên, số thực, ngày) vào một khuôn mẫu chuỗi cố định.
Phương pháp cổ điển và phổ biến: $`String.format()`$
Phương thức tĩnh $`String.format()`$ là cách thức tiêu chuẩn và được sử dụng rộng rãi nhất trong Java để thực hiện việc định dạng chuỗi. Đây là công cụ không thể thiếu để tạo ra các chuỗi đầu ra có cấu trúc một cách linh hoạt và dễ kiểm soát.
Nguyên tắc hoạt động của String.format()
Cơ chế hoạt động của $`String.format()`$ được xây dựng dựa trên khái niệm chuỗi định dạng (format string), một chuỗi chứa văn bản cố định kết hợp với các chỉ định định dạng (format specifiers) bắt đầu bằng ký tự phần trăm ($`%`$).
Phương thức này lấy cảm hứng trực tiếp từ hàm $`printf()`$ phổ biến trong ngôn ngữ C. Nó hoạt động bằng cách thay thế các chỉ định định dạng ($`%s`$, $`%d`$, $`%f`$, v.v.) trong chuỗi định dạng bằng các đối số thực tế được truyền vào.
Lưu ý: Về mặt kỹ thuật, $`String.format()`$ sử dụng lớp java.util.Formatter ẩn danh để thực hiện công việc định dạng, và nó hoàn toàn tương đương với việc sử dụng $`System.out.printf()`$ hoặc $`PrintStream.printf()`$, chỉ khác là $`String.format()`$ trả về một chuỗi thay vì in trực tiếp ra console.
Các chỉ định định dạng cơ bản
Các chỉ định định dạng được sử dụng để xác định kiểu dữ liệu của đối số và cách thức hiển thị của nó.
Cú pháp chung là:
$`%[flags][width][.precision]conversion`$
%là một ký tự đặc biệt biểu thị rằng một hướng dẫn định dạng sau.- [argument index] xác định chỉ số của các đối số để được định dạng. Nếu không xác định cụ thể, các đối số sẽ được định dạng theo thứ tự như chúng xuất hiện trong danh sách đối số.
- [flag] là một hướng dẫn định dạng đặc biệt. Ví dụ, cờ + xác định rằng một giá trị số phải luôn luôn được định dạng với một ký hiệu, và cờ 0 chỉ định rằng 0 là ký tự đệm. Các cờ khác bao gồm – đó là pad ở bên phải, + pad ở bên trái (nếu đối tượng được định dạng là một chuỗi). Lưu ý rằng một số cờ không thể được kết hợp với một số cờ khác hoặc với các đối tượng được định dạng nhất định.
- [width] xác định số lượng tối thiểu các ký tự output cho đối tượng đó.
- [.precession] xác định chính xác của các số dấu chấm động trong output. Đó là cơ bản số chữ số thập phân bạn muốn in trên đầu ra. Nhưng nó có thể được sử dụng cho các loại khác để cắt giảm chiều rộng đầu ra
- type : type và % là các tham số định dạng bắt buộc duy nhất. type là kiểu đối tượng sẽ được định dạng trong đầu ra. Đối với các số nguyên
d, cho các chuỗi đó làs, cho các số dấu phẩy làf, cho các số nguyên có định dạng hex làx.
Định dạng số nguyên (%d)
Bạn có thể kiểm soát độ rộng và cách đệm (padding) cho số nguyên.
- Độ rộng tối thiểu:
$`%5d`$(đảm bảo số chiếm ít nhất 5 ký tự). - Thêm số 0 đầu:
$`%05d`$(thêm số 0 để đạt độ rộng tối thiểu).
// Ví dụ: Định dạng số 42 với độ rộng 5, đệm bằng số 0
String result = String.format("Mã số: %05d", 42);
// Output: "Mã số: 00042"
Định dạng số thực (%f)
Bạn có thể kiểm soát số chữ số sau dấu thập phân (độ chính xác). Độ chính xác: $`%.2f`$ (làm tròn và hiển thị 2 chữ số sau dấu phẩy).
// Ví dụ: Định dạng số Pi với 3 chữ số thập phân
String result = String.format("Pi là: %.3f", Math.PI);
// Output: "Pi là: 3.142"
Định Dạng Chuỗi (%s) và Căn Lề
Sử dụng cờ (flag) $`-`$ để căn lề trái. Mặc định là căn lề phải.
// Ví dụ: Căn lề trái trong không gian 10 ký tự
String result = String.format("|%-10s|", "Java");
// Output: "|Java |"
Định Dạng Ngày và Giờ (%t / %T)
Để định dạng đối tượng ngày và giờ ($`java.util.Date`$ hoặc $`java.time.ZonedDateTime`$), ta sử dụng chỉ định chuyển đổi %t (cho ký tự thường) hoặc %T (cho ký tự hoa) theo sau là ký tự chuyển đổi ngày giờ cụ thể.
Ta có bảng tổng hợp như sau:
| Ký hiệu | Mô tả |
| %tA | Tên đầy đủ của thứ trong tuần, ví dụ: “Thứ 2”, “Chủ Nhật”. |
| %ta | Tên viết tắt của thứ trong tuần, ví dụ: “T2”, “CN”. |
| %tB | Tên đầy đủ của tháng, ví dụ: “January”, “February”. |
| %tb | Tên viết tắt của tháng, ví dụ: “Jan”, “Feb”. |
| %tC | Phần thế kỷ của năm, gồm 2 chữ số, ví dụ: “00” đến “99”. |
| %tc | Ngày giờ đầy đủ theo định dạng locale, tương đương %ta %tb %td %tT %tZ %tY.Ví dụ: “Fri Feb 17 07:45:42 PST 2017”. |
| %tD | Ngày theo định dạng %tm/%td/%ty. |
| %td | Ngày trong tháng, định dạng 2 chữ số, ví dụ: “01” đến “31”. |
| %te | Ngày trong tháng, không có số 0 phía trước, ví dụ: “1” đến “31”. |
| %tF | Ngày theo chuẩn ISO 8601, định dạng %tY-%tm-%td. |
| %tH | Giờ trong ngày theo định dạng 24 giờ, có số 0 phía trước, ví dụ: “00” đến “23”. |
| %th | Giống với %tb (tên viết tắt của tháng). |
| %tI | Giờ trong ngày theo định dạng 12 giờ, có số 0 phía trước, ví dụ: “01” đến “12”. |
| %tj | Ngày thứ bao nhiêu trong năm, có số 0 phía trước, ví dụ: “001” đến “366”. |
| %tk | Giờ trong ngày theo định dạng 24 giờ, không có số 0 phía trước, ví dụ: “0” đến “23”. |
| %tl | Giờ trong ngày theo định dạng 12 giờ, không có số 0 phía trước, ví dụ: “1” đến “12”. |
| %tM | Phút trong giờ, có số 0 phía trước, ví dụ: “00” đến “59”. |
| %tm | Tháng, có số 0 phía trước, ví dụ: “01” đến “12”. |
| %tN | Nano giây, gồm 9 chữ số, có số 0 phía trước, ví dụ: “000000000” đến “999999999”. |
| %tp | Ký hiệu “am” hoặc “pm” theo locale. |
| %tQ | Số mili-giây tính từ mốc thời gian Unix (01/01/1970 00:00:00 UTC). |
| %tR | Thời gian theo định dạng 24 giờ, %tH:%tM. |
| %tr | Thời gian theo định dạng 12 giờ, %tI:%tM:%tS %Tp. |
| %tS | Giây trong phút, có số 0 phía trước, “00” đến “60” (60 dùng cho giây nhuận). |
| %ts | Số giây tính từ mốc thời gian Unix (01/01/1970 00:00:00 UTC). |
| %tT | Thời gian theo định dạng 24 giờ, %tH:%tM:%tS. |
| %tY | Năm đầy đủ 4 chữ số, ví dụ: “0000” đến “9999”. |
| %ty | Năm rút gọn 2 chữ số, ví dụ: “00” đến “99”. |
Ví dụ về cách sử dụng:
import java.time.LocalDateTime;
// Ví dụ: Lấy ngày, tháng và năm
LocalDateTime now = LocalDateTime.now();
String date = String.format("Hôm nay là %ta, ngày %td tháng %tm năm %tY",
now, now, now, now);
// Output: "Hôm nay là T2, ngày 17 tháng 11 năm 2025"
Đối số (Argument Index)
Một tính năng mạnh mẽ giúp cải thiện đáng kể khả năng đọc và tái sử dụng đối số là sử dụng chỉ số đối số (Argument Index), được biểu diễn bằng $N$, trong đó $`N`$ là chỉ mục của đối số (bắt đầu từ $1$\):
$`%[argument_index]$[flags][width][.precision]conversion\`$
Tính năng này cho phép bạn:
- Tái sử dụng một đối số nhiều lần mà không cần lặp lại nó trong danh sách tham số.
- Sắp xếp lại đối số trong trường hợp cần hỗ trợ ngôn ngữ khác (ví dụ: trong tiếng Anh, thứ tự tên và tuổi khác với tiếng Việt).
String name = "Minh";
int age = 25;
// Tái sử dụng đối số (Chỉ mục 1$ là 'name')
String result = String.format(
"Tên: %1$s (Lặp lại: %1$s). Tuổi: %2$d.",
name, age
);
// Output: "Tên: Minh (Lặp lại: Minh). Tuổi: 25."
Đây là nền tảng vững chắc để bạn bước vào các kỹ thuật định dạng nâng cao hơn.
Định dạng cho I/O: System.out.printf()
Mặc dù $`String.format()`$ là lựa chọn lý tưởng để tạo ra các chuỗi được định dạng để sử dụng trong logic ứng dụng, System.out.printf() lại là công cụ không thể thiếu khi bạn cần in trực tiếp dữ liệu ra console hoặc luồng đầu ra chuẩn (Standard Output – STDOUT).
Mối quan hệ giữa printf() và String.format()
Hai phương thức này chia sẻ một mối quan hệ cộng sinh vì chúng được xây dựng trên cùng một cơ chế nền tảng:
Cả $`System.out.printf()`$ và $`String.format()`$ đều sử dụng cú pháp định dạng của lớp $`java.util.Formatter`$ (bao gồm các chỉ định $`%s`$, $`%d`$, $`%f`$, $`%t`$, v.v.) mà chúng ta đã tìm hiểu ở phần trước.
Điều này có nghĩa là mọi quy tắc về căn lề, độ chính xác, và chỉ số đối số đều áp dụng cho cả hai.
Điểm khác biệt
String.format(...)trả về một đối tượng$`String`$mới.System.out.printf(...)(thực chất là một phương thức của lớp$`PrintStream`$) in chuỗi đã định dạng trực tiếp ra luồng đầu ra chuẩn$`System.out`$, và trả về đối tượng$`PrintStream`$.
Nói cách khác, $`System.out.printf(format, args)`$ về cơ bản là cách viết ngắn gọn của $`System.out.print(String.format(format, args))`$.
double value = 123.456;
// String.format(): Trả về chuỗi để gán hoặc sử dụng
String formattedString = String.format("Giá trị: $%.2f", value);
// System.out.printf(): In trực tiếp ra console
System.out.printf("Giá trị: $%.2f%n", value);
// %n là chỉ định ngắt dòng (newline) hoạt động trên mọi hệ điều hành
Định dạng hiện đại (Java 15+): Text Blocks
Trước Java 15, việc tạo ra các chuỗi đa dòng (multi-line strings) hoặc các chuỗi chứa ký tự đặc biệt (như JSON, HTML, SQL) rất khó khăn, đòi hỏi phải sử dụng nhiều ký tự thoát $`\n`$, $`\”`$, và phép nối chuỗi $`+`$.
Text Blocks (Chặn Văn Bản) ra đời để giải quyết vấn đề này, cải thiện đáng kể khả năng đọc và viết mã.
Text Blocks là gì?
Text Blocks là một tính năng của Java, được chính thức giới thiệu trong Java 15, cho phép bạn tạo ra một chuỗi đa dòng mà không cần sử dụng ký tự thoát cho các dòng mới ($`\n`$) hoặc dấu ngoặc kép ($`"`$).
Cú pháp và lợi ích:
Text Blocks sử dụng cú pháp ba dấu ngoặc kép mở và đóng: $`"""..."""`$.
- Mở: Bắt đầu bằng ba dấu ngoặc kép ($`”””`$) theo sau là một ký tự xuống dòng (new line).
- Nội dung: Chứa nội dung chuỗi, có thể trải dài trên nhiều dòng.
- Đóng: Kết thúc bằng ba dấu ngoặc kép ($`”””`$).
Lợi ích chính:
- Đơn giản hóa chuỗi đa dòng: Loại bỏ nhu cầu sử dụng $`\n`$ cho mỗi dòng mới.
- Giảm ký tự thoát: Bạn có thể sử dụng dấu ngoặc kép $`”`$ trong nội dung mà không cần phải thoát ($`\”`$).
- Khả năng đọc mã cao: Dễ dàng hình dung cấu trúc đầu ra (như JSON hoặc HTML) ngay trong mã nguồn.
// Trước đây (Java < 15)
String htmlOld = "<html>\n" +
" <body>\n" +
" <h1>Tiêu đề</h1>\n" +
" </body>\n" +
"</html>";
// Với Text Blocks (Java 15+)
String htmlNew = """
<html>
<body>
<h1>Tiêu đề</h1>
</body>
</html>
""";
Kết hợp Text Blocks với String.format()
Sức mạnh thực sự của Text Blocks được phát huy khi nó được kết hợp với phương thức định dạng chuỗi quen thuộc $`String.format()`$.
Cách làm: Sử dụng Text Blocks để tạo khuôn mẫu đa dòng, sau đó chèn các chỉ định định dạng ($`%s`$, $`%d`$, v.v.) vào khuôn mẫu đó. Cuối cùng, áp dụng $`String.format()`$ để điền dữ liệu động vào các vị trí đã đánh dấu.
Ví dụ: Tạo chuỗi JSON phức tạp
Giả sử bạn cần tạo một đối tượng JSON và điền tên sản phẩm, giá, và số lượng:
String product = "Laptop X1";
double price = 1500.99;
int stock = 20;
// Text Block làm khuôn mẫu, sử dụng %s và %.2f
String jsonTemplate = """
{
"productName": "%s",
"details": {
"price": "%.2f",
"stock": %d
},
"status": "Available"
}
""";
// Sử dụng String.format() để điền dữ liệu vào khuôn mẫu
String finalJson = String.format(jsonTemplate, product, price, stock);
System.out.println(finalJson);
Lợi ích: Bằng cách này, bạn giữ được sự rõ ràng của cấu trúc JSON/HTML/SQL và tận dụng khả năng định dạng mạnh mẽ của $`String.format()`$ để kiểm soát độ chính xác của số thực ($`%.2f`$) và các yếu tố khác.
Phương pháp hiệu suất cao: MessageFormat
$`java.text.MessageFormat`$ là một lớp mạnh mẽ được thiết kế để tạo ra các thông báo (messages) được bản địa hóa (localized) và phức tạp. Mặc dù có vẻ ngoài đơn giản, nó là công cụ hàng đầu cho các ứng dụng đa ngôn ngữ, nơi thứ tự và định dạng của các phần tử thay đổi theo ngôn ngữ.
Khi nào nên dùng MessageFormat?
$`MessageFormat`$ thực sự tỏa sáng trong các tình huống mà $String.format()$ gặp khó khăn, cụ thể là:
- Bản Địa Hóa (Localization – L10n): Khi bạn cần thay đổi thứ tự các đối số trong câu tùy thuộc vào ngôn ngữ. Ví dụ: “Tên tôi là [Tên]” trong tiếng Việt có thể ngược lại trong một số ngôn ngữ khác.
- Định Dạng Đối Tượng Nâng Cao: Tích hợp các kiểu định dạng phức tạp như tiền tệ (Currency), ngày tháng (Date/Time), số phần trăm (Percent) mà không cần sử dụng các chỉ định $`%`$ rườm rà.
- Xử Lý Số Nhiều (Pluralization): Giải quyết vấn đề ngữ pháp phức tạp khi số lượng thay đổi (ví dụ: “1 con mèo” khác với “2 con mèo”).
Cú pháp cơ bản
Khác với $`String.format()`$ sử dụng $`%`$, $`MessageFormat`$ sử dụng cú pháp chỉ số đối số trong ngoặc nhọn $`{N}`$.
Cú pháp:
MessageFormat.format(format_string, arg0, arg1, arg2, ...);
Ví dụ cơ bản:
Chúng ta sử dụng chỉ số $`{0}`$, $`{1}`$, $`{2}`$ để đại diện cho các đối số theo thứ tự.
import java.text.MessageFormat;
String template = "Khách hàng {0} đã đặt mua {1} sản phẩm với tổng giá trị {2}.";
String customerName = "Thắng";
int quantity = 5;
double totalAmount = 99.5;
String result = MessageFormat.format(template,
customerName,
quantity,
totalAmount);
// Output: "Khách hàng Thắng đã đặt mua 5 sản phẩm với tổng giá trị 99.5."
Ví dụ về định dạng tiền tệ và số nhiều (Pluralization)
$`MessageFormat`$ cho phép bạn nhúng các kiểu định dạng nâng cao vào bên trong chỉ số đối số:
- Định dạng Tiền Tệ: Cú pháp:
$`{index, number, currency}`$ - Xử lý Số Nhiều (Pluralization): Cú pháp:
$`{index, plural, rules...}`$(Đây là tính năng rất mạnh mẽ nhưng có cú pháp phức tạp hơn nhiều).
import java.text.MessageFormat;
import java.util.Locale;
// Thiết lập định dạng cho Tiền Tệ (Locale.US để dễ thấy định dạng $)
String currencyTemplate = "Giá trị đơn hàng là {0, number, currency} vào ngày {1, date, long}.";
String formattedCurrency = MessageFormat.format(currencyTemplate,
20500.75,
new java.util.Date());
// Output có thể là: "Giá trị đơn hàng là $20,500.75 vào ngày November 17, 2025."
// (Tùy thuộc vào Locale mặc định của hệ thống)
Lợi ích: $`MessageFormat`$ tự động điều chỉnh định dạng tiền tệ (ký hiệu $$, $`€`$, $`₫\`$) và định dạng ngày tháng theo Locale hiện tại của ứng dụng, làm cho nó trở thành công cụ tối thượng cho bản địa hóa.
Các câu hỏi thường gặp về Java String Format
String.format() có an toàn cho Thread (Thread-Safe) không?
Phương thức tĩnh $`String.format()`$ là an toàn cho Thread (Thread-Safe) vì nó sử dụng một đối tượng $`java.util.Formatter`$ cục bộ bên trong (local to the method call) và không chia sẻ trạng thái giữa các thread. Mỗi lần gọi $`String.format()`$ là độc lập với các lần gọi khác.
Tuy nhiên, nếu bạn tạo và tái sử dụng một đối tượng $`Formatter`$ hoặc $`MessageFormat`$ thủ công giữa các thread, chúng không an toàn cho Thread và bạn cần phải đồng bộ hóa (synchronize) truy cập.
Làm cách nào để xử lý java.util.IllegalFormatException?
Lỗi này xảy ra khi có sự không khớp giữa chỉ định định dạng (format specifier) và kiểu dữ liệu của đối số tương ứng.
Ví dụ: Bạn sử dụng $`%d`$ (mong đợi một số nguyên) nhưng lại truyền vào một chuỗi ($`String`$).
// Lỗi: Truyền "hello" (String) vào %d (Decimal)
String.format("Giá trị là: %d", "hello");
// Sẽ ném ra IllegalFormatException
Cách khắc phục:
Kiểm tra kỹ cú pháp $`%`$ và đảm bảo chỉ định định dạng khớp với kiểu dữ liệu của đối số được truyền vào.
Kiểm tra số lượng chỉ định định dạng và số lượng đối số truyền vào có bằng nhau không.
Có cách nào định dạng chuỗi hiệu suất cao hơn không?
Trong các tình huống cần xây dựng chuỗi trong vòng lặp với hiệu suất tối ưu, bạn nên ưu tiên sử dụng:
- StringBuilder: Để nối chuỗi hiệu quả hơn so với toán tử
$`+`$. - StringJoiner (Java 8+): Lý tưởng để xây dựng chuỗi từ một bộ sưu tập (Collection) các phần tử có dấu phân cách (delimiter).
// Ví dụ: Dùng StringJoiner
StringJoiner sj = new StringJoiner(", ");
sj.add("Apple").add("Banana").add("Cherry");
// Output: "Apple, Banana, Cherry"
Tổng kết
Việc định dạng chuỗi là một kỹ năng nền tảng trong Java. Tóm lại, bạn nên chọn công cụ phù hợp với mục đích: sử dụng String.format() cho nhu cầu định dạng số học và chuỗi chính xác; sử dụng Text Blocks để cải thiện khả năng đọc của các khuôn mẫu đa dòng (JSON/HTML); và ưu tiên MessageFormat khi xử lý bản địa hóa (l10n).
Nắm vững các phương pháp này không chỉ giúp mã của bạn chuyên nghiệp hơn mà còn đảm bảo dữ liệu đầu ra luôn sạch sẽ và có cấu trúc.

