Varargs — Nhận số lượng tham số tùy ý trong Java
Khám phá cú pháp tham số biến đổi varargs trong Java, cơ chế đóng gói mảng ở bytecode, quy tắc nạp chồng và cách dùng @SafeVarargs an toàn.
TL;DR: Tham số biến đổi T... (varargs) cho phép phương thức tiếp nhận số lượng đối số tùy ý (từ 0 đến nhiều). Trình biên dịch sẽ tự động gom các đối số thành một mảng T[] ở tầng bytecode tại nơi gọi (call site), và yêu cầu varargs phải đứng ở vị trí cuối cùng trong danh sách tham số.
Sau khi tìm hiểu về nạp chồng phương thức (overloading), bạn có thể nhận ra nhu cầu tiếp nhận số lượng đối số linh hoạt — như String.format, List.of, Arrays.asList, hay System.out.printf. Java cung cấp cú pháp varargs (variable arguments hay variable arity parameter) để giải quyết trọn vẹn nhu cầu này mà không cần viết hàng chục hàm nạp chồng thủ công.
1. Analogy ổ cắm đa năng minh họa varargs ra sao?
Ổ 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 |
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 khai báo tham số biến đổi varargs như thế nào?
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. Trình biên dịch đóng gói varargs thành mảng ở bytecode ra sao?
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:
Java
int[] arr = {10, 20, 30}; int s = sum(arr); // OK, khong unwrap roi wrap lai - Lời gọi
sum(...)(methodstatic) compile thànhinvokestatic sum([I)I— descriptor nhậnint[], returnint. nums.length == 0khi gọisum()không argument — không null, là mảng rỗng.
Ba lời gọi trông rất khác nhau ở phía người viết, nhưng cùng đi vào đúng một method nhận mảng int — khác nhau chỉ ở việc compiler có phải gói thêm hay không:

Cột giữa là chỗ hay bị bỏ qua nhất: truyền mảng có sẵn thì nums chính là mảng đó, không có bản sao nào ở giữa — hệ quả của chuyện này nằm ở phần 7.
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).
Thử ngẫmmột method log(String...) được gọi trong vòng lặp 1 triệu lần xử lý message queue. Escape analysis của JIT có chắc chắn tránh được allocation mảng mỗi lần, hay bạn phải tự overload fixed-arity?
4. Tương tác giữa varargs và nạp chồng (overloading) gây bẫy gì?
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(); // van in "String"
Kể cả với 0 argument, rule most-specific vẫn quyết được: String[] là subtype của Object[], nên f(String...) cụ thể hơn — javac chọn nó (JLS §15.12.2.5).
Ambiguous thật sự chỉ xảy ra khi hai kiểu varargs không có quan hệ subtype với nhau — không ai cụ thể hơn ai:
public static void g(String... xs) { System.out.println("String"); }
public static void g(Integer... xs) { System.out.println("Integer"); }
g(); // COMPILE ERROR — reference to g is ambiguous
String[] và Integer[] không ai là subtype của ai, compiler không chọn được → báo lỗi. Fix: truyền mảng rỗng đúng kiểu để chỉ rõ ý định — g(new String[0]).
Thử ngẫmhai overload g(String...) và g(Integer...) không ai là subtype của ai nên g() ambiguous. Nếu thêm một overload thứ ba g(Object...), lời gọi g() có hết ambiguous không?
5. Vì sao varargs kết hợp với Generics sinh cảnh báo 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 generic array tại call site, nên compiler ném warning "unchecked" để báo rằng 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 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 nên và không nên sử dụng varargs trong thiết kế API?
✅ 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 sẵn một mảng vào method varargs cần lưu ý điều gì?
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()).
Thử ngẫmzeroOut(data) sửa thẳng mảng data gốc vì varargs không copy khi nhận mảng có sẵn. Một method tương tự nhận List<Integer> thay vì int... có bị aliasing giống vậy không?
8. Các bẫy lỗi thường gặp nhất khi dùng varargs là gì?
❌ 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. Tài liệu Deep Dive Oracle quy định gì về variable arity?
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. Điểm cốt lõi cần nhớ về tham số varargs là gì?
- Varargs
Type...cho phép truyền số lượng đối số tùy ý và tự động đóng gói thành mảng tại nơi gọi. - Phương thức chỉ được phép chứa tối đa một tham số varargs và bắt buộc phải nằm ở vị trí cuối cùng.
- Khi không truyền đối số, phương thức nhận về mảng rỗng có độ dài bằng 0 chứ không phải giá trị
null. - Cần cẩn trọng khi dùng varargs trong vòng lặp hiệu năng cao hoặc kết hợp với Generics thiếu kiểm soát.
11. Tự kiểm tra kiến thức về varargs như thế nào?
- Q1Đoạn sau compile không?
public static void log(String... tags, String message) { ... } - 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})); - 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); - 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)); } - 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);
Bài tiếp theo: Recursion — method gọi chính nó
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Overloading — Nhiều method cùng tên, khác signature
Bài tiếpRecursion — Giải thuật đệ quy và cơ chế Call Stack
Lộ trình Junior Java Backend Developer
Chặng 2/21 · Lập trình từ số 0 với Java — bài 29/32 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này