String.format, List.of, Arrays.asList, Collections.addAll, System.out.printf — tất cả đều nhận số lượng argument tuỳ ý. Bạn có thể gọi of(), of(1), of(1, 2, 3, 4, 5). Java gọi tính năng đó là varargs (variable arguments). Nhìn giống magic, thực chất compiler đóng gói các argument vào một mảng rồi truyền vào.
Bài này giải thích cú pháp, cơ chế biến đổi sang mảng, rule "chỉ 1 varargs và phải cuối", và những bẫy khi varargs gặp overload hoặc generics.
1. Analogy — ổ cắm đa năng
Ổ cắm thường có 2 lỗ — đúng 2 chân dây, thiếu hoặc thừa đều không cắm được. Ổ cắm đa năng (multi-plug) chấp nhận 0, 1, 5, 10 thiết bị — bạn cắm bao nhiêu cũng được, miễn dưới giới hạn.
| Đời thường | Varargs |
|---|---|
| Ổ cắm 2 lỗ | void f(int a, int b) — phải đúng 2 |
| Ổ cắm đa năng | void f(int... xs) — 0 đến nhiều |
💡 💡 Cách nhớ
T... = "không hoặc nhiều T". Compiler tự gom thành T[]. Bạn gọi bao nhiêu argument tuỳ thích (kể cả 0), hoặc truyền sẵn một mảng.
2. Cú pháp
public static int sum(int... nums) {
int total = 0;
for (int n : nums) total += n;
return total;
}
sum(); // 0
sum(5); // 5
sum(1, 2, 3); // 6
sum(new int[]{4, 5, 6}); // 15 — truyen mang co san
Dấu ... đặt sau kiểu, trước tên parameter. Bên trong method, nums là mảng int[] thực sự — bạn dùng nó như mảng bình thường: nums.length, nums[i], for-each.
2.1 Rule 1 — chỉ 1 varargs trong 1 method
public static void f(int... a, int... b) { ... } // COMPILE ERROR
Vì compiler không biết chia argument nào cho a, argument nào cho b.
2.2 Rule 2 — varargs phải ở cuối parameter list
public static void log(String... tags, String message) { ... } // COMPILE ERROR
public static void log(String message, String... tags) { ... } // OK
Tương tự lý do: compiler cần biết đâu là tham số bắt buộc, đâu là varargs. Parameter cố định đi trước, varargs gom phần dư.
log("ERROR"); // message = "ERROR", tags = []
log("ERROR", "auth", "network"); // message = "ERROR", tags = ["auth", "network"]
3. Cơ chế bên dưới — thực ra là mảng
Bytecode đóng gói varargs thành mảng tại call site. Ví dụ:
// Source:
sum(1, 2, 3);
// Tuong duong voi:
sum(new int[]{1, 2, 3});
sum method có signature int sum(int[]) ở bytecode. Dấu ... là đường cú pháp (syntactic sugar) cho người viết, không cho JVM. Điều này có hệ quả:
- Bạn có thể truyền mảng sẵn có vào method varargs — Java không bắt buộc expand:
int[] arr = {10, 20, 30}; int s = sum(arr); // OK, khong unwrap roi wrap lai - Method varargs gọi từ class khác compile thành
invokevirtual sum([I)I— nhậnint[], returnint. nums.length == 0khi gọisum()không argument — không null, là mảng rỗng.
ℹ️ 📚 Varargs có tạo allocation mỗi lần gọi?
Có. Mỗi call sum(1, 2, 3) tạo một int[]{1, 2, 3} mới trên heap. Với code hot-path gọi hàng triệu lần, đây là vấn đề — JIT có thể escape-analyze và stack-allocate nếu mảng không thoát khỏi method, nhưng không đảm bảo. Nếu performance thực sự quan trọng, overload fixed-arity cho case 1-2-3 args là pattern phổ biến (xem String.format hay List.of JDK).
4. Varargs + overload — bẫy "ambiguous"
public static void show(int a, int b) { System.out.println("two"); }
public static void show(int... xs) { System.out.println("vararg"); }
show(1, 2); // in "two" — fixed-arity thang varargs
show(1); // in "vararg"
show(); // in "vararg"
Rule từ bài overloading: fixed-arity luôn thắng varargs nếu cả hai applicable.
Nhưng khi có 2 varargs method cùng applicable:
public static void f(Object... xs) { System.out.println("Object"); }
public static void f(String... xs) { System.out.println("String"); }
f("a", "b"); // in gi?
- Cả hai applicable:
String[]là subtype củaObject[]. - Compiler dùng rule "most specific":
String[]cụ thể hơnObject[]→ chọnf(String...)→ in"String".
f(); // in gi? AMBIGUOUS — compile error
Với 0 argument, cả Object[]{} và String[]{} đều match, không ai "more specific" hơn từ 0 phần tử → compile error. Fix: cast rõ f((String[]) null).
5. Varargs + generics — warning "unchecked"
public static <T> List<T> asList(T... items) {
return Arrays.asList(items);
}
asList("a", "b"); // warning: unchecked generic array creation
asList(1, 2, 3); // warning same
Vấn đề: Java không cho phép tạo new T[] (generic array) do type erasure — ở runtime T[] thành Object[]. Varargs với T buộc compiler tạo array generic → compiler ném warning "unchecked" báo bạn chịu trách nhiệm đảm bảo type safety.
Cách xử lý:
// Trong JDK, cac method nhu Arrays.asList bao dam an toan
// va danh dau @SafeVarargs de tat warning
@SafeVarargs
public static <T> List<T> asList(T... items) {
return new ArrayList<>(Arrays.asList(items));
}
@SafeVarargs nói với compiler: "Tôi đã kiểm tra, method này không làm hành động unsafe với generic array". Chỉ đặt lên method static, final, hoặc private (phương án mà không ai override được).
⚠️ ⚠️ Khi nào KHÔNG dùng @SafeVarargs?
Khi method thực sự dùng array unsafe (ép kiểu, lưu vào field, truyền ra ngoài). @SafeVarargs không kiểm tra gì — nó chỉ tắt warning. Nếu bạn sai, user sẽ gặp ClassCastException ở runtime khó lần. Chỉ đánh dấu khi bạn chắc chắn không truy cập mảng theo cách trái generic.
6. Khi nào dùng varargs?
✅ Nên khi:
- Method logic tự nhiên nhận 0 đến nhiều argument cùng kiểu:
List.of,String.format,Collections.addAll. - Call site gọn hơn hẳn viết
new T[]{...}:log("A", "B", "C")dễ đọc hơnlog(new String[]{"A", "B", "C"}).
❌ Tránh khi:
- Hot-path perf-critical — mỗi call allocate mảng mới.
- Ý nghĩa argument khác nhau → dùng
recordhoặc builder thay vì dồn vàoObject.... - Có thể gây ambiguous overload (đã nói ở phần 4).
7. Truyền mảng sẵn vào varargs — lưu ý
public static int sum(int... nums) { ... }
int[] arr = {1, 2, 3};
sum(arr); // nums = arr (cung tham chieu, khong copy)
int[][] matrix = {{1, 2}, {3, 4}};
sum(matrix); // COMPILE ERROR — int[][] khong match int...
Khi truyền mảng sẵn, Java dùng nó trực tiếp (không wrap lần nữa). Đây là tối ưu, nhưng có hệ quả: method có thể sửa mảng gốc.
public static void zeroOut(int... nums) {
for (int i = 0; i < nums.length; i++) nums[i] = 0;
}
int[] data = {1, 2, 3};
zeroOut(data);
System.out.println(Arrays.toString(data)); // [0, 0, 0]
Nếu không muốn mảng gốc bị sửa, clone trước: zeroOut(data.clone()).
8. Pitfall tổng hợp
❌ Nhầm 1: Đặt varargs không ở cuối.
void f(int... xs, String name) { ... } // COMPILE ERROR
✅ Varargs luôn ở cuối. Tham số bắt buộc trước.
❌ Nhầm 2: nums == null khi gọi không argument.
void f(int... nums) {
if (nums == null) ... // KHONG bao gio true khi goi f()
}
✅ nums là mảng rỗng int[0], không phải null. Nhưng có thể là null nếu người gọi ép f((int[]) null). Nếu cần defensive, kiểm tra nums == null vẫn có lý.
❌ Nhầm 3: Tưởng varargs rẻ tiền, spam trong loop.
for (int i = 0; i < 1_000_000; i++) log("iter", "n=" + i); // 2M String[] allocation
✅ Trong hot path, overload fixed-arity hoặc dùng StringBuilder + fixed method.
❌ Nhầm 4: @SafeVarargs trên method không static/final/private.
@SafeVarargs
public <T> void m(T... xs) { ... } // WARNING — instance method khong final co the override
✅ Chỉ đặt trên static, final, hoặc private.
❌ Nhầm 5: Object... args với Object[] đi vào.
void log(Object... args) { System.out.println(args.length); }
Object[] arr = {"a", "b"};
log(arr); // in 2 — arr dung lam varargs
log((Object) arr); // in 1 — arr la 1 phan tu Object
✅ Cast (Object) để buộc 1 argument, hoặc cast (Object[]) để buộc unwrap.
9. 📚 Deep Dive Oracle
ℹ️ 📚 Deep Dive Oracle (optional)
Spec / reference chính thức:
- JLS §8.4.1 — Formal Parameters — định nghĩa variable arity parameter, rule "phải cuối" và "chỉ 1".
- JLS §15.12.4.2 — Evaluate Arguments — cách compile argument list cho varargs vs fixed-arity.
- JLS §9.6.4.7 — @SafeVarargs — rule và scope của annotation.
- JEP 12 — Preview Language and VM Features — no-op reference.
Ghi chú: §8.4.1 định nghĩa varargs là "variable arity parameter" — chính thức không phải "varargs". Khi đọc spec, dùng đúng từ đó để search. Rule chỉ 1 và phải cuối có lý do rõ trong §15.12.4.2 — compiler cần biên giới giữa fixed và var.
10. Tóm tắt
T...= variable arity parameter. Compiler gom argument vào mảngT[].- Chỉ 1 varargs mỗi method, phải ở cuối parameter list.
- Call
f()không argument →xslà mảng rỗng (không null). - Có thể truyền mảng sẵn — method có thể sửa mảng gốc (cùng reference).
- Fixed-arity luôn thắng varargs khi cùng applicable. Giữa 2 varargs, rule "most specific".
- Varargs + generics → warning unchecked. Dùng
@SafeVarargskhi chắc chắn an toàn, chỉ trênstatic/final/private. - Mỗi call varargs tạo một mảng mới — tránh trong hot-path.
- Overload
f(T...)vớif(T, T)+f(T, T, T)là pattern chuẩn khi cần vừa tiện vừa perf.
11. Tự kiểm tra
Q1Đoạn sau compile không?public static void log(String... tags, String message) { ... }
▸
public static void log(String... tags, String message) { ... }log("a", "b", "hello") không xác định được "hello" là tag cuối hay là message. Đổi thứ tự thành log(String message, String... tags).Q2Đoạn sau in gì?public static int sum(int... nums) {
int t = 0; for (int n : nums) t += n; return t;
}
System.out.println(sum());
System.out.println(sum(new int[]{10, 20, 30}));
▸
public static int sum(int... nums) {
int t = 0; for (int n : nums) t += n; return t;
}
System.out.println(sum());
System.out.println(sum(new int[]{10, 20, 30}));In 0 rồi 60.
sum()—numslà mảng rỗngint[0],length = 0, vòng for không chạy, return0.sum(new int[]{10, 20, 30})— truyền mảng sẵn; Java không wrap lần nữa,numschính là mảng đó. Tổng: 60.
Chú ý: nums không null khi gọi không argument — Java luôn cho mảng rỗng.
Q3Đoạn sau gọi phiên bản nào và tại sao?public static void f(int a, int b) { System.out.println("fixed"); }
public static void f(int... xs) { System.out.println("var"); }
f(1, 2);
f(1, 2, 3);
▸
public static void f(int a, int b) { System.out.println("fixed"); }
public static void f(int... xs) { System.out.println("var"); }
f(1, 2);
f(1, 2, 3);f(1, 2) in fixed; f(1, 2, 3) in var.
Rule: fixed-arity luôn thắng varargs khi cả hai applicable (§15.12.2). f(1, 2) match exact f(int, int) → chọn nó. f(1, 2, 3) không match fixed-arity (3 vs 2 slot) → fall sang phase varargs.
Q4Vì sao đoạn sau có warning và cách sửa?public static <T> List<T> asList(T... items) {
return new ArrayList<>(Arrays.asList(items));
}
▸
public static <T> List<T> asList(T... items) {
return new ArrayList<>(Arrays.asList(items));
}Java cấm tạo new T[] (generic array creation) do type erasure — ở runtime T[] thành Object[]. Varargs với generic T buộc compiler tạo generic array tại call site → warning "unchecked generic array creation".
Fix: đánh dấu @SafeVarargs nếu method không làm hành động unsafe (không cast array sang kiểu khác, không expose array ra ngoài, không lưu vào field):
@SafeVarargs
public static <T> List<T> asList(T... items) {
return new ArrayList<>(Arrays.asList(items));
}Annotation chỉ hợp lệ trên static, final, hoặc private method — để không ai override lỏng lẻo. Method này static nên OK.
Q5Đoạn sau in gì?public static void show(Object... args) {
System.out.println(args.length);
}
Object[] arr = {"a", "b", "c"};
show(arr);
show((Object) arr);
▸
public static void show(Object... args) {
System.out.println(args.length);
}
Object[] arr = {"a", "b", "c"};
show(arr);
show((Object) arr);In 3 rồi 1.
show(arr)— Java thấyarrlàObject[]và varargs nhậnObject[]→ truyền trực tiếp mảng →args.length = 3.show((Object) arr)— castarrsangObject(1 reference đơn lẻ) → Java wrap vào mảng mớiObject[]{ arr }(1 phần tử) →args.length = 1.
Đây là bẫy khi method log/util dùng Object...: người gọi vô tình truyền mảng sẽ làm unwrap. Cast (Object) để buộc nguyên mảng, (Object[]) để expand.
Bài tiếp theo: Recursion — method gọi chính nó