Command Palette

Search for a command to run...

[Java Basics] Phương thức trong Java: khai báo và gọi method

Method là một khối code có tên, và bạn gọi nó bằng chính cái tên đó. Đúng một ý tưởng đó thôi cũng đủ để một chương trình Java thôi là một dãy statement và bắt đầu có hình dạng: hành vi được đặt tên, cái tên đó test được, và cùng một đoạn code không còn bị dán đi dán lại ba lần.

Bài này nói về khai báo method, gọi method, static thay đổi điều gì ở chỗ gọi, return thực sự làm gì, và call stack của JVM đang làm gì trong lúc tất cả những chuyện đó diễn ra. Mọi message lỗi và mọi dòng output bên dưới đều là thật, chạy trên OpenJDK 21.0.6.

Phương thức trong Java: khai báo và gọi method

Parameter ở đây chỉ dừng ở phần syntax. Argument thực sự được truyền như thế nào, parameter khác argument ra sao, và chuyện gì xảy ra khi hai method trùng tên — tất cả là việc của bài tiếp theo.

Vì sao cần method

Đây là một chương trình không tự viết method nào, tính thuế cho ba đơn hàng:

public class Duplicated {
    public static void main(String[] args) {
        double subtotal1 = 120.0;
        double tax1 = subtotal1 * 0.08;
        double total1 = subtotal1 + tax1;
        System.out.printf("Order 1: subtotal %.2f, tax %.2f, total %.2f%n", subtotal1, tax1, total1);

        double subtotal2 = 45.5;
        double tax2 = subtotal2 * 0.08;
        double total2 = subtotal2 + tax2;
        System.out.printf("Order 2: subtotal %.2f, tax %.2f, total %.2f%n", subtotal2, tax2, total2);

        double subtotal3 = 999.99;
        double tax3 = subtotal3 * 0.08;
        double total3 = subtotal3 + tax3;
        System.out.printf("Order 3: subtotal %.2f, tax %.2f, total %.2f%n", subtotal3, tax3, total3);
    }
}
Order 1: subtotal 120.00, tax 9.60, total 129.60
Order 2: subtotal 45.50, tax 3.64, total 49.14
Order 3: subtotal 999.99, tax 80.00, total 1079.99

Cùng bốn dòng đó lặp lại ba lần, chỉ khác mấy con số. Gom chúng vào một method là hết lặp:

public class Refactored {
    public static void main(String[] args) {
        printOrder(1, 120.0);
        printOrder(2, 45.5);
        printOrder(3, 999.99);
    }

    static void printOrder(int number, double subtotal) {
        double tax = subtotal * 0.08;
        double total = subtotal + tax;
        System.out.printf("Order %d: subtotal %.2f, tax %.2f, total %.2f%n", number, subtotal, tax, total);
    }
}

Output giống nhau đến từng byte. Đây không phải chuyện phải tin lời — cứ hứng cả hai ra file rồi so:

java Duplicated > d.txt && java Refactored > r.txt && diff d.txt r.txt && echo IDENTICAL
IDENTICAL

Hành vi của chương trình không đổi chút nào. Cái đổi là mọi thứ xung quanh nó:

TrướcSau
Thuế suất xuất hiện ba lầnXuất hiện đúng một lần
Sửa một bug là sửa ba chỗSửa một chỗ
Đoạn logic không có tênĐoạn logic tên là printOrder
Không test riêng phần tính toán đượcGọi printOrder từ test được
Thêm đơn hàng thứ tư là thêm mười hai dòngThêm đơn hàng thứ tư là thêm một dòng

Dòng cuối chính là lý do method tồn tại. Tái sử dụng là lợi ích dễ thấy; đặt tên mới là lợi ích sâu hơn. printOrder(3, 999.99) nói cho bạn biết chuyện gì xảy ra. Bốn dòng mà nó thay thế chỉ nói làm thế nào.

Cấu trúc một method declaration

Một declaration có hình dạng cố định: các modifier, một return type, một cái tên, một parameter list trong ngoặc tròn, và một body trong ngoặc nhọn.

public class Anatomy {
    public static double toFahrenheit(double celsius) {
        double fahrenheit = celsius * 9 / 5 + 32;
        return fahrenheit;
    }

    public static void main(String[] args) {
        System.out.println(toFahrenheit(100));
    }
}
212.0

Cấu trúc một method declaration: access modifier, static, return type, tên, parameter list, body và return, từng phần được chú thích

