Overloading — Nhiều method cùng tên, khác signature
Tìm hiểu nạp chồng phương thức trong Java, thuật toán 3 phase phân giải chữ ký hàm của compiler, tránh bẫy autoboxing và varargs.
TL;DR: Nạp chồng phương thức (overloading) cho phép nhiều hàm trong cùng một lớp mang cùng tên nhưng khác nhau về số lượng hoặc kiểu tham số. Trình biên dịch phân giải lời gọi tĩnh tại thời điểm compile qua 3 giai đoạn khắt khe (khớp chính xác & widening, autoboxing, rồi đến varargs), ưu tiên phiên bản cụ thể nhất (most specific).
Sau khi nắm vững cách khai báo method cơ bản, bạn sẽ thường xuyên đối mặt với bài toán: cần cung cấp các thao tác tương đồng cho nhiều kiểu dữ liệu khác nhau (như in số nguyên, số thực, chuỗi ký tự hay mảng). Nếu phải đặt các tên hàm rườm rà như printInt, printDouble, printString, API sẽ trở nên cồng kềnh. Java giải quyết bài toán này thông qua kỹ thuật overloading.
1. Analogy cửa hàng sửa chữa minh họa method overloading ra sao?
Tiệm sửa gọi là "Fix-It". Khách mang đến cái gì, chủ tiệm xử lý theo cái đó — đồng hồ sửa kiểu đồng hồ, giày sửa kiểu giày, máy tính sửa kiểu máy tính. Cùng một "dịch vụ" tên Fix, nhưng thao tác cụ thể khác nhau theo thứ khách đưa.
| Đời thường | Overloading |
|---|---|
| Tên dịch vụ "Fix" | Tên method fix |
| Khách mang đồng hồ → sửa đồng hồ | fix(Watch w) |
| Khách mang giày → sửa giày | fix(Shoe s) |
| Khách mang máy tính → sửa máy tính | fix(Computer c) |
Overloading = giữ một tên dễ nhớ, cho compiler chọn phiên bản đúng theo argument.
2. Cú pháp nạp chồng phương thức trong Java yêu cầu điều kiện gì?
public class Printer {
public static void print(int x) {
System.out.println("int: " + x);
}
public static void print(double x) {
System.out.println("double: " + x);
}
public static void print(String x) {
System.out.println("String: " + x);
}
public static void print(int x, int y) {
System.out.println("two ints: " + x + ", " + y);
}
}
Gọi:
Printer.print(42); // int: 42
Printer.print(3.14); // double: 3.14
Printer.print("hello"); // String: hello
Printer.print(1, 2); // two ints: 1, 2
Rule: signature phải khác — đổi số lượng hoặc kiểu của parameter. Không đủ nếu chỉ đổi:
- Tên parameter (
print(int x)vsprint(int y)) — compile error. - Return type (
int print(int x)vsdouble print(int x)) — compile error.
3. Trình biên dịch phân giải overload qua 3 phase như thế nào?
Compiler chạy 3 phase khi gọi method overloaded:
- Exact match + widening primitive — không autobox, không varargs.
- Autoboxing / unboxing — cho phép
int→Integer,Integer→int. - Varargs — coi tham số cuối là mảng.
Compiler dừng ở phase đầu tiên tìm được method applicable. Phase sau bị bỏ qua nếu phase trước đã có kết quả.
Thứ tự đó mới là phần quyết định — đọc riêng danh sách 3 phase dễ tưởng compiler cân nhắc cả ba rồi chọn cái "hợp lý nhất":

