Java — Từ Zero đến Senior/Phương thức (method)/Khai báo method — đóng gói một đoạn việc vào một cái tên
1/6
~18 phútPhương thức (method)

Khai báo method — đóng gói một đoạn việc vào một cái tên

Hiểu method trong Java từ chữ ký (signature), parameter, return type đến cơ chế call stack. Vì sao mỗi method là một stack frame độc lập và điều đó nghĩa là gì cho debug.

Đến hết Module 3, bạn đã viết được chương trình vài chục dòng dồn hết vào main. Nhưng khi logic chạm 100–200 dòng, main thành khối khổng lồ không ai đọc nổi — sửa 1 chỗ sợ vỡ 10 chỗ. Cách giải quyết ai cũng biết: tách thành nhiều hàm. Nhưng trong Java, "hàm" không đứng một mình — nó phải thuộc về một class, và nó có signature quyết định cách JVM gọi nó.

Bài này giải thích cách khai báo method, signature gồm gì, return type có ý nghĩa gì ở cấp bytecode, và call stack — cơ chế JVM quản lý chuỗi gọi hàm mà hiểu nó giúp bạn đọc stack trace thành thạo.

1. Analogy — công thức trong sổ tay đầu bếp

Bạn có một cuốn sổ tay nhà bếp. Mỗi trang là một công thức có tên (bánh xèo), nguyên liệu đầu vào (bột gạo, tôm), và thứ trả ra (1 đĩa bánh xèo). Khi cần bánh xèo, bạn lật đúng trang — không phải viết lại công thức mỗi lần.

Đời thườngMethod
Tên công thứcTên method (makePancake)
Nguyên liệuParameters (flour, shrimp)
Món trả raReturn value (Pancake)
Sách công thứcClass chứa method

💡 💡 Cách nhớ

Method = đặt tên cho một đoạn việc để có thể gọi lại nhiều lần, truyền input khác nhau, nhận output ra.

2. Cú pháp cơ bản

<modifier> <returnType> <methodName>(<paramType> <paramName>, ...) {
    // body
    return <value>;   // bat buoc neu returnType khac void
}

Ví dụ:

public class MathUtils {

    public static int add(int a, int b) {
        return a + b;
    }

    public static void printGreeting(String name) {
        System.out.println("Hello, " + name);
    }
}

Hai method trên có:

  • Modifierpublic static. static = method của class, không cần tạo object để gọi. Sẽ đào sâu ở phần 6.
  • Return typeint (trả số) hoặc void (không trả gì).
  • Tên — camelCase theo convention Java. add, printGreeting, calculateTax.
  • Parameter list — 0 hoặc nhiều cặp <kiểu> <tên>, phân cách bằng dấu phẩy.
  • Body — trong { }. Nếu có return type khác void, bắt buộcreturn trong mọi nhánh kết thúc.

Gọi method:

int sum = MathUtils.add(3, 5);           // sum = 8
MathUtils.printGreeting("Alice");         // in: Hello, Alice

2.1 void — method không trả gì

public static void log(String message) {
    System.out.println("[LOG] " + message);
}

void nghĩa là method chạy vì side effect (in, ghi file, gọi API, thay đổi state), không trả giá trị. Không cần return — hoặc chỉ return; trống để thoát sớm:

public static void login(User user) {
    if (user == null) return;       // thoat som
    if (!user.isActive()) return;
    // logic login
}

2.2 Return value — bắt buộc trong mọi nhánh

public static int sign(int x) {
    if (x > 0) return 1;
    if (x < 0) return -1;
    // quen return cho truong hop x == 0 -> COMPILE ERROR
}

Compiler bắt: "missing return statement". Java yêu cầu mọi đường thoát của method non-void phải có return. Fix:

public static int sign(int x) {
    if (x > 0) return 1;
    if (x < 0) return -1;
    return 0;
}

Hoặc switch expression cho compiler thấy exhaustive:

public static int sign(int x) {
    return Integer.signum(x);   // dung san trong JDK
}

3. Signature — thứ JVM dùng để phân biệt method

Signature của một method trong Java = tên + danh sách kiểu tham số. Return type không nằm trong signature.

int add(int a, int b)          // signature: add(int, int)
double add(double a, double b) // signature: add(double, double) — khac, hop le
int add(int x, int y)          // SAME signature voi cai dau — KHONG hop le trong cung class
String add(int a, int b)       // SAME signature — KHONG hop le

Có 2 hệ quả quan trọng:

  1. Overloading được: nhiều method cùng tên, khác parameter list. Bài sau.
  2. Return type đổi không tạo signature mới — muốn 2 method khác thì phải đổi số/kiểu parameter.

ℹ️ 📚 Bytecode thấy signature thế nào?

Trong file .class, mỗi method được nhận diện bằng tên + descriptor — nhưng descriptor bao gồm return type. Ví dụ add(int,int) return intadd(II)I. Đây là lý do JVM runtime phân biệt chính xác, nhưng compiler Java theo JLS quyết định "cùng signature" ở source level chỉ dựa tên + params — rõ hơn ở JLS §8.4.2.

