Phương thức

Xem thử

Tình huống mở đầu

Hệ thống của quán Cà Phê Gió tính giá có thuế ở ba nơi: màn hình giỏ hàng, hoá đơn và email xác nhận. Cả ba cùng dùng quy tắc "cộng 10 phần trăm thuế", nhưng mỗi nơi tự viết một bản. Hôm thứ Hai, một bạn sửa cách làm tròn ở hoá đơn và quên hai nơi còn lại. Khách thấy một ly trà giá 5 đồng ghi 5 ở giỏ hàng và 6 ở hoá đơn. Không có lỗi biên dịch, không có dòng chữ đỏ nào, chỉ có hai con số khác nhau và một cuộc điện thoại khiếu nại.

Vậy làm sao để một quy tắc chỉ nằm ở một nơi, mà ba nơi kia vẫn dùng được? Hãy giữ câu hỏi này, cuối bài bạn sẽ tự tay làm điều đó.

Ôn nhanh bài trước: vòng while (x < 3) chạy bao nhiêu lần nếu x bắt đầu bằng 0 và mỗi vòng tăng 1?

Đáp án: 3 lần, với x bằng 0, 1 và 2. Khi x bằng 3 thì điều kiện sai và vòng dừng.

Mục tiêu bài học

Sau bài này, bạn có thể:

  • Đọc và viết được khai báo phương thức gồm kiểu trả về, tên, tham số và thân, rồi gọi nó với đối số.
  • Nói được lời gọi phương thức biến thành giá trị trả về ra sao, và phân biệt phương thức void với phương thức trả giá trị.
  • Vẽ được ngăn xếp lời gọi (các khung) tại thời điểm một phương thức đang chạy.
  • Đọc được thông báo lỗi biên dịch về phương thức: thiếu return, dùng void như một giá trị, thiếu đối số.

Bài này đo được bằng năm câu trong phần Tự kiểm tra, mỗi câu ghi rõ nó kiểm tra mục tiêu nào.

Hãy xem video dưới đây để có cái nhìn đầu tiên, sau đó mình đi chậm hơn từng bước ở các phần sau.

Bức tranh lớn (Big Picture)

Trước khi vào chi tiết, hãy xem một quy tắc được dùng ở ba nơi như thế nào khi nó nằm trong một phương thức.

Cách đọc hình: đọc từ trên xuống. Hàng trên là ba nơi cần giá có thuế: giỏ hàng, hoá đơn và email. Mỗi nơi có một mũi tên "calls" đi xuống ô withVat(net) ở giữa. Ô dưới cùng cho biết kết quả là cùng một đáp án ở cả ba nơi. Màu chỉ giúp tìm nhanh, mọi ô đều có nhãn chữ.

Phương thức là cách đặt tên cho một đoạn việc để gọi lại ở nhiều nơi. Quy tắc nằm trong phương thức, còn ba nơi chỉ gọi tên. Muốn đổi quy tắc, bạn sửa một chỗ.

Xuyên suốt cả chương Phương thức, mình dùng một hình ảnh: phương thức là một chiếc máy nhỏ có dán nhãn tên, bạn đưa nguyên liệu vào khe (tham số) và nhận thành phẩm ở khe ra (giá trị trả về). Dự án xuyên suốt là máy tính hoá đơn của quán Cà Phê Gió, và mỗi bài thêm một tính năng cho nó. Có chỗ hình ảnh chiếc máy không còn đúng, mình sẽ chỉ ra khi tới.

Khái niệm cốt lõi

Bài này có ba khái niệm, mỗi khái niệm đi kèm một hình.

Khái niệm 1: phương thức là một đoạn việc có tên

Phương thức (method) là một khối code có tên, nhận đầu vào, làm một việc và có thể trả về một kết quả. Trong hình ảnh của chúng ta, đó là chiếc máy có nhãn withVat.

Một khai báo phương thức có bốn phần quan trọng. Kiểu trả về cho biết thứ gì quay ra khỏi máy (long). Tên là nhãn dán lên máy (withVat). Tham số (parameter) là khe đưa nguyên liệu vào, thực chất là một biến cục bộ của phương thức (long net). Thân (body) là phần việc nằm trong cặp ngoặc nhọn. Khi gọi withVat(5), số 5 là đối số (argument): giá trị thật được đưa vào khe.