Bốn trong số các từ đó bạn đã gặp rồi. Ở bài 4, public static void main(String[] args) đã được mổ xẻ theo từng keyword, và lý do mỗi từ là bắt buộc ở đó là một sự thật về launcher của JVM: nó phải nhìn thấy được main, phải gọi được main khi chưa có object nào, và phải tìm đúng cái tên đó. Ở đây cũng những từ ấy nhưng nằm trên một method bình thường, nơi chúng chỉ là modifier với ý nghĩa bình thường và không có launcher nào dính vào.

Thành phầnTrong ví dụTác dụng
Access modifierpublicAi được phép gọi method này. Không bắt buộc; bỏ đi thì method chỉ thấy được trong package của nó.
staticstaticMethod thuộc về class chứ không thuộc về object. Không bắt buộc.
Return typedoubleType của giá trị method trả về. Bắt buộc — không có mặc định.
TêntoFahrenheitCách bạn gọi nó. camelCase, theo convention là một cụm động từ.
Parameter list(double celsius)Các input, mỗi cái có một type và một tên. Có thể rỗng: ().
Body{ ... }Các statement chạy khi method được gọi.

Hai điểm trong đó nên chốt lại ngay. Return type không phải tùy chọn — bỏ nó ra thì compiler không đoán giúp:

public class NoReturnType {
    greet(String name) {
        System.out.println("Hi " + name);
    }