4. Call stack — JVM chạy chuỗi gọi method thế nào

Khi method A gọi method B, JVM đặt B lên trên A trong call stack. B chạy xong → pop khỏi stack, quay về A tiếp tục. Mỗi method đang chạy có stack frame riêng chứa parameter + local variable.

public static void main(String[] args) {
    int result = computeTotal(10, 20);
    System.out.println(result);
}

public static int computeTotal(int a, int b) {
    int sum = add(a, b);
    return sum * 2;
}

public static int add(int a, int b) {
    return a + b;
}

Luồng call stack khi chạy:

sequenceDiagram
    participant Main as main()
    participant Compute as computeTotal()
    participant Add as add()

    Main->>Compute: computeTotal(10, 20)
    Compute->>Add: add(10, 20)
    Add-->>Compute: return 30
    Compute-->>Main: return 60
    Main->>Main: println(60)

Tại moment add đang chạy, stack có 3 frame (từ dưới lên): main, computeTotal, add. Mỗi frame có local variable độc lập — a trong computeTotala trong add2 biến khác nhau, dù cùng tên.

4.1 Stack trace — tấm ảnh chụp call stack khi có exception

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at MathUtils.divide(MathUtils.java:15)
    at MathUtils.computeRatio(MathUtils.java:9)
    at MathUtils.main(MathUtils.java:4)

Đọc từ trên xuống: exception phát sinh ở divide line 15, được gọi từ computeRatio line 9, được gọi từ main line 4. Hiểu call stack giúp bạn debug bất cứ exception nào trong 30 giây.

💡 💡 Quick tip debug

Khi thấy stack trace, nhìn dòng "at" đầu tiên trong code của bạn (bỏ qua JDK internal). Đó là nơi lỗi thực sự xảy ra. 80% bug đọc stack trace đúng là tìm ra ngay.

4.2 StackOverflowError — stack đầy

Call stack có kích thước giới hạn (thường ~512KB–1MB). Đệ quy không có điều kiện dừng:

public static int bad(int n) {
    return bad(n + 1);   // khong bao gio dung
}

Chạy → StackOverflowError sau vài chục ngàn frame. Bài recursion sẽ đào sâu.

5. Tham số — pass-by-value, không phải pass-by-reference

Đã học ở Module 2 bài "Biến và khai báo". Nhắc lại ngắn:

public static void tryChange(int x) {
    x = 100;   // chi sua ban copy
}

public static void main(String[] args) {
    int a = 5;
    tryChange(a);
    System.out.println(a);   // van in 5
}

Java luôn pass-by-value. Với reference type, "value" là địa chỉ — method copy địa chỉ, có thể sửa object qua địa chỉ đó, nhưng không đổi được biến của caller.

public static void reset(List<String> list) {
    list = new ArrayList<>();   // chi sua bien local, caller khong biet
}

public static void clearAll(List<String> list) {
    list.clear();               // sua object thuc te, caller thay
}

6. static vs instance — bây giờ chỉ cần biết tạm

Bạn sẽ thấy static trong mọi method của bài này. Tạm hiểu:

  • static method — thuộc class, không cần tạo object. Gọi qua tên class: MathUtils.add(1, 2).
  • Instance method — thuộc object, phải tạo object trước rồi gọi qua object: myCalc.add(1, 2).

Module OOP sắp tới sẽ đào sâu. Hiện tại, vì chưa học class/object đầy đủ, bài này dùng static hết để tập trung vào method mechanics.

⚠️ ⚠️ Tại sao mọi method của bài này đều static?

Đơn giản: mainstatic, và static method chỉ gọi trực tiếp static method khác. Gọi instance method phải qua object — cần khái niệm class/object chưa học. Sang module OOP, bạn sẽ biết khi nào chọn static và khi nào không.

7. Convention đặt tên

  • camelCase bắt đầu bằng chữ thường: add, computeTotal, findUserById.
  • Động từ hoặc cụm động từ — method làm một việc: save, load, validate, parse.
  • get<X> / set<X> cho truy xuất / gán property (convention JavaBean).
  • is<X> / has<X> / can<X> trả boolean: isEmpty, hasPermission, canEdit.
  • Ngắn gọn, rõ ý. calculate tốt hơn doCalcStuff. processOrder tốt hơn proc.

Tên method kém là dấu hiệu logic không rõ trong đầu bạn. Dừng lại đặt tên đến khi đọc 1 giây hiểu ngay — thường kéo theo thiết kế method cũng tốt.

8. Pitfall tổng hợp

Nhầm 1: Quên return ở method non-void.

public static int abs(int x) {
    if (x >= 0) return x;
    // quen return cho nhanh x < 0 -> compile error
}

✅ Đảm bảo mọi nhánhreturn. Compiler sẽ bắt giúp bạn.

Nhầm 2: Tưởng đổi tham số trong method làm đổi biến ngoài.

public static void addOne(int n) { n++; }
int x = 5; addOne(x); System.out.println(x);   // van 5

✅ Return giá trị mới: public static int addOne(int n) { return n + 1; } rồi x = addOne(x);.