Cách đọc hình: hàng trên là bốn phần của dòng khai báo, từ trái sang phải: bổ nghĩa static, kiểu trả về long, tên withVat, tham số long net. Hàng dưới có hai ô: thân của phương thức và lời gọi withVat(5). Mũi tên "5 is copied into net" cho thấy đối số được chép vào tham số, còn mũi tên đứt nét từ thân lên kiểu trả về cho thấy giá trị mà thân trả ra phải hợp với kiểu đã khai báo.

Chữ ký (signature) của phương thức gồm tên và danh sách kiểu tham số. Kiểu trả về và thân không thuộc chữ ký. Bài Nạp chồng sẽ dùng khái niệm này.

Khái niệm 2: return, lời gọi biến thành giá trị

Khi phương thức chạy tới return, nó trao giá trị ra ngoài và kết thúc. Nơi gọi nhận giá trị đó thay chỗ cho chính lời gọi: dòng long t = withVat(5) hoạt động như long t = 6. Phương thức void thì không trả gì, nên bạn dùng nó như một câu lệnh, không gán nó vào đâu được.

Cách đọc hình: hàng trên là phương thức có giá trị trả về: lời gọi, thân chạy, rồi biến t giữ 6. Hàng dưới là phương thức void: lời gọi, thân chạy và in một dòng, nhưng ô cuối cho biết không có gì quay về, nên viết int n = printLine(...) sẽ không biên dịch.

Đây là khái niệm quan trọng nhất của bài, vì lỗi phổ biến nhất của người mới là gọi phương thức rồi quên lưu kết quả.

Chỗ hình ảnh chiếc máy không còn đúng: máy thật có thể in hoá đơn mà không đưa gì ra khe. Phương thức làm được điều đó, đó là void. Nhưng máy thật không bao giờ "biến" cái nút bấm thành thành phẩm, còn lời gọi phương thức thì bị thay hẳn bằng giá trị trả về.

Concept Check 1. Đoạn sau in ra gì?

java
long net = 5;
withVat(net);
System.out.println(net);
  • A. 6, vì withVat đã đổi net thành 6.
  • B. 5, vì kết quả 6 không được gán cho biến nào.
  • C. Lỗi biên dịch, vì không được bỏ kết quả của phương thức.
  • D. 5.5, vì chưa làm tròn.

Đáp án: B. A là hiểu lầm hay gặp nhất: phương thức nhận một bản sao của net, và kết quả chỉ tồn tại khi bạn lưu nó. C sai vì Java cho phép gọi phương thức mà bỏ kết quả, đó là lý do lỗi này im lặng. D sai vì withVat đã làm tròn bên trong, và dù sao net cũng không bị đổi.

Khái niệm 3: ngăn xếp lời gọi

Khi một phương thức gọi phương thức khác, máy ảo giữ một khung (frame) cho mỗi lời gọi đang dở. Các khung xếp chồng như chồng đĩa: lời gọi mới đặt khung lên đỉnh, return gỡ đúng khung trên cùng. Chồng đĩa đó gọi là ngăn xếp lời gọi (call stack).

Cách đọc hình: ba khung xếp từ dưới lên: main ở đáy, printLine ở giữa, withVat ở đỉnh. Hai mũi tên "called" chỉ ai gọi ai. Chỉ khung trên cùng đang chạy, hai khung bên dưới đứng chờ. Ô bên phải cho biết khi withVat trả về 49500 thì khung đó bị gỡ.

Vì chỉ khung trên cùng chạy, thứ tự gọi và thứ tự trả về luôn ngược nhau: ai được gọi sau thì xong trước.

Ví dụ đầu tiên (Worked Example) và "Xem từng bước"

Đây là phiên bản đầu tiên của máy tính hoá đơn: một phương thức tính thuế, một phương thức in dòng hoá đơn, và main gọi cả hai.

Bill.java

java
public class Bill {
    static long withVat(long net) {
        return Math.round(net * 1.1); // nhân 1.1 rồi làm tròn tới số nguyên gần nhất
    }

    static void printLine(String item, long net) {
        long gross = withVat(net); // lưu giá trị trả về, không lưu thì mất
        System.out.println(item + ": " + gross);
    }

    public static void main(String[] args) {
        printLine("Latte", 45000);
        printLine("Tea", 5);
        long net = 5;
        withVat(net); // cố ý không gán kết quả nên net không đổi
        System.out.println("net is still " + net);
    }
}

