Java cơ bản #5: phương thức — tham số, giá trị trả về, nạp chồng, đệ quy
Chia chương trình thành các phương thức nhỏ, mỗi phương thức một việc: khai báo, tham số và return, vì sao Java luôn truyền tham trị (kể cả với mảng), phạm vi biến, nạp chồng (overload), varargs, đệ quy và khi nào không nên dùng nó.
11 phút đọc
Mục lục
Khi chương trình dài vài trăm dòng trong một main, sửa một chỗ có thể làm hỏng chỗ khác, và cùng một đoạn tính toán bị chép đi chép lại. Phương thức (method, ở ngôn ngữ khác gọi là hàm) giải quyết bằng cách đặt tên cho một khối code và gọi nó khi cần.
Viết phương thức tốt là kỹ năng nền của mọi lập trình viên: mỗi phương thức làm một việc, có tên nói rõ việc đó, đủ ngắn để đọc trong một màn hình.
Khai báo và gọi phương thức
// kiểu-trả-về tên(danh sách tham số) { thân }
double dienTichHinhTron(double banKinh) {
return Math.PI * banKinh * banKinh;
}
void inDongKe(int doDai) { // void: không trả về gì
IO.println("-".repeat(doDai));
}
boolean laSoChan(int n) {
return n % 2 == 0;
}
void main() {
inDongKe(20);
IO.println("Diện tích: " + String.format("%.2f", dienTichHinhTron(3)));
IO.println("10 chẵn? " + laSoChan(10));
inDongKe(20);
}| Thành phần | Ý nghĩa |
|---|---|
| Kiểu trả về | double, boolean, String… hoặc void nếu không trả gì |
| Tên | Động từ, camelCase: tinhTong, laSoNguyenTo, inHoaDon |
| Tham số | Dữ liệu đầu vào, mỗi tham số có kiểu và tên |
return | Trả kết quả và kết thúc phương thức ngay lập tức |
Trong compact source file (Java 25), các phương thức viết thẳng cạnh
main. Trong lớp truyền thống, phương thức gọi từpublic static void mainphải làstatic:static double dienTichHinhTron(double r) {...}. Bài 6 giải thíchstaticlà gì.
return sớm cho code dễ đọc
boolean laSoNguyenTo(int n) {
if (n < 2) return false; // loại trường hợp đặc biệt trước
for (int i = 2; (long) i * i <= n; i++) {
if (n % i == 0) return false; // tìm thấy ước → kết luận ngay
}
return true;
}
void main() {
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 50; i++) {
if (laSoNguyenTo(i)) sb.append(i).append(' ');
}
IO.println(sb); // 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
}Nhờ tách laSoNguyenTo, main đọc như một câu tiếng Việt: "với mỗi số từ 1 tới 50, nếu là số nguyên tố thì in ra". Đó là mục tiêu.
Java luôn truyền tham trị
Khi gọi phương thức, Java sao chép giá trị của đối số vào tham số. Với kiểu nguyên thuỷ, đó là sao chép con số:
import java.util.Arrays;
void tangMot(int x) {
x++; // chỉ đổi bản sao
}
void tangPhanTuDau(int[] mang) {
mang[0]++; // bản sao của ĐỊA CHỈ → vẫn trỏ cùng mảng → mảng gốc đổi
}
void ganMangMoi(int[] mang) {
mang = new int[]{0, 0, 0}; // chỉ đổi bản sao địa chỉ; biến bên ngoài không bị ảnh hưởng
}
void main() {
int so = 5;
tangMot(so);
IO.println(so); // 5
int[] a = {1, 2, 3};
tangPhanTuDau(a);
IO.println(Arrays.toString(a)); // [2, 2, 3]
ganMangMoi(a);
IO.println(Arrays.toString(a)); // [2, 2, 3]
}Với kiểu tham chiếu, giá trị được sao chép là địa chỉ. Vì vậy phương thức sửa được nội dung đối tượng (phần tử mảng), nhưng không làm biến bên ngoài trỏ sang đối tượng khác. Nhớ câu: "Java truyền tham trị; với đối tượng, giá trị đó là tham chiếu."
Muốn phương thức "trả về" kết quả, hãy dùng return thay vì sửa tham số — code dễ hiểu và dễ kiểm thử hơn.
Phạm vi biến (scope)
Biến chỉ tồn tại trong khối { } nơi nó được khai báo:
int nhanDoi(int n) {
int ketQua = n * 2; // ketQua chỉ sống trong nhanDoi
return ketQua;
}
void main() {
int tong = 0;
for (int i = 1; i <= 3; i++) {
int binhPhuong = i * i; // sống trong một lần lặp
tong += binhPhuong;
}
// IO.println(i); → lỗi: cannot find symbol (i hết phạm vi)
// IO.println(ketQua); → lỗi: ketQua thuộc phương thức khác
IO.println(tong + " " + nhanDoi(tong));
}Khai báo biến gần chỗ dùng nhất và với phạm vi hẹp nhất có thể — ít chỗ có thể đọc/sửa nhầm.
Nạp chồng (overload)
Nhiều phương thức có thể cùng tên nếu danh sách tham số khác nhau (số lượng hoặc kiểu):
int tong(int a, int b) { return a + b; }
int tong(int a, int b, int c) { return a + b + c; }
double tong(double a, double b) { return a + b; }
String chao(String ten) { return chao(ten, "bạn"); } // gọi lại bản đầy đủ, tránh lặp code
String chao(String ten, String danhXung) { return "Chào " + danhXung + " " + ten; }
void main() {
IO.println(tong(1, 2)); // 3 → bản (int, int)
IO.println(tong(1, 2, 3)); // 6 → bản 3 tham số
IO.println(tong(1.5, 2)); // 3.5 → bản (double, double), 2 được nâng lên double
IO.println(chao("Minh"));
IO.println(chao("Hoa", "chị"));
}Chỉ khác kiểu trả về thì không nạp chồng được — trình biên dịch không biết chọn bản nào. System.out.println chính là một phương thức nạp chồng với hàng chục phiên bản cho int, double, String, Object…
Varargs: số lượng tham số tuỳ ý
double trungBinh(double... cacSo) { // cacSo thực chất là double[]
if (cacSo.length == 0) return 0;
double tong = 0;
for (double x : cacSo) tong += x;
return tong / cacSo.length;
}
void main() {
IO.println(trungBinh(8, 9));
IO.println(trungBinh(7, 8, 9, 10));
IO.println(trungBinh());
IO.println(String.format("%s có %d tuổi", "An", 20)); // String.format cũng dùng varargs
}Varargs phải là tham số cuối cùng, và mỗi phương thức chỉ có một.
Đệ quy
Phương thức gọi lại chính nó với bài toán nhỏ hơn, tới khi gặp trường hợp cơ sở giải được trực tiếp:
long giaiThua(int n) {
if (n <= 1) return 1; // trường hợp cơ sở — THIẾU dòng này là đệ quy vô hạn
return n * giaiThua(n - 1); // bước đệ quy: bài toán nhỏ hơn
}
int tongChuSo(int n) {
if (n < 10) return n;
return n % 10 + tongChuSo(n / 10);
}
// Tìm kiếm nhị phân đệ quy trên mảng đã sắp xếp
int timNhiPhan(int[] a, int x, int trai, int phai) {
if (trai > phai) return -1;
int giua = (trai + phai) >>> 1; // tránh tràn số khi trai + phai rất lớn
if (a[giua] == x) return giua;
return a[giua] < x ? timNhiPhan(a, x, giua + 1, phai) : timNhiPhan(a, x, trai, giua - 1);
}
void main() {
IO.println(giaiThua(5)); // 120
IO.println(giaiThua(20)); // 2432902008176640000 — giới hạn của long
IO.println(tongChuSo(2026)); // 10
int[] a = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91};
IO.println(timNhiPhan(a, 23, 0, a.length - 1)); // 5
}Mỗi lần gọi phương thức chiếm một "khung" trên ngăn xếp gọi (call stack). Đệ quy quá sâu (thường vài nghìn tới vài chục nghìn tầng) → StackOverflowError.
Khi nào không nên đệ quy
Đệ quy Fibonacci ngây thơ tính lại cùng một giá trị rất nhiều lần — fib(40) gọi hơn 300 triệu lần:
long fibDeQuy(int n) {
return n < 2 ? n : fibDeQuy(n - 1) + fibDeQuy(n - 2); // O(2^n)
}
long fibVongLap(int n) {
long a = 0, b = 1;
for (int i = 0; i < n; i++) {
long tiep = a + b;
a = b;
b = tiep;
}
return a; // O(n)
}
void main() {
long t = System.nanoTime();
IO.println(fibDeQuy(35) + " (đệ quy: " + (System.nanoTime() - t) / 1_000_000 + " ms)");
t = System.nanoTime();
IO.println(fibVongLap(35) + " (vòng lặp: " + (System.nanoTime() - t) / 1_000 + " µs)");
IO.println(fibVongLap(90));
}Đệ quy hợp với bài toán có cấu trúc đệ quy tự nhiên: duyệt cây thư mục, cây nhị phân, chia để trị (merge sort, quick sort), sinh hoán vị. Với phép lặp đơn giản, vòng lặp thường nhanh và an toàn hơn.
Viết phương thức tốt
| Nên | Tránh |
|---|---|
| Một phương thức làm một việc | Phương thức 200 dòng làm năm việc |
Tên động từ rõ nghĩa: tinhPhiVanChuyen | Tên mơ hồ: xuLy, doIt, func1 |
| Ít tham số (≤ 3–4) | 8 tham số cùng kiểu dễ truyền nhầm thứ tự |
Trả kết quả bằng return | Sửa tham số để "trả về" kết quả |
| Tách phần tính toán khỏi phần in ra màn hình | Tính toán lẫn với println — không tái sử dụng, không kiểm thử được |
Điểm cuối rất quan trọng: double tinhBmi(double cao, double nang) dùng lại được ở ứng dụng web, ứng dụng di động, bài kiểm thử; còn phương thức vừa tính vừa println thì chỉ dùng được trong console.
Bài tập
- Viết
int ucln(int a, int b)theo thuật toán Euclid (bản vòng lặp và bản đệ quy), rồiint bcnn(int a, int b)dùng lạiucln. - Viết
String xepLoai(double diem)vàboolean hopLe(double diem);mainchỉ nhập, gọi hai phương thức và in kết quả. - Viết
int[] daoNguoc(int[] a)trả về mảng mới đảo ngược, không sửa mảng gốc. - Viết đệ quy
boolean laDoiXung(String s)kiểm tra chuỗi đối xứng. - Viết
String docSo(int n)đọc số có 1–3 chữ số thành chữ tiếng Việt (105→"một trăm lẻ năm").
Tổng kết
- Phương thức = tên + tham số + kiểu trả về + thân;
returntrả kết quả và kết thúc ngay. - Java luôn truyền tham trị; với đối tượng/mảng, giá trị được sao chép là tham chiếu → sửa được nội dung, không đổi được biến bên ngoài.
- Biến chỉ sống trong khối khai báo nó; giữ phạm vi hẹp nhất có thể.
- Nạp chồng: cùng tên, khác danh sách tham số; varargs cho số lượng tham số tuỳ ý.
- Đệ quy cần trường hợp cơ sở; tránh đệ quy tính lặp (Fibonacci ngây thơ) và đệ quy quá sâu.
- Bài tiếp theo: bước vào lập trình hướng đối tượng — lớp và đối tượng.
Nguồn tham khảo: dev.java — "Defining Methods", "Passing Information to a Method"; The Java Language Specification (Java SE 25), mục 8.4 và 15.12; Java Programming MOOC — University of Helsinki, phần 2; Bloch — Effective Java, mục 49–53.
Bài này thế nào?
Bài viết có hữu ích với bạn?
Đánh giá giúp mình biết nên viết thêm về chủ đề nào.