    public static void main(String[] args) {
        System.out.println("x");
    }
}
NoReturnType.java:2: error: invalid method declaration; return type required
    greet(String name) {
    ^
1 error

parameter list có thể rỗng, nhưng cặp ngoặc thì không được bỏ. describe() với khoảng trống giữa hai ngoặc là một declaration hoàn toàn bình thường.

void: return type không trả về gì

void là một return type như mọi return type khác, chỉ khác ở chỗ nó nghĩa là "không có giá trị nào cả". Một void method được gọi vì việc nó làm — in ra, ghi file, thay đổi một thứ gì đó — chứ không phải vì thứ nó đưa lại.

Hệ quả thực tế là lời gọi một void method không phải một expression, nên không đứng được ở chỗ cần một giá trị:

public class VoidExpr {
    static void log(String msg) {
        System.out.println(msg);
    }

    public static void main(String[] args) {
        String s = log("hi");
        System.out.println(s);
    }
}
VoidExpr.java:7: error: incompatible types: void cannot be converted to String
        String s = log("hi");
                      ^
1 error

printOrder ở trên là void: nó in ra và không trả về gì. Còn toFahrenheit trả về một double, nên System.out.println(toFahrenheit(100)) chạy được.

Cách gọi một method

Một lời gọi gồm tên method, rồi các argument trong ngoặc tròn, rồi dấu chấm phẩy nếu lời gọi đứng riêng thành một statement. Gọi được từ đâu thì tùy vào nơi method đó nằm:

public class CallOrder {
    public static void main(String[] args) {
        System.out.println("main starts");
        greet("Hoang");
        System.out.println("sum = " + add(2, 3));
    }

    static void greet(String name) {
        System.out.println("Hello, " + name);
        System.out.println(shout("welcome"));
    }

    static String shout(String text) {
        return text.toUpperCase() + "!";
    }

    static int add(int a, int b) {
        return a + b;
    }
}
main starts
Hello, Hoang
WELCOME!
sum = 5

Có ba chuyện diễn ra ở đó. main gọi greet, cho thấy một method gọi được từ main. greet gọi shout, cho thấy một method gọi được method khác — main chẳng có gì đặc biệt ở khoản này. Và add(2, 3) được dùng bên trong một expression: vì add trả về int, lời gọi đó đứng được ở bất cứ chỗ nào cần một int.

Một static method ở class khác thì gọi qua tên class:

class MathUtils {
    static int square(int n) {
        return n * n;
    }
}

public class UseUtils {
    public static void main(String[] args) {
        System.out.println(MathUtils.square(7));
    }
}
49

Trong cùng một class bạn cũng viết tên class được — UseUtils.something() — nhưng không ai viết vậy; gọi tên trần là đủ hiểu.

Thứ tự khai báo trong class không quan trọng

main trong CallOrder gọi greet, mà greet được khai báo bên dưới nó, rồi greet gọi shout nằm dưới nữa. Nó compile và chạy đúng như trên. Đây không phải trường hợp đặc biệt: phần thân của một class không được đọc từ trên xuống theo kiểu các statement trong một method body. Compiler thu thập toàn bộ member của class trước, rồi mới kiểm tra từng body, nên method nào cũng thấy được method nào bất kể thứ tự.

Đây là khác biệt thật so với những ngôn ngữ bắt phải khai báo trước khi dùng, và nó có nghĩa là thứ tự method thuần túy là quyết định về khả năng đọc. Convention phổ biến là đặt main lên trước, rồi các method phụ trợ bên dưới, xếp đại khái theo thứ tự được gọi.

static method so với instance method

Bỏ static khỏi một declaration thì method thành instance method: nó thuộc về một object chứ không thuộc về class. Còn static method thuộc về chính class, và được gọi theo dạng ClassName.method().

Khác biệt đó hôm nay chỉ dẫn tới đúng một hệ quả, và đó lại chính là thứ chặn phần lớn người mới ngay lần đầu họ viết thêm method thứ hai. mainstatic, nên nó chạy khi chưa có object nào của class tồn tại. Từ bên trong đó, không có gì để một instance method được gọi lên trên:

public class StaticTrap {
    public static void main(String[] args) {
        System.out.println(describe());
    }

    String describe() {
        return "I belong to an object";
    }
}

static method gọi thẳng được từ static main, còn instance method cần một object trước, kèm lỗi javac thật và cả hai cách sửa

Lỗi mà ai mới học cũng gặp

StaticTrap.java:3: error: non-static method describe() cannot be referenced from a static context
        System.out.println(describe());
                           ^
1 error

Hãy đọc message theo đúng nghĩa đen, vì nó rất chính xác. describe() là non-static, nên gọi nó cần có một object. main là một static context, nên không có object nào trong tầm với. Compiler không hề bối rối; nó đang báo cho bạn biết là thiếu receiver.

Hai cách sửa

Cho method thành static luôn. Nếu method không cần dữ liệu riêng của từng object — mà một method chỉ format hay tính toán thì thường là không cần — đây là cách sửa đúng:

public class StaticTrap {
    public static void main(String[] args) {
        System.out.println(describe());
    }

    static String describe() {
        return "I belong to the class";
    }
}
I belong to the class

Hoặc tạo một object rồi gọi method trên object đó. new StaticTrap() tạo ra một object, và lời gọi đi qua reference đó:

public class StaticTrap {
    public static void main(String[] args) {
        StaticTrap app = new StaticTrap();
        System.out.println(app.describe());
    }

    String describe() {
        return "I belong to an object";
    }
}
I belong to an object

Cả hai đều compile và đều chạy. Chọn cách nào thì tùy method đó có cần state thuộc về một object cụ thể hay không — mà object, new, field và this là chủ đề của bài 23, ở đó mới nói cho ra ngô ra khoai. Từ giờ đến lúc đó, cứ đánh dấu các method phụ trợ là static thì câu hỏi này không phát sinh.

Chiều ngược lại, tiện thể nói luôn, thì luôn hợp lệ: một instance method gọi một static method thoải mái, vì class thì lúc nào cũng sẵn ở đó.

Câu lệnh return

return làm hai việc cùng lúc: nó trả một giá trị về cho phía gọi, và nó kết thúc method ngay lập tức. Nửa sau rất hay bị quên.

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

Type của expression được trả về phải gán được vào return type đã khai báo. return a + b; trong một method khai báo int thì ổn; trả một String từ đó thì không.

Return sớm: guard clause

return thoát ngay, bạn có thể xử lý mấy trường hợp lằng nhằng trước rồi để nhánh chính không phải thụt vào. Kiểu viết đó gọi là guard clause:

public class Guard {
    static String classify(int score) {
        if (score < 0) {
            return "invalid";
        }
        if (score > 100) {
            return "invalid";
        }
        if (score >= 80) {
            return "excellent";
        }
        if (score >= 50) {
            return "pass";
        }
        return "fail";
    }

    static void report(String name, int score) {
        if (name == null || name.isBlank()) {
            System.out.println("skipped: no name");
            return;
        }
        System.out.println(name + " -> " + classify(score));
    }

    public static void main(String[] args) {
        report("Hoang", 91);
        report("Lan", 64);
        report("Minh", 12);
        report("", 100);
        System.out.println(classify(-5));
    }
}
Hoang -> excellent
Lan -> pass
Minh -> fail
skipped: no name
invalid

classify có năm câu return và không có else nào cả. Một khi một trong số đó chạy, phần còn lại của method không bao giờ chạy nữa — chính điều đó làm cho dãy if phẳng lì kia là đúng chứ không phải cẩu thả.

return trần trong một void method

report trong cùng chương trình cho thấy dạng còn lại: return; không kèm giá trị. Trong một void method đó là hợp lệ và có nghĩa là "dừng ở đây". Đây là cách chuẩn để thoát sớm khỏi một method:

static void report(String name, int score) {
    if (name == null || name.isBlank()) {
        System.out.println("skipped: no name");
        return;
    }
    System.out.println(name + " -> " + classify(score));
}

Lời gọi rỗng report("", 100) in ra skipped: no name và hết — dòng cuối không bao giờ chạy. Một void method không có return nào thì đơn giản là trả về khi chạy tới dấu ngoặc đóng của nó, và đó là lý do phần lớn void method chẳng cần return.

missing return statement

Một method có return type khác void bắt buộc phải trả về một giá trị trên mọi đường ra khỏi nó. Compiler kiểm tra chuyện này, và kiểm tra theo cấu trúc chứ không suy luận theo logic của bạn:

public class MissingReturn {
    static String grade(int score) {
        if (score >= 50) {
            return "pass";
        }
    }

    public static void main(String[] args) {
        System.out.println(grade(60));
    }
}
MissingReturn.java:6: error: missing return statement
    }
    ^