Predict: hãy dừng lại và đoán trên giấy nháp: chương trình in ra những dòng nào, và biến net ở dòng cuối bằng bao nhiêu? Đoán xong rồi hãy đọc tiếp.

Kết quả mong đợi [Đã chạy, JDK 17.0.15]:

text
Latte: 49500
Tea: 6
net is still 5

Đọc code từng dòng:

Dòng codeMáy làm gìVì sao viết như vậy
static long withVat(long net)Khai báo phương thức nhận một số nguyên dài và trả về một số nguyên dàistatic để gọi thẳng từ main không cần tạo đối tượng. Kiểu long đủ chứa số tiền lớn
return Math.round(net * 1.1);Nhân với 1.1, làm tròn tới số nguyên gần nhất rồi trả vềMath.round nhận số thực và trả về long, khớp kiểu trả về đã khai báo
static void printLine(String item, long net)Khai báo phương thức không trả gì, nhận một chuỗi và một sốvoid vì việc của nó là in ra, không cần trả kết quả cho ai
long gross = withVat(net);Gọi withVat với bản sao của net, rồi lưu giá trị trả về vào grossLưu kết quả vào biến để dùng ở dòng sau. Tham số net của printLine và của withVat là hai biến khác nhau
System.out.println(item + ": " + gross);In tên món, dấu hai chấm và giáGhép chuỗi với số bằng dấu +
printLine("Latte", 45000);Gọi printLine như một câu lệnhDùng như câu lệnh vì void không có giá trị để lưu
withVat(net);Gọi withVat, nhận 6 rồi bỏ điCố ý viết sai để bạn thấy: không gán thì net không đổi

Bây giờ xem từng bước. Mỗi bước nêu dòng code đang chạy, các khung trên ngăn xếp và lý do:

Đang tải Animation…

Bảng trace (bản chữ tương đương của animation, [Suy luận] bằng cách đi từng dòng và khớp với kết quả chạy thật):

BướcNgăn xếp từ đáy lên đỉnhGiá trị đáng chú ýĐiều xảy ra
1mainchưa có netChương trình bắt đầu
2main, printLineitem là Lattemain gọi printLine
3main, printLine, withVatnet của withVat là 45000printLine gọi withVat
4main, printLinegross là 49500withVat trả về 49500 và khung bị gỡ
5mainin Latte: 49500printLine kết thúc
6 đến 7main, printLine, withVat rồi mainnet là 5 rồi in Tea: 6Lần gọi thứ hai trả về 6
8 đến 9main, withVat rồi mainnet của main vẫn 56 bị bỏ đi vì không được gán

Bạn vừa làm được: đọc một chương trình có ba phương thức và nói đúng ngăn xếp có mấy khung, khung nào đang chạy, và giá trị nào bị bỏ đi.

Concept Check xen kẽ

Chọn đáp án của bạn trước, rồi đọc phần đáp án ngay bên dưới mỗi câu.

Concept Check 2. Phương thức static void log(String s) đã tồn tại. Điều gì xảy ra với dòng int n = log("x");?

  • A. n nhận 0, vì phương thức không trả gì.
  • B. n nhận null, vì không có giá trị trả về.
  • C. Chương trình chạy bình thường nhưng n chưa được gán.
  • D. Lỗi biên dịch, vì void không có giá trị để gán cho int.

Đáp án: D. Trình biên dịch báo incompatible types: void cannot be converted to int. A và B nhầm void với một giá trị mặc định: void nghĩa là không có giá trị, nên không có gì để gán. C sai vì lỗi xảy ra trước khi chương trình chạy.

Concept Check 3. Trong Bill, khi withVat đang tính giá cho Latte, ngăn xếp có mấy khung và khung nào đang chạy?

  • A. Ba khung, và withVat ở trên cùng đang chạy.
  • B. Một khung, vì chỉ withVat đang làm việc.
  • C. Ba khung, và main ở đáy đang chạy vì nó gọi đầu tiên.
  • D. Hai khung, vì printLine đã xong phần việc của nó.

Đáp án: A. main và printLine chưa kết thúc, chúng đang chờ withVat trả về nên khung của chúng vẫn nằm trên ngăn xếp. B sai vì khung chỉ bị gỡ khi phương thức trả về. C sai vì khung trên cùng mới là khung đang chạy. D sai vì printLine còn dòng println chưa chạy.