Nhầm 3: Method quá dài (100+ dòng) dồn nhiều trách nhiệm. ✅ Single Responsibility — method làm 1 việc. Nếu tên method có chữ "And" ("parseAndValidate"), khả năng cao nên tách 2.

Nhầm 4: Tên method là danh từ.

public static int calculation(int a, int b) { ... }   // nghe nhu mot bien

✅ Động từ: calculate, computeCost, sumOf.

Nhầm 5: Return type không khớp signature override (sẽ gặp ở OOP). ✅ Khi override, return type phải là kiểu con hoặc cùng kiểu của method cha (covariant return).

9. 📚 Deep Dive Oracle

ℹ️ 📚 Deep Dive Oracle (optional)

Spec / reference chính thức:

Ghi chú: JLS §8.4.2 phân biệt rõ "signature" (source level) và "descriptor" (bytecode level). Source signature bỏ return type; bytecode descriptor có. Đây là lý do bạn không overload được chỉ bằng đổi return type, dù ở bytecode chúng trông là 2 method khác nhau.

10. Tóm tắt

  • Method = đóng gói một đoạn việc vào một cái tên + input (parameters) + output (return).
  • Signature = tên + parameter list. Return type không nằm trong signature (ở source level).
  • Method non-void phải có returnmọi nhánh thoát. void không return hoặc return; trống.
  • Java luôn pass-by-value — đổi parameter không đổi biến caller; sửa object qua reference thì thấy.
  • Call stack quản lý chuỗi gọi; mỗi method đang chạy có stack frame riêng với local variable riêng.
  • Stack trace = ảnh chụp call stack khi exception. Đọc từ trên xuống, tìm dòng đầu tiên trong code của bạn.
  • static method = thuộc class, gọi qua ClassName.method(...). Instance method cần object — sẽ học ở OOP.
  • Tên method: động từ, camelCase, ngắn rõ ý.

11. Tự kiểm tra

Tự kiểm tra
Q1
Đoạn sau compile được không?
public static int classify(int x) {
    if (x > 0) return 1;
    if (x < 0) return -1;
}

Không. Compiler báo "missing return statement". Khi x == 0, không có nhánh nào match → method thoát mà không return → vi phạm rule "mọi đường thoát của method non-void phải có return".

Fix: thêm return 0; ở cuối, hoặc chuyển 2 nhánh đầu thành else if/else, hoặc dùng switch expression để compiler thấy exhaustive.

Q2
Hai method sau có hợp lệ trong cùng class không?
public static int compute(int a, int b) { return a + b; }
public static double compute(int a, int b) { return a + b; }
Không hợp lệ — compile error "method compute is already defined". Signature ở source level = tên + parameter list, không gồm return type. Hai method này cùng tên compute, cùng parameter list (int, int) → cùng signature → đụng nhau. Đổi return type không tạo method khác. Muốn 2 method cùng tên, phải đổi số lượng hoặc kiểu parameter (đây là overloading, bài sau).
Q3
Đoạn sau in gì?
public static void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

public static void main(String[] args) {
    int x = 1, y = 2;
    swap(x, y);
    System.out.println(x + " " + y);
}

In 1 2không bị swap.

Java là pass-by-value: swap nhận bản copy của x, y. Trong swap, hai biến local a, b được hoán đổi, nhưng xy của caller hoàn toàn không bị đụng.

Trong Java không có cách viết swap cho primitive. Phải return tuple qua int[], record, hoặc đổi design (tự làm tại call site).

Q4
Đọc stack trace sau và nói lỗi xảy ra ở đâu trong code của bạn:
Exception in thread "main" java.lang.NullPointerException
    at java.base/java.util.Objects.requireNonNull(Objects.java:208)
    at UserService.save(UserService.java:42)
    at App.main(App.java:12)

Lỗi xảy ra ở UserService.save dòng 42 — đó là dòng đầu tiên trong code của bạn. Dòng trên cùng là Objects.requireNonNull trong JDK internal (gói java.base) — đây là chỗ NPE được ném, nhưng không phải là nơi bạn gây ra lỗi.

Cách đọc: bỏ qua các dòng java.base/..., java.util.*, sun.* — tìm dòng đầu tiên thuộc package của project. Dòng sau là ai đã gọi dòng đó (App.main dòng 12 — gọi save). Hiểu cơ chế call stack giúp trace chuỗi gọi trong 30 giây.

Q5
Vì sao gần như mọi method trong module này đều static?

main(String[] args)static — JVM gọi nó mà không cần tạo object. Từ static method, bạn chỉ gọi trực tiếp static method khác (instance method cần một object cụ thể để "sở hữu" lời gọi).

Module này tập trung vào cơ chế method (signature, parameter, return, call stack) mà chưa học class/object đầy đủ — dùng static cho đơn giản. Ở module OOP sau, bạn sẽ hiểu khi nào để method là instance (gắn với trạng thái object) và khi nào để static (thao tác thuần túy trên input).


Bài tiếp theo: Overloading và overriding — cùng tên nhưng khác việc