Java Foundations
29/39
Bài 29 / 39~12 phútPhương thức (method)Lộ trình · chặng 02/21Miễn phí… lượt xem

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ườngVarargs
Ổ cắm 2 lỗvoid f(int a, int b) — phải đúng 2
Ổ cắm đa năngvoid 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 khai báo tham số biến đổi varargs như thế nào?

Java
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

Java
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

Java
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ư.

Java
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ụ:

Java
// 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ả:

  1. 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
    
  2. Lời gọi sum(...) (method static) compile thành invokestatic sum([I)I — descriptor nhận int[], return int.
  3. nums.length == 0 khi gọi sum() 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:

Ba lời gọi sum khác nhau cùng đi vào một method nhận mảng int: đối số rời được gói thành mảng mới, mảng có sẵn truyền thẳng không gói lại, gọi rỗng cho mảng rỗ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.

📚 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).

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ì?

Java
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:

Java
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ủa Object[].
  • Compiler dùng rule "most specific": String[] cụ thể hơn Object[] → chọn f(String...) → in "String".
Java
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:

Java
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?

Java
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ý:

Java
// 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 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ơn log(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 record hoặc builder thay vì dồn vào Object....
  • 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ì?

Java
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.

Java
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.

Java
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.

Java
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.

Java
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.

Java
@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.

Java
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?

📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

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?

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Đoạn sau compile không?
    public static void log(String... tags, String message) { ... }
  2. 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}));
  3. 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);
  4. Q4
    Vì 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));
    }
  5. 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.

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

Bài tiếp theo

Recursion — Giải thuật đệ quy và cơ chế Call Stack