Làm cùng mình (Guided Practice)

Bạn sẽ dựng dần class Receipt qua năm bước nhỏ. Mỗi bước thêm vào chương trình của bước trước, hỗ trợ giảm dần: bước 1 gần như làm sẵn, bước 5 chỉ còn đề. Gõ code vào file Receipt.java rồi chạy bằng java Receipt.java.

Bước 1: viết phương thức thuế.

java
public class Receipt {
    static long withVat(long net) {
        // TODO 1: trả về net cộng 10 phần trăm, làm tròn bằng Math.round
        return 0;
    }

    public static void main(String[] args) {
        System.out.println("step 1: " + withVat(5) + " " + withVat(45000));
    }
}

Chạy xong bạn sẽ thấy: step 1: 6 49500. Gợi ý: nhìn lại dòng return trong ví dụ Bill.

Bước 2: thêm phương thức tính tiền một dòng. Thêm vào class, và thêm một dòng println vào main:

java
    static long lineTotal(long price, int qty) {
        // TODO 2: nếu qty nhỏ hơn hoặc bằng 0 thì trả về 0, ngược lại trả về price * qty
        return -1;
    }

Dòng thêm vào main: System.out.println("step 2: " + lineTotal(45000, 2) + " " + lineTotal(45000, 0));. Chạy xong bạn sẽ thấy: step 2: 90000 0.

Vẽ ra giấy: vẽ ngăn xếp khi lineTotal(45000, 0) đang chạy. Đối chiếu: có hai khung là main và lineTotal, và lineTotal trả về sớm ở dòng return 0 mà không chạy tới phép nhân.

Bước 3: ghép hai phương thức thành một dòng hoá đơn.

java
    static long printLine(String name, long price, int qty) {
        // TODO 3: tính gross từ withVat(lineTotal(price, qty)), in "name xqty: gross", rồi trả về gross
        return 0;
    }

Gọi printLine("Latte", 45000, 2); trong main. Chạy xong bạn sẽ thấy: Latte x2: 99000. Gợi ý: lời gọi bên trong withVat(...) được tính trước, rồi kết quả của nó thành đối số cho lời gọi bên ngoài.

Vẽ ra giấy: vẽ ngăn xếp tại thời điểm withVat đang chạy bên trong printLine. Đối chiếu: ba khung main, printLine, withVat, giống hình ngăn xếp ở khái niệm 3. Lưu ý lineTotal đã trả về xong nên không còn trên ngăn xếp.

Bước 4: cộng ba dòng hoá đơn. Bạn tự quyết cách viết. Trong main, gọi printLine cho Latte hai ly 45000, Tea không ly nào giá 30000, và Cake một cái 38000, rồi in tổng. Chạy xong bạn sẽ thấy ba dòng Latte x2: 99000, Tea x0: 0, Cake x1: 41800 và total: 140800. Gợi ý: printLine trả về gross, nên cộng ba lời gọi lại được.

Bước 5: đổi thuế thành 8 phần trăm. Chỉ sửa một dòng trong withVat. Chạy xong bạn sẽ thấy bốn dòng đổi sang 97200, 0, 41040 và total: 138240, trong khi printLine và main không sửa gì. Đây chính là điều tình huống mở đầu cần: một quy tắc, một nơi. Lời giải nằm ở cuối bài.

Bạn vừa làm được: dựng máy tính hoá đơn từ ba phương thức nhỏ, vẽ ngăn xếp của nó, và đổi một quy tắc mà chỉ sửa một dòng. Toàn bộ mã nguồn của bài cũng nằm trong thư mục 03-phuong-thuc/01-phuong-thuc của kho GitHub, nhưng bạn không cần mở nó để học.

Thử phá nó đi (Lỗi thường gặp)

Bốn lỗi dưới đây là những lỗi người mới gặp nhiều nhất với phương thức. Mỗi dòng đã được chạy thật [Đã chạy, JDK 17.0.15; chữ trong thông báo lỗi có thể khác ở phiên bản Java khác]. Với các lỗi biên dịch, mỗi đoạn được đặt trong một class A tối thiểu và chỉ lấy dòng đầu của thông báo.