1 error

Dấu mũ chỉ vào ngoặc đóng của grade, và đó là chỗ trung thực nhất: đó chính là đường ra mà compiler tìm thấy nhưng không có return nào trên đó. Để ý là lời gọi là grade(60), tức là sẽ đi vào nhánh có return — không liên quan. javac không chạy chương trình của bạn; nó chỉ kiểm tra rằng có tồn tại một đường như thế.

unreachable statement

Trường hợp đối xứng: code đặt sau một return trong cùng block thì không bao giờ chạy được, và Java từ chối compile chứ không lặng lẽ bỏ qua.

public class Unreachable {
    static int twice(int n) {
        return n * 2;
        System.out.println("never runs");
    }

    public static void main(String[] args) {
        System.out.println(twice(21));
    }
}
Unreachable.java:4: error: unreachable statement
        System.out.println("never runs");
        ^
1 error

Đây là một lỗi thật sự hữu ích. Nó thường có nghĩa là bạn đã kéo một return lên trên trong lúc debug rồi bỏ quên cái đuôi cũ ở dưới.

Call stack: mỗi lời gọi một frame

Khi một method được gọi, JVM push một stack frame cho nó. Frame đó giữ các local variable và parameter của riêng lời gọi đó, cùng với chỗ để nhảy ngược về. Khi method return, frame của nó bị pop và phía gọi chạy tiếp với giá trị trả về trong tay.

Ba tầng là đủ để thấy rõ hình dạng đó:

public class CallStack {
    public static void main(String[] args) {
        System.out.println("main: start");
        int result = level1(10);
        System.out.println("main: got " + result);
    }

    static int level1(int n) {
        System.out.println("  level1: n = " + n);
        int r = level2(n + 5);
        System.out.println("  level1: returning " + r);
        return r;
    }

    static int level2(int n) {
        System.out.println("    level2: n = " + n);
        int r = level3(n * 2);
        System.out.println("    level2: returning " + r);
        return r;
    }

    static int level3(int n) {
        System.out.println("      level3: n = " + n + ", returning " + (n + 1));
        return n + 1;
    }
}
main: start
  level1: n = 10
    level2: n = 15
      level3: n = 30, returning 31
    level2: returning 31
  level1: returning 31
main: got 31

Cứ đọc phần thụt lề như là độ sâu. Luồng chạy đi vào ba tầng rồi quay ra qua đúng ba tầng đó theo chiều ngược lại, và phía gọi không làm gì cả trong lúc phía được gọi đang chạy.

Bảy lát cắt của call stack khi main gọi level1, level2 và level3, rồi các frame pop ra và 31 chảy ngược về

Để ý là cả ba method đều dùng một parameter tên n và một local tên r, mà chúng không hề đụng nhau. Mỗi lời gọi có frame riêng, nên mỗi lời gọi có bản sao riêng:

public class Frames {
    public static void main(String[] args) {
        int n = 1;
        System.out.println("main before: n = " + n);
        inner();
        System.out.println("main after:  n = " + n);
    }

    static void inner() {
        int n = 99;
        System.out.println("inner:       n = " + n);
    }
}
main before: n = 1
inner:       n = 99
main after:  n = 1

inner gán vào n của nó không ảnh hưởng gì tới n của main. Đó là hai variable khác nhau tình cờ viết giống nhau, sống trong hai frame khác nhau. Còn quy tắc chung về cái tên nào nhìn thấy được ở đâu là chủ đề của bài 21.

