Java — Từ Zero đến Senior/Phương thức (method)/Overloading — nhiều method cùng tên, khác signature
2/6
~18 phútPhương thức (method)

Overloading — nhiều method cùng tên, khác signature

Khi nào Java cho phép nhiều method cùng tên trong một class, cách JVM chọn phiên bản nào khi compile, và vì sao autoboxing + varargs làm overloading resolution trở thành mìn.

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ườngOverloading
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àyfix(Shoe s)
Khách mang máy tính → sửa máy tínhfix(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) vs print(int y)) — compile error.
  • Return type (int print(int x) vs double 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:

  1. Exact match + widening primitive — không autobox, không varargs.
  2. Autoboxing / unboxing — cho phép intInteger, Integerint.
  3. 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: 5int. Thử exact match — không có test(int). Thử widening primitive: int → long OK → tìm thấy test(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 specificlong 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"String, match handle(String) trực tiếp. 42int, không match exact, phase 2 autobox thành IntegerIntegerObject 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 → long widening ✓ → tìm thấy pick(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-aritysum(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ư int vs Integer vs long).

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:

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 primitiveautoboxing/unboxingvarargs. 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) vs remove(Object) — bug kinh điển: số nguyên primitive match remove(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

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; }
Không compile. Signature ở source level = tên + parameter list, không gồm return type. Hai method này cùng tên 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);

In long.

Compiler chạy 3 phase: Phase 1 (widening primitive, không autobox)5int; 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" 5Integer, 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);

In [10, 30] — đã xóa phần tử ở chỉ số 1 (giá trị 20).

List<E> có 2 method overload: remove(int index)remove(Object o). Khi gọi list.remove(1), 1int 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);
Gọi 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.
Q5
Vì sao đoạn sau compile error?
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ả StringInteger (reference types đều nhận null), và không có quan hệ kế thừa giữa StringInteger → 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 ý