Code lỗiThông báo lỗiNguyên nhân bằng lời thườngCách sửa
withVat(net); rồi in netKhông báo lỗi, net vẫn là 5Kết quả 6 được trả về nhưng không ai giữnet = withVat(net);
int sign(int x) { if (x > 0) { return 1; } }missing return statementKhi x không dương, phương thức chạy hết mà không returnThêm return cho mọi đường đi
int n = log("x"); với log là voidincompatible types: void cannot be converted to intvoid không có giá trị để gánĐổi log để trả về giá trị, hoặc bỏ phần gán
add(1) khi add cần hai tham số intmethod add in class A cannot be applied to given types;Thiếu đối số so với khai báoTruyền đủ hai đối số

Lỗi đầu tiên là lỗi nguy hiểm nhất vì máy không phàn nàn gì. Hình dưới đây gom mô hình sai và mô hình đúng.

Cách đọc hình: hàng trên màu đỏ nhạt là mô hình sai: nhiều người tưởng lời gọi withVat(net) tự cập nhật net thành 6, nên đoán dòng in ra là 6. Hàng dưới màu lục là mô hình đúng: lời gọi tạo ra giá trị 6 rồi bị bỏ đi, nên net vẫn là 5. Dòng ghi chú cuối hình cho cách sửa.

Mô hình đúng này chính là điều animation đã cho bạn thấy ở bước 8 và 9.

Bạn vừa làm được: đọc bốn lỗi phổ biến của phương thức và nói được nguyên nhân bằng lời thường.

Tự tay làm (Bài tập)

Làm lần lượt từng mức. Mỗi bài có đề, dữ liệu vào, kết quả mong đợi, tiêu chí nghiệm thu và gợi ý. Lời giải nằm riêng ở cuối bài, hãy thử trước rồi mới so.

Mức Cơ bản

Bài 1. Điền bảng trace. Dữ liệu vào là đoạn code sau:

java
static int twice(int n) { return n * 2; }
static int addOne(int n) { return n + 1; }

int a = 3;
int b = addOne(twice(a));
int c = twice(addOne(a));

Điền bảng gồm bốn cột: dòng, lời gọi được tính theo thứ tự, giá trị mỗi lời gọi trả về, biến nhận kết quả. Kết quả mong đợi: bảng cho b và c. Tiêu chí nghiệm thu: bạn nêu được giá trị của b và c, và giải thích vì sao hai số khác nhau dù cùng dùng hai phương thức. Gợi ý: lời gọi nằm bên trong ngoặc được tính trước.

Bài 2. Viết lineTotal. Dữ liệu vào: giá 45000 và số lượng 2, rồi số lượng 0. Viết static long lineTotal(long price, int qty) trả về price * qty, và trả về 0 nếu qty nhỏ hơn hoặc bằng 0. Kết quả mong đợi: 90000 và 0. Tiêu chí nghiệm thu: chạy đúng cả hai trường hợp, và trường hợp qty bằng 0 không đi tới phép nhân. Gợi ý: trả về sớm bằng một câu if ở đầu phương thức.

Bài 3. Sửa hai đoạn lỗi. Dữ liệu vào là hai đoạn sau.

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

long net = 100;
withVat(net);
System.out.println(net);

Kết quả mong đợi sau khi sửa: sign(7), sign(-3), sign(0) cho 1, -1, 0, và dòng in cho 110. Tiêu chí nghiệm thu: đoạn một biên dịch được, đoạn hai in ra 110, và bạn nêu được tên lỗi của từng đoạn. Gợi ý: đoạn một hỏi "khi x bằng 0 thì sao".

Mức Vận dụng

Bài 4. Hoá đơn ba dòng cho quán Cà Phê Gió. Viết class Receipt có ba phương thức withVat, lineTotal và printLine như ở phần Làm cùng mình, và main in ba dòng rồi tổng. Dữ liệu vào: Latte hai ly giá 45000, Tea không ly giá 30000, Cake một cái giá 38000. Kết quả mong đợi:

text
Latte x2: 99000
Tea x0: 0
Cake x1: 41800
total: 140800

Tiêu chí nghiệm thu: bốn dòng khớp từng ký tự, quy tắc thuế chỉ xuất hiện đúng một lần trong code, và printLine trả về giá trị để main cộng lại. Gợi ý: printLine vừa in vừa return gross.

Mức Thử thách (tuỳ chọn)