StackOverflowError: stack có đáy

Mỗi frame chiếm bộ nhớ, và stack của một thread có kích thước cố định. Một method tự gọi chính nó mà không có cách nào dừng lại sẽ dùng cạn chỗ đó:

public class Overflow {
    static int depth = 0;

    static void dig() {
        depth++;
        dig();
    }

    public static void main(String[] args) {
        try {
            dig();
        } catch (StackOverflowError e) {
            System.out.println("depth reached: " + depth);
            throw e;
        }
    }
}

Chạy lên thì nó in ra bộ đếm, rồi những dòng đầu của trace:

depth reached: 45922
Exception in thread "main" java.lang.StackOverflowError
	at Overflow.dig(Overflow.java:6)
	at Overflow.dig(Overflow.java:6)
	at Overflow.dig(Overflow.java:6)
	at Overflow.dig(Overflow.java:6)
	at Overflow.dig(Overflow.java:6)

Hai chi tiết trong output đó rất đáng nhớ. Con số độ sâu đó tùy vào từng máy chứ không phải hằng số của ngôn ngữ — chạy lại nhiều lần trên cùng một máy cho ra 45922, 46051 và 46161, còn chạy với stack nhỏ hơn, java -Xss256k Overflow, chỉ tới 1479. Và trace bị cắt bớt: mặc định JVM chỉ in tối đa 1024 frame, điều khiển bằng -XX:MaxJavaStackTraceDepth, nên thứ bạn nhìn thấy chỉ là phần đỉnh của một stack sâu vài chục nghìn frame.

Cũng để ý là StackOverflowError là một Error chứ không phải Exception. Bắt nó như trên thì ổn cho một ví dụ minh họa, và là ý tồi trong code thật.

⚠️ Một method tự gọi chính nó là recursion, và khi dùng đúng thì đó là kỹ thuật bình thường với một base case để dừng lại. Đó là chủ đề của bài 22. Ở đây nó xuất hiện chỉ vì recursion không có điểm dừng là cách rẻ nhất để nhìn thấy call stack thực sự hết chỗ.

Cách đọc một stack trace

Stack trace là call stack được in ra, frame trong cùng đứng trước. Đọc được nó rồi thì phần lớn lỗi runtime hết bí ẩn. Bạn có thể in ra một cái bất cứ lúc nào mà không cần throw gì cả, bằng Thread.dumpStack():

public class Trace {
    public static void main(String[] args) {
        System.out.println("main: calling outer()");
        outer();
        System.out.println("main: back, still alive");
    }

    static void outer() {
        inner();
    }

    static void inner() {
        Thread.dumpStack();
    }
}
main: calling outer()
java.lang.Exception: Stack trace
	at java.base/java.lang.Thread.dumpStack(Thread.java:2210)
	at Trace.inner(Trace.java:13)
	at Trace.outer(Trace.java:9)
	at Trace.main(Trace.java:4)
main: back, still alive

Đọc từ dưới lên thì đó là chuỗi lời gọi: main ở dòng 4 gọi outer, outer ở dòng 9 gọi inner, inner ở dòng 13 gọi dumpStack. Đọc từ trên xuống thì đó là "tôi đang ở đâu, và tôi tới đây bằng đường nào". Tiền tố java.base/ đánh dấu một frame đến từ module của JDK chứ không phải code của bạn. Chương trình vẫn chạy tiếp sau đó, và đó chính là điểm hay của dumpStack — nó in ra rồi return.

Đúng cấu trúc ấy xuất hiện khi có thứ gì đó hỏng thật, tức là lúc bạn thực sự cần tới nó:

public class Boom {
    public static void main(String[] args) {
        System.out.println("main: start");
        loadUser(3);
    }

    static void loadUser(int id) {
        String name = lookupName(id);
        System.out.println("user " + id + " is " + name.toUpperCase());
    }

    static String lookupName(int id) {
        if (id == 3) {
            return null;
        }
        return "user" + id;
    }
}
main: start
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "<local1>" is null
	at Boom.loadUser(Boom.java:9)
	at Boom.main(Boom.java:4)

Bốn điều rút ra từ trace đó:

  • Frame trên cùng là nơi nổ, không phải nơi có bug. Dòng 9 là name.toUpperCase(); sai lầm thật nằm ở chỗ lookupName trả về null ở dòng 14.
  • Các frame bên dưới cho biết bạn tới đó bằng đường nào. Boom.main(Boom.java:4) là lời gọi khởi đầu mọi thứ.
  • lookupName không hề có trong trace — nó đã return xong và frame của nó đã bị pop trước khi lỗi xảy ra. Stack trace cho thấy stack tại thời điểm throw, chứ không phải lịch sử mọi lời gọi từng diễn ra.
  • Message NullPointerException chi tiết nêu luôn expression, đây là tính năng của Java 14 và bật mặc định từ Java 15.

