Bạn muốn viết print() nhận int, double, String, Object, mảng... Nếu phải đặt tên khác nhau (printInt, printDouble, printString) thì API người dùng nhớ khó và viết sai dễ. Java giải quyết bằng overloading — nhiều method cùng tên, khác signature, compiler chọn đúng phiên bản dựa vào kiểu argument.
Nghe đơn giản. Nhưng khi trộn autoboxing, varargs, và kiểu con, compiler phải chạy overload resolution theo thứ tự nhiều bước — hiểu rules này giúp bạn tránh bug im lặng kiểu "tưởng gọi A mà thực ra gọi B".
1. Analogy — cửa hàng sửa chữa
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) |
💡 💡 Cách nhớ
Overloading = giữ một tên dễ nhớ, cho compiler chọn phiên bản đúng theo argument.
2. Cú pháp — thay đổi parameter list
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. Overload resolution — compiler chọn phiên bản 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ả.
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. Autoboxing — nơi overload resolution hay gây bug
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.remove(int)` vs `List.remove(Object)` — bug kinh điển
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ị.
5. Varargs trong overload
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.
6. Khi nào nên overload, khi nào không?
✅ Nên khi:
- Cùng ngữ nghĩa, khác kiểu input hoặc mức chi tiết:
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:
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õ:
// 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. Covariant return — luật lệ liên quan (sẽ học ở OOP)
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ế bên dưới — compile time, không phải runtime
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).
9. Pitfall tổng hợp
❌ 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. 📚 Deep Dive Oracle
ℹ️ 📚 Deep Dive Oracle (optional)
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. Tóm tắt
- Overloading = nhiều method cùng tên, khác signature trong một class.
- Signature khác = đổi số lượng hoặc kiểu parameter. Return type không tính.
- Compiler resolve 3 phase: widening primitive → autoboxing/unboxing → varargs. Phase trước dừng lại ngay khi có match → phase sau bị bỏ.
- "Most specific" thắng khi nhiều method cùng applicable.
List<Integer>.remove(int)vsremove(Object)— bug kinh điển: số nguyên primitive matchremove(int index)trước.- Fixed-arity luôn thắng varargs nếu cả hai applicable.
- Overloading giải quyết tại compile time (static dispatch), khác overriding (dynamic dispatch).
- Không overload khi 2 phiên bản khác ngữ nghĩa — đặt 2 tên rõ.
12. Tự kiểm tra
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; }
▸
public static int calc(int a, int b) { return a + b; }
public static double calc(int a, int b) { return a * b; }calc, cùng parameter list (int, int) → cùng signature → trùng → compile error "method calc is already defined". Muốn 2 method cùng tên, phải đổi số lượng hoặc kiểu parameter. Đổi return type một mình không giải quyết.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);
▸
public static void pick(Integer x) { System.out.println("Integer"); }
public static void pick(long x) { System.out.println("long"); }
pick(5);In long.
Compiler chạy 3 phase: Phase 1 (widening primitive, không autobox) — 5 là int; int → long là widening primitive hợp lệ → pick(long) applicable → dừng.
Autoboxing int → Integer là phase 2, không được xét đến vì phase 1 đã có kết quả. Kể cả bạn "trực giác" 5 là Integer, compiler không theo trực giác mà theo rule.
Q3Đoạn sau in gì?List<Integer> list = new ArrayList<>(List.of(10, 20, 30));
list.remove(1);
System.out.println(list);
▸
List<Integer> list = new ArrayList<>(List.of(10, 20, 30));
list.remove(1);
System.out.println(list);In [10, 30] — đã xóa phần tử ở chỉ số 1 (giá trị 20).
List<E> có 2 method overload: remove(int index) và remove(Object o). Khi gọi list.remove(1), 1 là int primitive — phase 1 match exact remove(int) → xóa theo index.
Muốn xóa giá trị 1: list.remove(Integer.valueOf(1)) hoặc cast list.remove((Integer) 1) để ép match remove(Object).
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);
▸
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);sum(int, int). Rule: fixed-arity luôn thắng varargs nếu cả hai applicable. sum(1, 2) match exact sum(int, int) ở phase 1 → compiler chọn nó, không xét tiếp varargs. Chỉ khi số argument không match fixed-arity (vd sum(1, 2, 3)) varargs mới được dùng.Q5Vì sao đoạn sau compile error?public static void show(String s) { ... }
public static void show(Integer i) { ... }
show(null);
▸
public static void show(String s) { ... }
public static void show(Integer i) { ... }
show(null);Compile error: reference to show is ambiguous.
null compatible với cả String và Integer (reference types đều nhận null), và không có quan hệ kế thừa giữa String và Integer → không áp dụng được rule "most specific" → compiler không biết chọn phiên bản nào.
Fix: cast rõ ràng tại call site — show((String) null) hoặc show((Integer) null). Bài học: tránh overload với 2 reference type ngang cấp nếu người dùng có thể truyền null.
Bài tiếp theo: Varargs — nhận số tham số tùy ý