Bài 5. Làm tròn tới nghìn. Quán muốn giá cuối làm tròn tới 1000 đồng. Viết static long roundToThousand(long amount) rồi dùng nó trong printLine mà không sửa main. Dữ liệu vào: 49500, 49499 và 6. Kết quả mong đợi: 50000, 49000 và 0. Tiêu chí nghiệm thu: ba kết quả đúng, và bạn chỉ thêm một lời gọi vào printLine. Gợi ý: chia cho 1000.0, làm tròn bằng Math.round, rồi nhân lại 1000.

Tự kiểm tra (Retrieval Practice)

Trả lời bằng trí nhớ, không xem lại bài. Mỗi câu ghi rõ nó kiểm tra mục tiêu nào trong bốn mục tiêu ở đầu bài.

Câu 1 (nhớ lại, mục tiêu 1). Với static long withVat(long net), hãy gọi tên bốn phần của dòng khai báo và nói net là tham số hay đối số.

Câu 2 (dự đoán kết quả code, mục tiêu 2). Đoạn sau in ra những gì?

java
static int half(int n) { return n / 2; }
static void show(int n) { System.out.println(n); }

int a = half(9);
show(a);
show(half(a) + 1);

Câu 3 (dự đoán trạng thái sau nhiều bước, mục tiêu 3). main gọi a(), a gọi b(), b gọi c(). Viết ngăn xếp (từ đáy lên đỉnh) khi c đang chạy, rồi sau khi c trả về, rồi sau khi b trả về.

Câu 4 (tìm lỗi, mục tiêu 4). Phương thức dưới đây báo missing return statement. Hãy chỉ ra đường đi bị thiếu và sửa.

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

Câu 5 (giải thích cho bạn chưa học, mục tiêu 2). Bạn của bạn hỏi: "Mình gọi withVat(net); rồi sao net vẫn y nguyên?" Hãy giải thích bằng hình ảnh chiếc máy có khe vào và khe ra.

Điều cần nhớ

  • Một khai báo phương thức có kiểu trả về, tên, tham số và thân, và lời gọi truyền đối số vào tham số (mục tiêu 1).
  • Lời gọi phương thức được thay bằng giá trị trả về, nên phải lưu giá trị nếu muốn dùng; void thì không có giá trị (mục tiêu 2).
  • Mỗi lời gọi thêm một khung lên đỉnh ngăn xếp, mỗi return gỡ đúng khung trên cùng (mục tiêu 3).
  • Thiếu return, dùng void như giá trị và thiếu đối số đều bị trình biên dịch chặn trước khi chạy (mục tiêu 4).

Cheat sheet một trang.

Việc cần làmCú phápGhi chú
Khai báo phương thức có giá trịstatic long withVat(long net)Kiểu trả về đứng trước tên
Khai báo phương thức không trả gìstatic void printLine(...)Dùng khi mục đích là một tác dụng như in ra
Gọi và lưu kết quảlong gross = withVat(net);Không gán thì kết quả bị bỏ
Trả về sớmif (qty <= 0) { return 0; }Câu lệnh bảo vệ, tránh if lồng nhau
Chữ kýtên cộng kiểu tham sốKiểu trả về không thuộc chữ ký
Mọi đường đi phải kết thúcreturn ở cuốiNếu không sẽ có missing return statement
Hình: tóm tắt việc nào dùng công cụ phương thức nào

Cách đọc hình: mỗi hàng là một tình huống ở bên trái và việc nên làm ở bên phải. Đọc theo hàng ngang: cùng một quy tắc dùng nhiều nơi thì viết một phương thức, cần đáp án thì trả về và lưu, còn gặp missing return thì mọi đường đi phải có return.

Câu hỏi để dành cho ngày mai: khi bạn gọi withVat(net), tham số net của phương thức là một bản sao của biến net bên ngoài. Nếu phương thức gán net = 0 bên trong thì net bên ngoài có đổi không? Còn nếu truyền vào một đối tượng thì sao? Hãy nghĩ trước khi sang bài sau.

Đào sâu

Phần này dành cho bạn tò mò. Bỏ qua cũng không thiếu gì ở trên.

Bên trong ngăn xếp. Ngăn xếp không chỉ là hình vẽ, bạn có thể hỏi thẳng máy ảo. Chương trình CallStack gọi outer, middle, inner rồi dùng StackWalker (có từ Java 9) để lấy tên các khung từ trên xuống:

CallStack.java

java
import java.util.List;