Điểm cuối có một chi tiết phụ đáng biết. Message ở trên ghi "<local1>" vì class được compile mà không kèm tên local variable vào class file. Compile với javac -g thì cũng lần chạy đó sẽ gọi đúng tên variable:

javac -g Boom.java && java Boom
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "name" is null
	at Boom.loadUser(Boom.java:9)
	at Boom.main(Boom.java:4)

Phần lớn build tool đã truyền sẵn -g, và đó là lý do trace từ một project Maven hay Gradle thì gọi đúng tên variable, còn trace từ javac trần đôi khi thì không.

Đặt tên và giữ method đủ nhỏ

Những gì nói ở đây là convention chứ không phải syntax, nhưng trong Java chúng được theo gần như tuyệt đối và người review sẽ soi bạn bằng chúng.

Đặt tên theo việc nó làm, dưới dạng một cụm động từ. calculateTax, findUserById, isValid, toFahrenheit. Method là một hành động, nên tên nó phải đọc lên như một hành động. Danh từ để dành cho variable và class.

Loại methodConventionVí dụ
Làm một việc gì đócụm động từprintOrder, saveDocument, sendEmail
Trả về một giá trịcụm động từ mô tả giá trị đócalculateTotal, findUser, parsePrice
Trả về booleantiền tố is, has, canisValid, hasPermission, canRetry
Đọc một fieldtiền tố getgetName, getSize
Ghi một fieldtiền tố setsetName, setSize
Chuyển đổitiền tố totoString, toFahrenheit

Mỗi method một việc. Nếu bạn phải dùng chữ "và" để mô tả một method làm gì thì nhiều khả năng đó là hai method. Dấu hiệu rõ nhất là cái tên trở nên mơ hồ: handleData, process, doStuff đều là tên của những method ôm đồm quá nhiều.

public class Design {
    public static void main(String[] args) {
        String[] raw = {"12.50", "abc", "-3", "100"};
        double total = 0;
        for (String s : raw) {
            if (!isValidPrice(s)) {
                System.out.println("rejected: " + s);
                continue;
            }
            total += parsePrice(s);
        }
        System.out.println(formatMoney(total));
    }

