Đế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ường | Method |
|---|---|
| Tên công thức | Tên method (makePancake) |
| Nguyên liệu | Parameters (flour, shrimp) |
| Món trả ra | Return value (Pancake) |
| Sách công thức | Class 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ó:
- Modifier —
public static.static= method của class, không cần tạo object để gọi. Sẽ đào sâu ở phần 6. - Return type —
int(trả số) hoặcvoid(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ácvoid, bắt buộc córeturntrong 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:
- Overloading được: nhiều method cùng tên, khác parameter list. Bài sau.
- 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 có bao gồm return type. Ví dụ add(int,int) return int là add(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 computeTotal và a trong add là 2 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:
staticmethod — 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: main là static, 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õ ý.
calculatetốt hơndoCalcStuff.processOrdertốt hơnproc.
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ánh có return. 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:
- JLS §8.4 — Method Declarations — đầy đủ cú pháp, modifier, return type.
- JLS §8.4.2 — Method Signature — định nghĩa signature và quy tắc "same signature".
- JVMS §2.6 — Frames — stack frame structure: local variable array, operand stack, reference to constant pool.
- JVMS §4.3.3 — Method Descriptors — format descriptor
(II)Ichoint(int,int).
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ó
returnở mọi nhánh thoát.voidkhông return hoặcreturn;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.
staticmethod = thuộc class, gọi quaClassName.method(...). Instance method cần object — sẽ học ở OOP.- Tên method: động từ, camelCase, ngắn rõ ý.
11. 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;
}
▸
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.
Q2Hai 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; }
▸
public static int compute(int a, int b) { return a + b; }
public static double compute(int a, int b) { return a + b; }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);
}
▸
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 2 — khô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 x và y 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)
▸
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.
Q5Vì sao gần như mọi method trong module này đều static?▸
static?main(String[] args) là 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