Ví dụ:
public static void test(Integer x) { System.out.println("Integer"); }
public static void test(long x) { System.out.println("long"); }
test(5); // in gi?
- Phase 1:
5làint. Thử exact match — không cótest(int). Thử widening primitive:int → longOK → tìm thấytest(long). - Dừng ở phase 1 → in
"long".
Autoboxing int → Integer thuộc phase 2, bị bỏ qua vì phase 1 đã tìm thấy.
3.1 Widening primitive — thứ tự
Java tự mở rộng primitive theo chuỗi không mất dữ liệu:
byte → short → int → long → float → double
char ↗
Vậy int có thể match long, float, double — nhưng không match short hay byte (narrowing, cần cast rõ).
public static void show(long x) { System.out.println("long"); }
public static void show(double x) { System.out.println("double"); }
show(5); // in "long" — long gan hon int trong chuoi widening
Compiler chọn most specific — long gần int hơn double → ưu tiên long.
3.2 Overload với kiểu reference — rule "most specific"
public static void handle(Object o) { System.out.println("Object"); }
public static void handle(String s) { System.out.println("String"); }
handle("hi"); // in "String"
handle(42); // in "Object" (42 autobox -> Integer, Integer khong phai String -> Object)
"hi" là String, match handle(String) trực tiếp. 42 là int, không match exact, phase 2 autobox thành Integer — Integer là Object nhưng không phải String → chọn handle(Object).
4. Bẫy lỗi autoboxing kết hợp method overloading nguy hiểm ra sao?
public static void print(int x) { System.out.println("int"); }
public static void print(Integer x) { System.out.println("Integer"); }
int a = 5;
Integer b = 5;
print(a); // int
print(b); // Integer
Đẹp và rõ. Nhưng:
public static void pick(Integer x) { System.out.println("Integer"); }
public static void pick(long x) { System.out.println("long"); }
pick(5); // in gi?
- Phase 1:
int → longwidening ✓ → tìm thấypick(long)→ dừng → in"long".
Trực giác người đọc "5 là Integer" sai — compiler theo phase, không theo tên tương tự.
List<Integer> có 2 method remove:
E remove(int index); // xoa phan tu tai chi so
boolean remove(Object o); // xoa phan tu equals voi o
List<Integer> list = new ArrayList<>(List.of(10, 20, 30));
list.remove(1); // xoa index 1 -> con [10, 30]
list.remove(Integer.valueOf(1)); // xoa gia tri 1 (khong co) -> khong doi
Compiler ưu tiên remove(int) vì exact match primitive. Code list.remove(1) không xóa giá trị 1, mà xóa vị trí số 1. Nhiều dev đã dính bug này. Luôn dùng Integer.valueOf(1) hoặc (Integer) 1 khi muốn xóa theo giá trị.
Thử ngẫmbạn review một PR có dòng orderIds.remove(1) trên List<Integer>, không kèm test. Bạn có chắc nó xoá phần tử giá trị 1, hay xoá vị trí thứ 1?
5. Quy tắc ưu tiên giữa fixed-arity và varargs hoạt động như thế nào?
public static int sum(int a, int b) { return a + b; }
public static int sum(int... nums) {
int total = 0;
for (int n : nums) total += n;
return total;
}
sum(1, 2); // in gi?
sum(1, 2, 3); // in gi?
sum(1, 2): Phase 1 tìm exact fixed-arity →sum(int, int)match → dùng nó. Varargs bị bỏ qua.sum(1, 2, 3): fixed-arity không match (3 arg vs 2 slot) → phase 3 dùng varargs.
Rule: fixed-arity luôn thắng varargs nếu cả hai đều applicable. Varargs chỉ được dùng khi phase 1 và 2 fail.
Thử ngẫmcodebase có sẵn sum(int, int), giờ thêm sum(int...) để hỗ trợ N tham số. Code cũ gọi sum(1, 2) khắp nơi — overload mới có đổi hành vi những chỗ gọi cũ không?
6. Khi nào nên áp dụng và khi nào cần tránh nạp chồng phương thức?
✅ Nên khi:
- Cùng ngữ nghĩa, khác kiểu input hoặc mức chi tiết:
Java
LocalDate of(int year, int month, int day) LocalDate of(int year, Month month, int day) - Cho phép gọi ngắn với default value giả lập:
Java
Logger.log(String msg) // level = INFO Logger.log(Level level, String msg) // level tu chi dinh
❌ Không nên khi:
- Hai phiên bản làm việc khác ngữ nghĩa — nên tách 2 tên rõ:
Java
// Xau — cung ten nhung lam 2 viec khac hoan toan parse(String s) // parse CSV parse(File f) // parse JSON - Overload chỉ để ép user đọc JavaDoc — nếu tên không tự giải thích được khác biệt, đặt 2 tên.
- Parameter list dễ bị boxing/widening nhầm (như
intvsIntegervslong).
7. Khái niệm covariant return có liên quan gì đến overloading?
Khi bạn override method ở class con, Java cho phép return type là subclass của return type cha — gọi là covariant return. Đây là rule override (polymorphism), không phải overload, nhưng dễ nhầm. Sẽ đào ở module OOP.
8. Cơ chế phân giải tĩnh (static dispatch) ở bytecode hoạt động ra sao?
Overloading được giải quyết tại compile time (gọi là static dispatch). Compiler nhìn kiểu khai báo của argument → chọn method → nhúng đúng instruction invokestatic / invokevirtual với method descriptor cụ thể vào bytecode.
// Source:
print(5);
// Bytecode (gia su tim thay print(int)):
iconst_5
invokestatic Printer.print(I)V
Method descriptor (I)V nói rõ: take int, return void. JVM không "chọn lại" ở runtime — nó chạy đúng method đã ghi.
Đây là điểm khác overriding (sẽ học OOP): overriding dispatch tại runtime dựa vào kiểu thật của object (dynamic dispatch).
Thử ngẫmoverload được chọn tại compile time dựa vào kiểu khai báo, không phải kiểu thật lúc chạy. Gán Object o = "hello" rồi gọi handle(o) với hai overload handle(String)/handle(Object) ở mục 3.2 — compiler chọn cái nào?
9. Các bẫy lỗi thường gặp nhất với method overloading là gì?
❌ Nhầm 1: Cho rằng compiler chọn theo "tên kiểu tương tự nhất".
void f(Integer x) { ... }
void f(long x) { ... }
f(5); // tuong "Integer" — thuc te chay long (widening tu int uu tien autoboxing)
✅ Hiểu 3 phase: widening → autoboxing → varargs. Phase trước luôn thắng.
❌ Nhầm 2: Đổi chỉ return type rồi tưởng là overload.
int calc(int x) { ... }
double calc(int x) { ... } // COMPILE ERROR — cung signature
✅ Signature không gồm return type. Đổi số/kiểu parameter.
❌ Nhầm 3: list.remove(1) trên List<Integer> — xóa index chứ không xóa giá trị.
✅ list.remove(Integer.valueOf(1)) hoặc list.removeIf(x -> x == 1).
❌ Nhầm 4: Overload với Object + kiểu cụ thể → gọi với null compile fail.
void f(String s) { ... }
void f(Integer i) { ... }
f(null); // AMBIGUOUS — ca hai deu chap nhan null
✅ Cast rõ: f((String) null) hoặc f((Integer) null). Hoặc tránh overload với nullable reference types có quan hệ ngang cấp.
❌ Nhầm 5: Dùng overload thay vì default parameter (Java không có).
✅ Chain: save(order) gọi save(order, LocalDateTime.now()). Mỗi phiên bản ngắn fill default rồi delegate.
10. Tài liệu Deep Dive Oracle quy định gì về overloading?
Spec / reference chính thức:
- JLS §8.4.9 — Overloading — rule cho phép khai báo nhiều method cùng tên.
- JLS §15.12.2 — Compile-Time Step 2: Determine Method Signature — thuật toán 3 phase để chọn method applicable.
- JLS §15.12.2.5 — Choosing the Most Specific Method — rule "most specific" khi nhiều method applicable.
- JLS §5.1.2 — Widening Primitive Conversion — thứ tự mở rộng primitive không mất dữ liệu.
Ghi chú: §15.12.2 là phần luật lệ dài nhất của JLS — 3 phase resolution nói trên có rất nhiều corner case (generic erasure, boxing + varargs kết hợp, interface default method). Với code thường ngày, nhớ 3 phase và "most specific wins" là đủ; khi gặp "tại sao compiler chọn cái này mà không phải cái kia" thì quay lại spec.
11. Điểm cốt lõi cần nhớ về nạp chồng phương thức là gì?
- Overloading định nghĩa nhiều method cùng tên nhưng khác biệt về số lượng hoặc kiểu tham số trong cùng class.
- Compiler phân giải static dispatch tại thời điểm biên dịch qua 3 phase: Widening primitive, Autoboxing, và Varargs.
- Phương thức fixed-arity luôn được ưu tiên hơn varargs; nguyên lý most specific quyết định khi có nhiều hàm tương thích.
- Tránh overload các phương thức có cùng số lượng tham số nhưng khác kiểu bọc Wrapper/Primitive hoặc kiểu tham chiếu ngang hàng.
12. Tự kiểm tra kiến thức về method overloading như thế nào?
- Q1Đoạn sau compile không? Nếu không, giải thích.
public static int calc(int a, int b) { return a + b; } public static double calc(int a, int b) { return a * b; } - Q2Đoạn sau in gì và tại sao?
public static void pick(Integer x) { System.out.println("Integer"); } public static void pick(long x) { System.out.println("long"); } pick(5); - Q3Đoạn sau in gì?
List<Integer> list = new ArrayList<>(List.of(10, 20, 30)); list.remove(1); System.out.println(list); - Q4Đoạn sau gọi phiên bản nào?
public static int sum(int a, int b) { return a + b; } public static int sum(int... nums) { int t = 0; for (int n : nums) t += n; return t; } sum(1, 2); - Q5Vì sao đoạn sau compile error?
public static void show(String s) { ... } public static void show(Integer i) { ... } show(null);
Bài tiếp theo: Varargs — nhận số tham số tùy ý
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.
Khai báo method — Đóng gói một đoạn việc vào một cái tên
Bài tiếpVarargs — Nhận số lượng tham số tùy ý trong Java
Lộ trình Junior Java Backend Developer
Chặng 2/21 · Lập trình từ số 0 với Java — bài 28/32 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này