Bỏ qua điều hướng
EN
Tất cả bài viết

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

Ôn tập bài nàyThẻ ghi nhớ & câu hỏi ôn tập6 thẻ ghi nhớ · 6 câu trắc nghiệm
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

PhuongThucDauTien.java
// 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
returnTrả 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 main phải là static: static double dienTichHinhTron(double r) {...}. Bài 6 giải thích static là gì.

return sớm cho code dễ đọc

NguyenTo.java
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ố:

TruyenThamTri.java
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:

PhamVi.java
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):

NapChong.java
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ỳ ý

Varargs.java
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:

n!=n×(n−1)!,0!=1n! = n \times (n-1)!, \qquad 0! = 1
DeQuy.java
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:

Fibonacci.java
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ênTránh
Một phương thức làm một việcPhương thức 200 dòng làm năm việc
Tên động từ rõ nghĩa: tinhPhiVanChuyenTê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 returnSử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ìnhTí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

  1. 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ồi int bcnn(int a, int b) dùng lại ucln.
  2. Viết String xepLoai(double diem) và boolean hopLe(double diem); main chỉ nhập, gọi hai phương thức và in kết quả.
  3. Viết int[] daoNguoc(int[] a) trả về mảng mới đảo ngược, không sửa mảng gốc.
  4. Viết đệ quy boolean laDoiXung(String s) kiểm tra chuỗi đối xứng.
  5. 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; return trả 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.

Đang tải câu hỏi luyện tập…

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.

Đang tải đánh giá…
Chọn số sao từ 1 đến 5

Bình luận

Đăng nhập để bình luận không cần nhập tên và được duyệt nhanh hơn.

0/2000
Đang tải bình luận…

Đọc tiếp

Bài số № 55

Java cơ bản #12: dự án cuối — ứng dụng quản lý chi tiêu với Maven và JUnit

Gom toàn bộ kiến thức vào một dự án thật: cấu trúc Maven và pom.xml, chia lớp model–kho dữ liệu–service–giao diện, lưu file CSV, thống kê bằng Stream, cảnh báo ngân sách, viết kiểm thử JUnit 5, đóng gói file jar chạy được — và lộ trình học tiếp sau Java cơ bản.

19 phút đọc

Bài số № 54

Java cơ bản #11: lambda, tham chiếu phương thức, Stream API và Optional

Lập trình kiểu hàm trong Java: lambda và functional interface (Predicate, Function, Consumer, Supplier), tham chiếu phương thức ::, Stream với filter–map–reduce, sorted, collect, groupingBy, Optional thay cho null — cùng lời khuyên khi nào vòng lặp vẫn tốt hơn stream.

14 phút đọc

Bài số № 53

Java cơ bản #10: xử lý ngoại lệ và đọc ghi file với java.nio.file

Khi chương trình gặp lỗi: đọc stack trace, try/catch/finally, checked và unchecked exception, throw và throws, tự định nghĩa ngoại lệ nghiệp vụ, try-with-resources tự đóng tài nguyên; đọc ghi file văn bản và CSV bằng Files, Path, UTF-8 — kèm các thói quen xử lý lỗi nên tránh.

15 phút đọc