    static boolean isValidPrice(String text) {
        try {
            return Double.parseDouble(text) >= 0;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    static double parsePrice(String text) {
        return Double.parseDouble(text);
    }

    static String formatMoney(double amount) {
        return String.format("$%.2f", amount);
    }
}
rejected: abc
rejected: -3
$112.50

Kiểm tra hợp lệ, parse và format là ba mối quan tâm tách biệt, nên chúng là ba method. Mỗi cái hiểu được, test được và sửa được một cách độc lập, còn main đọc lên như một bản mô tả quy trình chứ không phải phần cài đặt của quy trình đó.

Giữ chúng ngắn. Không có giới hạn nào về mặt cú pháp, nhưng một method mà bạn không nhìn hết được một lượt thì rất khó suy luận; khoảng một màn hình là trần thực tế quen thuộc, và phần lớn method tốt còn ngắn hơn thế nhiều. Method dài gần như luôn là mấy method chưa được tách ra.

Một ranh giới tốt là ranh giới mà bạn gọi tên chính xác được và mô tả được mà không cần nhắc tới phía gọi. isValidPrice(String) đạt: nó nhận text, nó trả lời một câu hỏi có-hay-không, và nó không quan tâm ai đang hỏi. Một method tên handleThirdLoopIteration thì trượt ở mọi tiêu chí.

Viết tài liệu cho method bằng Javadoc

Bài 4 đã giới thiệu ba dạng comment và nói rằng /** ... */ không chỉ là comment. Trên một method, đó là chỗ để mô tả các parameter và giá trị trả về, bằng @param@return:

/** Small geometry helpers. */
public class Geometry {

    /**
     * Computes the area of a rectangle.
     *
     * @param width  the width, in metres
     * @param height the height, in metres
     * @return the area in square metres
     */
    public static double rectangleArea(double width, double height) {
        return width * height;
    }

    public static void main(String[] args) {
        System.out.println(rectangleArea(3, 4));
    }
}

Compiler bỏ qua nó, nhưng tool javadoc của JDK biến nó thành HTML:

javadoc -d doc Geometry.java

Mở doc/Geometry.html lên và các tag đã thành từng mục có cấu trúc:

rectangleArea
public static double rectangleArea(double width, double height)
Computes the area of a rectangle.
Parameters:
  width - the width, in metres
  height - the height, in metres
Returns:
  the area in square metres

javadoc cũng nói cho bạn biết chỗ nào còn chưa viết tài liệu, và đó là một cú nhắc khá được việc:

Geometry.java:15: warning: no comment
    public static void main(String[] args) {
                       ^
TagDùng cho
@param nameMỗi parameter một dòng, theo thứ tự khai báo
@returnThứ method trả về. Bỏ qua với void method
@throwsMột exception mà phía gọi nên lường trước
@deprecatedVì sao không nên dùng nữa, và nên dùng gì thay thế

Câu đầu tiên là dòng tóm tắt hiện ra trong danh sách và trong tooltip của IDE, nên hãy viết nó thành một câu tử tế. Và hãy viết vì saocái gì, đừng viết làm thế nào — phần body đã nói làm thế nào rồi.

Những lỗi hay gặp và message chúng sinh ra

Gọi method mà quên cặp ngoặc. Chính cặp ngoặc mới biến nó thành một lời gọi. Thiếu nó thì compiler đi tìm một variable trùng tên:

public class NoParens {
    static int total() {
        return 42;
    }

    public static void main(String[] args) {
        System.out.println(total);
    }
}
NoParens.java:7: error: cannot find symbol
        System.out.println(total);
                           ^
  symbol:   variable total
  location: class NoParens
1 error

Dấu hiệu nhận biết là chữ symbol: variable trong khi trong đầu bạn đang nghĩ tới một method.

Khai báo một method bên trong một method khác. Java không có method lồng nhau. Parser bỏ cuộc theo kiểu đổ dây chuyền trông rất khó hiểu:

public class Nested {
    public static void main(String[] args) {
        static void helper() {
            System.out.println("nope");
        }
        helper();
    }
}
Nested.java:3: error: illegal start of expression
        static void helper() {
        ^
Nested.java:6: error: invalid method declaration; return type required
        helper();
        ^
Nested.java:8: error: class, interface, enum, or record expected
}
^
3 errors

Ba lỗi từ một sai lầm, và chỉ lỗi đầu tiên là thật. Sửa lỗi đầu rồi compile lại trước khi đọc mấy lỗi sau — lời khuyên đó đúng cho mọi chuỗi lỗi dây chuyền của javac.

Sai số lượng argument. Compiler liệt kê cả danh sách cần và danh sách tìm thấy, rồi nói rõ khác nhau chỗ nào:

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

    public static void main(String[] args) {
        System.out.println(add(1, 2, 3));
    }
}
ArgCount.java:7: error: method add in class ArgCount cannot be applied to given types;
        System.out.println(add(1, 2, 3));
                           ^
  required: int,int
  found:    int,int,int
  reason: actual and formal argument lists differ in length
1 error

Bỏ qua giá trị trả về. Cái này không phải lỗi gì cả, và đó mới là chỗ nguy hiểm. String.trim() trả về một string mới chứ không sửa string gốc, nên gọi nó rồi vứt kết quả đi là hợp lệ và vô ích:

public class IgnoreResult {
    public static void main(String[] args) {
        String s = "  hello  ";
        s.trim();
        System.out.println("[" + s + "]");

        s = s.trim();
        System.out.println("[" + s + "]");
    }
}
[  hello  ]
[hello]

javac không nói gì về lời gọi đầu tiên. Hễ một method trả về giá trị, hãy tự hỏi bạn đang làm gì với giá trị đó.

Quên mất rằng mainstatic. Đã nói ở trên, nhưng nó xứng đáng nằm trong danh sách này vì đó là cách phổ biến nhất khiến method thứ hai của người mới không compile được: non-static method ... cannot be referenced from a static context.

FAQ

Một method có tự gọi chính nó được không?

Được. Một method tự gọi chính nó là recursion, và đó là chuyện hợp lệ và bình thường — tính giai thừa hay duyệt cây thư mục thường được viết như vậy. Nó cần một base case để dừng các lời gọi lại, nếu không thì stack cạn như ví dụ Overflow ở trên. Recursion với tư cách một kỹ thuật là bài 22.

Method khác function ở chỗ nào?

Trong Java thì không khác gì đáng kể trong công việc hằng ngày: method chính là function của Java, và người ta gọi là "method" vì cái nào cũng thuộc về một class hay một interface. Java không có function đứng tự do như C hay JavaScript. Người từ ngôn ngữ khác sang hay nói "function" và ý cũng là thứ đó.

Hai method trong cùng một class trùng tên được không?

Được, miễn là parameter list của chúng khác nhau. Đó là method overloading, và đó là lý do System.out.println nhận được cả String, int lẫn double. Đây là chủ đề của bài tiếp theo, cùng với chuyện gì xảy ra khi compiler phải chọn giữa hai ứng viên.

Vì sao gọi method của tôi mà như không có gì xảy ra?

Gần như luôn là một trong hai chuyện. Hoặc method trả về một giá trị mà bạn đang vứt nó đi — s.trim(); đứng một mình chẳng thay đổi gì — hoặc method nhận một parameter và bạn tưởng gán vào parameter đó sẽ đổi được variable của phía gọi. Chuyện thứ hai thuộc về cách argument được truyền, và đó là điều đầu tiên bài tiếp theo xử lý.

Một method nên dài bao nhiêu?

Đủ ngắn để bạn giữ được cả nó trong đầu. Compiler không giới hạn, và mọi con số cứng đều tùy tiện, nhưng phép thử thực tế là bạn có đọc hết method mà không cần cuộn và nói được trong một câu nó làm gì hay không. Nếu không thì tách ra — và để tên của từng mảnh làm nhiệm vụ giải thích.

Tôi khai báo một method bên trong một method khác được không?

Không. Method là member của class, nên chúng chỉ xuất hiện trực tiếp trong thân của một class, interface, enum hay record. Thử làm sẽ ra chuỗi lỗi illegal start of expression ở trên. Thứ Java cho phép đặt bên trong một method là local class và lambda, nhìn thoáng qua thì hao hao nhưng là chủ đề của rất nhiều bài sau nữa.

Kết luận

Method là một khối code có tên và gọi được: các modifier, một return type, một cái tên, một parameter list, một body. static quyết định nó gắn vào class hay gắn vào object, và gọi một instance method từ static main mà không có object chính là lỗi bạn sẽ gặp đầu tiên. return vừa trả giá trị về vừa thoát ngay lập tức, và đó là thứ làm cho guard clause hoạt động, cũng là thứ làm cho missing return statementunreachable statement xuất hiện. Bên dưới tất cả, mỗi lời gọi push một frame giữ local variable của riêng nó và bị pop khi return — nhìn thấy được trong stack trace, và nghe thấy được qua StackOverflowError khi các frame hết chỗ nhét.

Thứ bài này cố tình chỉ dừng ở syntax là parameter list. Bài tiếp theo mổ nó ra cho đàng hoàng: parameter khác argument ở đâu, vì sao Java là pass-by-value và điều đó thực sự nghĩa là gì với object, varargs, và method overloading — làm sao một cái tên có nhiều signature và compiler chọn giữa chúng thế nào.

Bài viết liên quan

[Java Basics] Mảng một chiều trong Java: khai báo, khởi tạo và duyệt mảng

Hướng dẫn đầy đủ về mảng một chiều trong Java - mọi cách khai báo, giá trị mặc định, field length, index và ArrayIndexOutOfBoundsException, duyệt mảng bằng for và for-each, cái bẫy reference, copy thật với Arrays.copyOf và System.arraycopy, cùng Arrays.equals, mọi ví dụ đều compile và chạy trên JDK 21.

[Java Basics] Tính trừu tượng trong Java: abstract class và abstract method

Abstract class và abstract method trong Java: vì sao không tạo được object của Shape, abstract class chứa được những gì, template method pattern, anonymous subclass, thứ tự constructor, cùng mọi compile error chạy thật trên JDK 21.

[Java Basics] Tính kế thừa trong Java: extends và super

extends hoạt động thế nào, subclass kế thừa gì và không kế thừa gì, vì sao constructor không bao giờ được kế thừa, super(...) xâu chuỗi constructor lên tới java.lang.Object rồi chạy ngược xuống ra sao, field hiding khác overriding chỗ nào, protected qua package, final class, vấn đề fragile base class, và khi nào composition là lựa chọn đúng.

[Java Basics] Vòng lặp trong Java: for, while và do-while

Vòng lặp trong Java giải thích bằng cách chạy thật: thứ tự thực thi chính xác của header for, while so với do-while, enhanced for và lý do nó không ghi ngược vào array, lỗi off-by-one với length, và ba cách tạo ra vòng lặp vô hạn.