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
voidvớ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ùngvoidnhư 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ì?
long net = 5;
withVat(net);
System.out.println(net);
- A. 6, vì
withVatđã đổinetthà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
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]:
Latte: 49500
Tea: 6
net is still 5
Đọc code từng dòng:
| Dòng code | Má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ài | static để 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 gross | Lư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ệnh | Dù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ỏ đi | Cố ý 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ước | Ngăn xếp từ đáy lên đỉnh | Giá trị đáng chú ý | Điều xảy ra |
|---|---|---|---|
| 1 | main | chưa có net | Chương trình bắt đầu |
| 2 | main, printLine | item là Latte | main gọi printLine |
| 3 | main, printLine, withVat | net của withVat là 45000 | printLine gọi withVat |
| 4 | main, printLine | gross là 49500 | withVat trả về 49500 và khung bị gỡ |
| 5 | main | in Latte: 49500 | printLine kết thúc |
| 6 đến 7 | main, printLine, withVat rồi main | net là 5 rồi in Tea: 6 | Lần gọi thứ hai trả về 6 |
| 8 đến 9 | main, withVat rồi main | net của main vẫn 5 | 6 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.
nnhận 0, vì phương thức không trả gì. - B.
nnhậnnull, vì không có giá trị trả về. - C. Chương trình chạy bình thường nhưng
nchưa được gán. - D. Lỗi biên dịch, vì
voidkhông có giá trị để gán choint.
Đá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ế.
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:
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.
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ỗi | Thông báo lỗi | Nguyên nhân bằng lời thường | Cách sửa |
|---|---|---|---|
withVat(net); rồi in net | Không báo lỗi, net vẫn là 5 | Kế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 statement | Khi x không dương, phương thức chạy hết mà không return | Thêm return cho mọi đường đi |
int n = log("x"); với log là void | incompatible types: void cannot be converted to int | void 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ố int | method add in class A cannot be applied to given types; | Thiếu đối số so với khai báo | Truyề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:
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.
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:
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ì?
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.
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;
voidthì 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ùngvoidnhư 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àm | Cú pháp | Ghi 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ớm | if (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úc | return ở cuối | Nếu không sẽ có missing return statement |
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
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]:
frames inside inner(), top first = [inner, middle, outer, main]
| Dòng code | Má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ải | walk 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 khung | Bạ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ách | Bố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):
- The Java Tutorials: Defining Methods
- The Java Tutorials: Returning a Value from a Method
- Javadoc: java.lang.StackWalker
- Java Language Specification, mục 8.4: Method Declarations
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]:
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 code | Má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ề long | Quy 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 withVat | Lờ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 đã in | main 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òng | Lờ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 7 | b bằng 7 |
int c = twice(addOne(a)); | addOne(3) rồi twice(4) | 4 rồi 8 | c 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]
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ú ý:
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 code | Má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ổng | Ghép chuỗi với số |
Lời giải Bài 5. [Đã chạy, cho 50000, 49000 và 0]
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.
- Bổ nghĩa
static, kiểu trả vềlong, tênwithVat, tham sốlong net.netlà tham số (biến trong khai báo); trong lời gọiwithVat(5)thì số 5 mới là đối số. Kiểm tra mục tiêu 1. - In ra
4rồi3[Đã chạy].half(9)là 4 vì chia số nguyên cắt phần thập phân,abằng 4 vàshow(a)in 4;half(4) + 1bằng 3. Kiểm tra mục tiêu 2. - Khi
cchạy: main, a, b, c (c ở đỉnh). Sau khictrả về: main, a, b (b ở đỉnh). Sau khibtrả về: main, a (a ở đỉnh) [Suy luận]. Kiểm tra mục tiêu 3. - Đường đi bị thiếu là khi
xbằng 0, vì cả hai nhánhifđều sai. Thêmreturn 0;ở cuối thìsign(0)cho 0 [Đã chạy]. Kiểm tra mục tiêu 4. - 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
netcủa bạn thì vẫn nằm yên. Muốn dùng kết quả thì viếtnet = withVat(net);. Kiểm tra mục tiêu 2.