public class CallStack {
    static List<String> inner() {
        return StackWalker.getInstance().walk(frames -> frames.map(StackWalker.StackFrame::getMethodName).limit(4).toList()); // lấy tên bốn khung trên cùng của ngăn xếp
    }

    static List<String> middle() {
        return inner();
    }

    static List<String> outer() {
        return middle();
    }

    public static void main(String[] args) {
        System.out.println("frames inside inner(), top first = " + outer());
    }
}

Kết quả [Đã chạy, JDK 17.0.15]:

text
frames inside inner(), top first = [inner, middle, outer, main]
Dòng codeMáy làm gìVì sao viết như vậy
StackWalker.getInstance().walk(frames -> ...)Đi qua các khung đang có trên ngăn xếp, đưa cho đoạn xử lý bên phảiwalk cho bạn một dòng khung để lọc và gom lại
frames.map(StackWalker.StackFrame::getMethodName)Lấy tên phương thức của từng khungBạn chỉ cần tên để so với hình vẽ
.limit(4).toList()Giữ bốn khung đầu và gom thành danh sáchBốn khung là inner, middle, outer, main; cú pháp lambda và dấu hai chấm kép sẽ được giải thích ở chương Java 8

So với lựa chọn khác. Một phương thức chỉ trả về đúng một giá trị. Muốn trả hai giá trị, như giá nhỏ nhất và lớn nhất của một dãy số, bạn gói chúng vào một record: kết quả chạy thật là Range[min=1, max=9] [Đã chạy, JDK 17.0.15]. record sẽ được học kỹ ở chương OOP.

Hiểu lầm nâng cao. Có hai điều nhiều người nghĩ sai. Một là đối số được tính khi cần dùng trong thân phương thức. Thật ra Java tính hết các đối số từ trái sang phải trước khi vào thân: với sum3(mark("first", 1), mark("second", 2), mark("third", 3)) thứ tự ghi nhận là [first, second, third, sum3] [Đã chạy]. Hai là phép chia số thực cho 0 sẽ báo lỗi: thật ra (double) 0 / 0 cho NaN và chương trình vẫn chạy, nên trung bình của một mảng rỗng là NaN [Đã chạy]. Lỗi chỉ lộ ra khi bạn dùng kết quả đó.

Giới hạn của bài. Bài này chưa nói về việc tham số nhận bản sao là chính xác điều gì với đối tượng và varargs (bài kế tiếp), nạp chồng, đệ quy, static so với phương thức của đối tượng. Số liệu thuộc JDK 17.0.15.

Đọc thêm (tài liệu chính thống, Java SE 17):

Bài tiếp theo

Bài sau trả lời câu hỏi mà phần Điều cần nhớ để lại: khi bạn truyền một biến vào phương thức, phương thức nhận được chính xác điều gì, và vì sao gán lại tham số không đổi biến bên ngoài.

Mini-dự án nối tiếp: ở bài sau, máy tính hoá đơn sẽ có thêm một đối tượng Order, và bạn sẽ thấy phương thức sửa được nội dung của nó nhưng không thay được nó. Hãy giữ lại class Receipt bạn vừa viết.

Lời giải và đáp án

Hãy thử xong rồi mới đọc phần này.

Lời giải Làm cùng mình. Thay các dòng // TODO bằng những dòng dưới đây, ghép lại chạy ra đúng kết quả đã nêu [Đã chạy, JDK 17.0.15]:

java
return Math.round(net * 1.1); // thân của withVat

if (qty <= 0) { // thân của lineTotal
    return 0; // trả về sớm cho số lượng không hợp lệ
}
return price * qty;

long gross = withVat(lineTotal(price, qty)); // thân của printLine, lời gọi trong cùng chạy trước
System.out.println(name + " x" + qty + ": " + gross);
return gross; // trả về để main cộng tổng
Dòng codeMáy làm gìLý do chọn cách này
return Math.round(net * 1.1);Nhân, làm tròn rồi trả về longQuy tắc thuế chỉ nằm ở dòng này, nên bước 5 chỉ cần đổi 1.1 thành 1.08
if (qty <= 0) { return 0; }Thoát sớm khi số lượng không hợp lệCâu lệnh bảo vệ, phần còn lại không phải thụt vào
withVat(lineTotal(price, qty))Tính lineTotal trước, đưa kết quả cho withVatLời gọi trong cùng được tính trước, kết quả thành đối số của lời gọi ngoài
return gross;Trả về giá cuối sau khi đã inmain cần con số này để cộng tổng

Ở bước 4, tổng được tính bằng printLine("Latte", 45000, 2) + printLine("Tea", 30000, 0) + printLine("Cake", 38000, 1), ra total: 140800. Ở bước 5, đổi 1.1 thành 1.08 cho 97200, 0, 41040 và tổng 138240.

Lời giải Bài 1. [Suy luận, khớp với kết quả chạy thật b = 7, c = 8]

DòngLời gọi theo thứ tựGiá trị trả vềBiến nhận
int b = addOne(twice(a));twice(3) rồi addOne(6)6 rồi 7b bằng 7
int c = twice(addOne(a));addOne(3) rồi twice(4)4 rồi 8c bằng 8

Hai số khác nhau vì thứ tự áp dụng khác nhau: lời gọi trong ngoặc luôn được tính trước.

Lời giải Bài 2. [Đã chạy, cho 90000 và 0]

java
if (qty <= 0) {
    return 0; // thoát sớm, dòng nhân bên dưới không chạy
}
return price * qty;

Khi qty bằng 0, câu return 0 chạy và phương thức kết thúc ngay nên dòng nhân không bao giờ được chạy.

Lời giải Bài 3. [Đã chạy]

Đoạn một thiếu return khi x bằng 0: thêm return 0; ở cuối thì sign(7), sign(-3), sign(0) cho 1, -1, 0. Đoạn hai bỏ kết quả của withVat: đổi thành net = withVat(net); thì in ra 110.

Lời giải Bài 4. [Đã chạy, bốn dòng khớp với kết quả mong đợi]

Ba phương thức là những đoạn bạn đã viết ở phần Làm cùng mình. Phần main đáng chú ý:

java
long total = printLine("Latte", 45000, 2) + printLine("Tea", 30000, 0) + printLine("Cake", 38000, 1); // gọi từ trái sang phải rồi cộng
System.out.println("total: " + total);
Dòng codeMáy làm gìLý do chọn cách này
printLine(...) + printLine(...) + printLine(...)Gọi ba lần từ trái sang phải, mỗi lần in một dòng rồi trả về giá cuối, sau đó cộng ba sốVì printLine trả về gross, main không cần tự tính lại
System.out.println("total: " + total);In tổngGhép chuỗi với số

Lời giải Bài 5. [Đã chạy, cho 50000, 49000 và 0]

java
static long roundToThousand(long amount) {
    return Math.round(amount / 1000.0) * 1000; // chia cho số thực để không bị cắt phần thập phân
}

Trong printLine, đổi dòng tính gross thành long gross = roundToThousand(withVat(lineTotal(price, qty)));. Chia cho 1000.0 (số thực) để không bị cắt phần thập phân, làm tròn, rồi nhân lại 1000. Vì quy tắc nằm ở một phương thức, chỉ một dòng của printLine đổi.

Đáp án Tự kiểm tra.

  1. Bổ nghĩa static, kiểu trả về long, tên withVat, tham số long net. net là tham số (biến trong khai báo); trong lời gọi withVat(5) thì số 5 mới là đối số. Kiểm tra mục tiêu 1.
  2. In ra 4 rồi 3 [Đã chạy]. half(9) là 4 vì chia số nguyên cắt phần thập phân, a bằng 4 và show(a) in 4; half(4) + 1 bằng 3. Kiểm tra mục tiêu 2.
  3. Khi c chạy: main, a, b, c (c ở đỉnh). Sau khi c trả về: main, a, b (b ở đỉnh). Sau khi b trả về: main, a (a ở đỉnh) [Suy luận]. Kiểm tra mục tiêu 3.
  4. Đường đi bị thiếu là khi x bằng 0, vì cả hai nhánh if đều sai. Thêm return 0; ở cuối thì sign(0) cho 0 [Đã chạy]. Kiểm tra mục tiêu 4.
  5. Máy nhận một bản sao nguyên liệu và đưa thành phẩm ra khe ra. Nếu bạn không đặt gì hứng ở khe ra (không gán vào biến), thành phẩm rơi mất, còn nguyên liệu gốc net của bạn thì vẫn nằm yên. Muốn dùng kết quả thì viết net = withVat(net);. Kiểm tra mục tiêu 2.