Buổi 4: Phương thức và xử lý ngoại lệ
Hãy hình dung bạn đang viết phần mềm cho một cửa hàng sách. Việc tính tiền, kiểm tra mã khuyến mãi, tính điểm tích lũy, in hóa đơn… đều lặp lại ở nhiều chỗ. Nếu mỗi nơi lại chép một đoạn code riêng, chương trình sẽ nhanh chóng thành một mớ hỗn độn: sửa một công thức phải sửa ở mười nơi, quên một nơi là sai tiền khách hàng. Phương thức (method) — hay còn gọi là hàm — giải quyết bài toán này: đặt tên cho một đoạn việc, viết một lần, gọi ở bất kỳ đâu.
Nhưng chương trình thật còn phải đối mặt với một sự thật khó chịu: mọi thứ đều có thể sai. Khách nhập chữ thay vì số, tệp không tồn tại, phép chia cho 0, mạng đứt giữa chừng. Java có cơ chế ngoại lệ (exception) để các lỗi này được báo, truyền đi và xử lý có tổ chức thay vì làm chương trình chết bất ngờ. Buổi này bạn học cả hai mảng kiến thức nền tảng đó: viết phương thức cho tốt, và viết chương trình biết "vấp mà không ngã".
Mục tiêu buổi học
- Sau buổi này bạn có thể viết và gọi phương thức tĩnh có tham số, giá trị trả về, biến cục bộ; giải thích được vì sao Java luôn truyền tham số theo giá trị.
- Bạn có thể chồng hàm (overloading) và dùng tham số biến đổi (varargs) đúng chỗ.
- Bạn có thể phân tích một bài toán thành dạng đệ quy (điều kiện dừng + bước đệ quy), theo dõi được ngăn xếp lời gọi và biết khi nào cần ghi nhớ (memoization).
- Bạn có thể kiểm thử phương thức bằng cách tự viết các phép kiểm tra (assert thủ công) và hiểu ý tưởng của JUnit.
- Bạn có thể dùng
try / catch / finally,throw,throws; phân biệt ngoại lệ checked và unchecked; tự định nghĩa ngoại lệ riêng; dùngtry-with-resources. - Bạn có thể viết chú thích JavaDoc và sinh tài liệu từ mã nguồn.
Bản đồ buổi học
| Phần | Nội dung | Thời lượng gợi ý |
|---|---|---|
| 1 | Phương thức: khái niệm, hàm tĩnh, tham số, biến, giá trị trả về | 25 phút |
| 2 | Chồng hàm và tham số biến đổi (varargs) + thực hành 4.1 | 25 phút |
| 3 | Đệ quy và ngăn xếp lời gọi + thực hành 4.2 | 40 phút |
| 4 | Kiểm thử phương thức | 10 phút |
| 5 | Ngoại lệ: try / catch / finally + thực hành 4.3 | 35 phút |
| 6 | throw, throws, ngoại lệ tự định nghĩa, try-with-resources + thực hành 4.4 | 35 phút |
| 7 | JavaDoc và tổng kết | 10 phút |
Tổng cộng 180 phút, trong đó thực hành khoảng 55%.
1. Phương thức trong Java
1.1. Phương thức là gì và vì sao cần
Phương thức là một khối lệnh được đặt tên, có thể nhận dữ liệu đầu vào (tham số), thực hiện công việc, và có thể trả về một kết quả. Nó mang lại ba lợi ích lớn:
- Tái sử dụng: viết một lần, gọi nhiều lần.
- Chia nhỏ bài toán: chương trình lớn trở thành nhiều bước nhỏ có tên dễ đọc (
tinhTongTien,kiemTraMaGiamGia). - Che giấu chi tiết: người gọi chỉ cần biết phương thức làm gì, không cần biết làm thế nào.
Cú pháp tổng quát của một phương thức:
modifier kieuTraVe tenPhuongThuc(kieu1 thamSo1, kieu2 thamSo2) {
// thân phương thức
return giaTri; // nếu kiểu trả về khác void
}Phần đầu (từ modifier đến dấu )) gọi là chữ ký (signature) cùng kiểu trả về; phần trong cặp ngoặc nhọn là thân (body). Tên phương thức theo quy ước camelCase, bắt đầu bằng động từ: tinhDiemTrungBinh, isPrime, findMax.
1.2. Phương thức tĩnh (static)
Trong Java mọi phương thức đều nằm trong một lớp. Ở buổi này ta chỉ dùng phương thức tĩnh (static): thuộc về chính lớp, gọi trực tiếp từ main mà chưa cần tạo đối tượng (đối tượng sẽ học ở Buổi 5). Chính main cũng là một phương thức tĩnh.
Ví dụ đầu tiên: một phương thức có kết quả trả về và một phương thức chỉ "làm việc" (kiểu void).
public class Main {
static int square(int x) {
return x * x;
}
static void greet(String name) {
System.out.println("Xin chào, " + name + "!");
}
public static void main(String[] args) {
greet("Lan");
int s = square(7);
System.out.println("7 bình phương = " + s);
System.out.println(square(square(2)));
}
}Bạn thấy square(square(2)): lời gọi bên trong chạy trước, cho 4, rồi square(4) cho 16. Một lời gọi phương thức có kiểu trả về khác void là một biểu thức, nên đặt được ở mọi nơi biểu thức hợp lệ.
Lưu ý: nếu gọi phương thức tĩnh từ chính lớp chứa nó, chỉ cần viết tên (
square(7)). Gọi từ lớp khác thì viếtTenLop.tenPhuongThuc(...), ví dụMath.max(3, 9)hayInteger.parseInt("42")— đều là phương thức tĩnh của thư viện chuẩn.
1.3. Tham số và đối số
Tham số (parameter) là biến khai báo trong ngoặc đơn của phương thức; đối số (argument) là giá trị thật bạn truyền khi gọi. Đối số được gán lần lượt cho tham số theo thứ tự, kiểu phải tương thích (có thể mở rộng tự động, ví dụ truyền int vào tham số double).
1.4. Truyền theo giá trị — quy tắc quan trọng nhất
Java luôn truyền tham số theo giá trị (pass-by-value). Khi gọi, giá trị của đối số được sao chép vào tham số. Hệ quả:
- Với kiểu nguyên thủy: phương thức sửa tham số thì biến bên ngoài không đổi.
- Với kiểu tham chiếu (mảng, đối tượng): thứ được sao chép là địa chỉ (tham chiếu). Phương thức có thể sửa nội dung của đối tượng mà tham chiếu trỏ tới, nhưng gán tham số sang đối tượng khác thì chỉ đổi bản sao địa chỉ, không ảnh hưởng biến bên ngoài.
import java.util.Arrays;
public class Main {
static void tang(int x) {
x = x + 100;
System.out.println("trong hàm: " + x);
}
static void doiPhanTuDau(int[] a) { a[0] = 99; }
static void ganLai(int[] a) { a = new int[]{7, 7, 7}; }
public static void main(String[] args) {
int n = 5;
tang(n);
System.out.println("n sau khi gọi: " + n);
int[] arr = {1, 2, 3};
doiPhanTuDau(arr);
System.out.println(Arrays.toString(arr));
ganLai(arr);
System.out.println(Arrays.toString(arr));
}
}Kết quả: n vẫn là 5; mảng bị đổi phần tử đầu thành 99 (cùng một đối tượng trên heap); còn ganLai không làm mảng bên ngoài thành 7 7 7 vì nó chỉ đổi bản sao tham chiếu. Hình dưới minh họa vị trí của các biến: mỗi lần gọi phương thức tạo một khung (frame) trên Stack, còn mảng nằm trên Heap.
void main() {
int age = 20;
Student s = new Student("An");
greet(s); // p trong greet() cũng trỏ tới đối tượng đó
}Biến nguyên thủy lưu giá trị ngay trong ô ở stack; biến tham chiếu chỉ lưu địa chỉ, còn đối tượng thật nằm ở heap. Hai biến s và p cùng trỏ một đối tượng — không có bản sao.
Hãy đọc hình theo hai vùng: biến cục bộ và tham số (kể cả bản sao tham chiếu) nằm trên Stack; đối tượng và mảng nằm trên Heap. Hai tham chiếu trỏ cùng một mảng thì thấy cùng nội dung.
Cảnh báo: câu "Java truyền đối tượng theo tham chiếu" là một hiểu lầm phổ biến. Java truyền giá trị của tham chiếu. Bạn không thể viết phương thức
swap(a, b)hoán đổi hai biếnintcủa người gọi.
1.5. Biến cục bộ và biến thành viên
- Biến cục bộ (local variable): khai báo trong thân phương thức (kể cả tham số). Chỉ tồn tại trong khối chứa nó, sinh ra khi vào phương thức và mất khi thoát. Phải được gán giá trị trước khi dùng; mỗi lần gọi có bản riêng.
- Biến thành viên (member variable): khai báo ở cấp lớp, ngoài mọi phương thức. Trong ngữ cảnh phương thức tĩnh, ta dùng biến thành viên tĩnh (
static): một bản duy nhất, tồn tại suốt chương trình, mọi phương thức trong lớp dùng chung. (Biến thành viên không tĩnh gắn với từng đối tượng — Buổi 5.)
public class Main {
static int counter = 0; // biến thành viên tĩnh: dùng chung
static int next() {
int tmp = counter + 1; // biến cục bộ: mỗi lần gọi một bản mới
counter = tmp;
return tmp;
}
public static void main(String[] args) {
System.out.println(next());
System.out.println(next());
System.out.println(next());
System.out.println("counter = " + counter);
int counter = 50; // che (shadow) biến thành viên trong main
System.out.println("counter trong main = " + counter);
System.out.println("Main.counter = " + Main.counter);
}
}Biến cục bộ cùng tên với biến thành viên sẽ che (shadow) nó trong phạm vi hẹp hơn; muốn dùng biến thành viên thì ghi rõ Main.counter. Cách tốt hơn là đừng đặt trùng tên.
Mẹo: hạn chế biến tĩnh thay đổi được ("trạng thái toàn cục"). Phương thức chỉ dựa vào tham số và trả về kết quả (không đụng biến ngoài) dễ hiểu, dễ kiểm thử và dễ chạy song song hơn nhiều.
1.6. Giá trị trả về và return
return giaTri; kết thúc phương thức ngay lập tức và đưa giá trị về cho người gọi. Kiểu của giá trị phải phù hợp kiểu khai báo. Với void có thể viết return; để thoát sớm. Mọi đường đi trong phương thức có kiểu trả về khác void đều phải kết thúc bằng return (hoặc ném ngoại lệ), nếu không trình biên dịch báo lỗi missing return statement.
Thoát sớm (early return) giúp code phẳng, ít else lồng nhau:
public class Main {
static boolean isLeap(int year) {
if (year % 400 == 0) return true;
if (year % 100 == 0) return false;
return year % 4 == 0;
}
static void printGrade(double score) {
if (score < 0 || score > 10) {
System.out.println("Điểm không hợp lệ");
return; // thoát sớm khỏi hàm void
}
System.out.println(score >= 8.5 ? "Giỏi" : score >= 5 ? "Đạt" : "Chưa đạt");
}
public static void main(String[] args) {
for (int y : new int[]{1900, 2000, 2024, 2026}) {
System.out.println(y + " nhuận? " + isLeap(y));
}
printGrade(9.0);
printGrade(4.5);
printGrade(11);
}
}Ví dụ thực tế: ở một hệ thống thư viện,
canBorrow(member, book)trả vềbooleanđể trả lời "có cho mượn không?", cònprintReceipt(member, book)làvoidvì chỉ in phiếu. Đặt tên theo kiểu trả về: hàm trả boolean thường bắt đầu bằngis,has,can.
2. Chồng hàm và tham số biến đổi
2.1. Chồng hàm (overloading)
Nhiều phương thức cùng tên nhưng khác danh sách tham số (khác số lượng hoặc khác kiểu) gọi là chồng hàm. Nhờ vậy cùng một "ý nghĩa" (tính diện tích, in ra màn hình) dùng chung một tên dễ nhớ, như Math.abs có bản cho int, long, double, hay System.out.println có hàng chục bản.
public class Main {
static double area(double side) {
return side * side; // hình vuông
}
static double area(double w, double h) {
return w * h; // hình chữ nhật
}
static int max(int a, int b) { return a > b ? a : b; }
static int max(int a, int b, int c) { return max(max(a, b), c); }
public static void main(String[] args) {
System.out.println(area(4));
System.out.println(area(3, 5));
System.out.println(max(4, 9));
System.out.println(max(4, 9, 6));
}
}Quy tắc phân biệt: trình biên dịch chọn phương thức dựa vào tên + số lượng + kiểu đối số tại thời điểm biên dịch. Kiểu trả về không tham gia phân biệt: hai phương thức chỉ khác kiểu trả về là lỗi biên dịch.
2.2. Java chọn bản chồng hàm nào?
Khi không có bản khớp chính xác, Java tìm theo thứ tự: khớp chính xác → mở rộng kiểu nguyên thủy (int → long → float → double) → đóng/mở hộp (boxing) → tham số biến đổi. Thử đoán kết quả bên dưới trước khi chạy:
public class Main {
static void show(int x) { System.out.println("int " + x); }
static void show(long x) { System.out.println("long " + x); }
static void show(double x) { System.out.println("double " + x); }
public static void main(String[] args) {
show(5);
show(5L);
show('A');
show(2.5f);
byte b = 3;
show(b);
}
}show('A') chọn int (ký tự mở rộng thành int 65), show(2.5f) chọn double (không có bản float, mở rộng lên double), show(b) với byte cũng chọn int vì mở rộng gần nhất. Bảng sau tóm tắt:
| Lời gọi | Bản được chọn | Lý do |
|---|---|---|
show(5) |
show(int) |
khớp chính xác |
show(5L) |
show(long) |
khớp chính xác |
show('A') |
show(int) |
char mở rộng thành int |
show(2.5f) |
show(double) |
float mở rộng thành double |
Cảnh báo: nếu có hai bản đều khớp "ngang nhau" (ví dụ
f(int, double)vàf(double, int)mà gọif(1, 2)), trình biên dịch báo lỗi ambiguous method call. Đừng thiết kế chồng hàm gây mơ hồ như vậy.
2.3. Tham số biến đổi (varargs)
Cú pháp kieu... ten cho phép truyền số lượng đối số tùy ý (kể cả không có). Bên trong phương thức, ten là một mảng. Tham số varargs phải đứng cuối danh sách và mỗi phương thức chỉ có một.
public class Main {
static int sum(int... nums) {
int total = 0;
for (int v : nums) total += v;
return total;
}
static String join(String sep, String... parts) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
if (i > 0) sb.append(sep);
sb.append(parts[i]);
}
return sb.toString();
}
public static void main(String[] args) {
System.out.println(sum());
System.out.println(sum(4));
System.out.println(sum(1, 2, 3, 4));
System.out.println(sum(new int[]{10, 20})); // truyền thẳng một mảng
System.out.println(join("-", "a", "b", "c"));
}
}sum() không đối số cho 0 vì mảng rỗng; ta cũng truyền trực tiếp một mảng int[] được. Ví dụ quen thuộc nhất của varargs là String.format("%d - %s", 5, "x") và Arrays.asList(1, 2, 3).
Lưu ý: varargs có ưu tiên thấp nhất khi chọn chồng hàm:
show(int)luôn được chọn trướcshow(int...)nếu truyền đúng mộtint. Đừng chồng hàm theo kiểuf(int)vàf(int...)trừ khi thật sự cần.
Chồng hàm còn dùng để cung cấp giá trị mặc định — Java không có tham số mặc định như một số ngôn ngữ khác, nên ta viết các bản "ngắn" gọi bản "đầy đủ":
public class Main {
static String join(String[] arr, String sep) {
return String.join(sep, arr);
}
static String join(String[] arr) {
return join(arr, ","); // bản ngắn gọi bản đầy đủ với sep mặc định
}
public static void main(String[] args) {
String[] names = {"An", "Bình", "Chi"};
System.out.println(join(names));
System.out.println(join(names, " | "));
}
}Bài tập đầu tiên nằm ngay ở đây: luyện phương thức tĩnh, chồng hàm và varargs.
Vừa học xong phương thức tĩnh, chồng hàm và varargs, hãy làm các bài sau để quen với việc tách logic thành hàm nhỏ, đặt tên rõ ràng và thử hàm với nhiều bộ dữ liệu.
3. Đệ quy
3.1. Ý tưởng
Đệ quy (recursion) là khi một phương thức tự gọi chính nó để giải một bài toán nhỏ hơn cùng dạng. Nó hợp với những bài toán có cấu trúc tự lặp lại: giai thừa, dãy Fibonacci, duyệt thư mục, tìm kiếm nhị phân, tháp Hà Nội…
Mọi hàm đệ quy đúng đều có hai phần:
- Điều kiện dừng (base case): trường hợp nhỏ nhất, trả kết quả ngay không cần gọi tiếp.
- Bước đệ quy (recursive case): quy bài toán về một bài toán nhỏ hơn rồi gọi lại chính hàm.
Giai thừa: 0! = 1 (điều kiện dừng) và n! = n × (n−1)! (bước đệ quy).
public class Main {
static long factorial(int n) {
if (n == 0) return 1; // điều kiện dừng
return n * factorial(n - 1); // bước đệ quy
}
public static void main(String[] args) {
for (int n = 0; n <= 5; n++) {
System.out.println(n + "! = " + factorial(n));
}
System.out.println("20! = " + factorial(20));
}
}Mẹo: khi gặp bài đệ quy, hãy tự hỏi ba câu: (1) trường hợp nhỏ nhất là gì? (2) nếu giả sử hàm đã đúng cho bài nhỏ hơn thì dùng nó thế nào để giải bài hiện tại? (3) mỗi lần gọi, tham số có tiến gần điều kiện dừng không?
3.2. Ngăn xếp lời gọi (call stack)
Mỗi lần gọi phương thức, JVM tạo một khung (stack frame) chứa tham số và biến cục bộ, đặt lên đỉnh Stack. Khi phương thức trả về, khung đó bị gỡ. Với đệ quy, các khung chồng lên nhau cho tới khi chạm điều kiện dừng, rồi được gỡ dần theo thứ tự ngược lại ("trả kết quả về").
Mô phỏng sau cho bạn chạy từng bước lời gọi đệ quy để xem khung nào được đẩy vào hay rút khỏi Stack.
Call stack: đệ quy factorial(n)
Mỗi lời gọi đẩy một khung (stack frame) lên stackGọi quá sâu (ví dụ thiếu trường hợp cơ sở) sẽ tràn stack: StackOverflowError.
Hãy bấm từng bước và để ý hai pha: pha đi xuống (các lời gọi factorial(3), factorial(2), … chất lên nhau, chưa có phép nhân nào xong) và pha đi lên (từ đáy, từng khung trả giá trị để khung bên dưới nhân tiếp).
Đoạn code sau in dấu vết bằng cách thụt lề theo độ sâu, giúp "nhìn thấy" hai pha ấy:
public class Main {
static int sumTo(int n, int depth) {
String pad = " ".repeat(depth);
System.out.println(pad + "vào sumTo(" + n + ")");
int result;
if (n == 0) {
result = 0;
} else {
result = n + sumTo(n - 1, depth + 1);
}
System.out.println(pad + "ra sumTo(" + n + ") = " + result);
return result;
}
public static void main(String[] args) {
System.out.println("Kết quả: " + sumTo(3, 0));
}
}3.3. Lỗi tràn ngăn xếp (StackOverflowError)
Nếu thiếu điều kiện dừng, hoặc tham số không tiến về điều kiện dừng, các khung nối tiếp nhau cho tới khi hết bộ nhớ Stack và Java ném StackOverflowError. Đây là lỗi (Error), không phải ngoại lệ thông thường — nhưng với mục đích minh họa ta vẫn bắt được:
public class Main {
static int broken(int n) {
return 1 + broken(n + 1); // không bao giờ dừng
}
public static void main(String[] args) {
try {
broken(0);
} catch (StackOverflowError e) {
System.out.println("Tràn ngăn xếp: đệ quy không có điều kiện dừng!");
}
}
}Cảnh báo: Java không tối ưu đệ quy đuôi, và Stack mặc định chỉ chứa khoảng vài nghìn đến vài chục nghìn khung. Với đầu vào lớn (ví dụ
sumTo(1_000_000)), hãy dùng vòng lặp thay vì đệ quy.
3.4. Đệ quy tuyến tính, nhị phân và hiện tượng lặp tính toán
Một số bài chỉ gọi đệ quy một lần mỗi bước (giai thừa, đảo chuỗi, tổng chữ số): chi phí tỉ lệ độ sâu. Một số gọi hai lần trở lên (Fibonacci), dễ tính đi tính lại các bài toán con:
public class Main {
static int calls = 0;
static long fib(int n) {
calls++;
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
public static void main(String[] args) {
System.out.println("fib(10) = " + fib(10) + ", số lần gọi: " + calls);
calls = 0;
System.out.println("fib(20) = " + fib(20) + ", số lần gọi: " + calls);
}
}Chỉ tính fib(20) đã cần hơn 21 nghìn lời gọi, và con số này tăng gần như gấp đôi mỗi khi n tăng thêm 1 (độ phức tạp mũ). Lý do: fib(18) bị tính lại hai lần, fib(17) ba lần, …
3.5. Ghi nhớ kết quả (memoization)
Cách chữa: lưu kết quả đã tính vào một bảng; lần sau gặp lại thì lấy ngay.
public class Main {
static long[] memo = new long[91];
static int calls = 0;
static long fib(int n) {
calls++;
if (n < 2) return n;
if (memo[n] != 0) return memo[n]; // đã tính rồi
return memo[n] = fib(n - 1) + fib(n - 2); // tính và ghi nhớ
}
public static void main(String[] args) {
System.out.println("fib(20) = " + fib(20) + ", số lần gọi: " + calls);
calls = 0;
System.out.println("fib(90) = " + fib(90) + ", số lần gọi: " + calls);
}
}Số lần gọi giảm từ hàng chục nghìn xuống còn tuyến tính theo n. Đây là bước đầu làm quen với quy hoạch động (dynamic programming) mà bạn sẽ gặp ở môn Cấu trúc dữ liệu và giải thuật.
3.6. Một số mẫu đệ quy kinh điển
Ước chung lớn nhất (Euclid): gcd(a, 0) = a, gcd(a, b) = gcd(b, a mod b). Lũy thừa nhanh: a^n bằng (a^(n/2))^2 (nhân thêm a nếu n lẻ), chỉ cần khoảng log n lời gọi.
public class Main {
static int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
static long power(long a, int n) {
if (n == 0) return 1;
long half = power(a, n / 2);
return n % 2 == 0 ? half * half : half * half * a;
}
public static void main(String[] args) {
System.out.println("gcd(48, 18) = " + gcd(48, 18));
System.out.println("2^30 = " + power(2, 30));
System.out.println("3^13 = " + power(3, 13));
}
}Tháp Hà Nội: muốn chuyển n đĩa từ cột A sang cột C qua cột B, ta chuyển n−1 đĩa sang B, chuyển đĩa lớn nhất sang C, rồi chuyển n−1 đĩa từ B sang C. Chỉ cần ba dòng đệ quy, nhưng số bước là 2^n − 1:
public class Main {
static int moves = 0;
static void hanoi(int n, char from, char to, char via) {
if (n == 0) return;
hanoi(n - 1, from, via, to);
moves++;
System.out.println("Chuyển đĩa " + n + ": " + from + " -> " + to);
hanoi(n - 1, via, to, from);
}
public static void main(String[] args) {
hanoi(3, 'A', 'C', 'B');
System.out.println("Tổng số bước: " + moves);
}
}Đệ quy trên chuỗi và mảng: thường ta xử lý một phần tử rồi giao phần còn lại cho lời gọi sau. Ví dụ kiểm tra chuỗi đối xứng bằng cách so ký tự đầu và cuối rồi thu hẹp:
public class Main {
static boolean isPalindrome(String s, int lo, int hi) {
if (lo >= hi) return true;
if (s.charAt(lo) != s.charAt(hi)) return false;
return isPalindrome(s, lo + 1, hi - 1);
}
static String reverse(String s) {
if (s.isEmpty()) return "";
return reverse(s.substring(1)) + s.charAt(0);
}
public static void main(String[] args) {
System.out.println(isPalindrome("radar", 0, 4));
System.out.println(isPalindrome("java", 0, 3));
System.out.println(reverse("Codelab"));
}
}3.7. Đệ quy hay vòng lặp?
| Tiêu chí | Đệ quy | Vòng lặp |
|---|---|---|
| Độ rõ ràng với bài có cấu trúc lồng/phân chia | Rất tự nhiên (cây, tháp Hà Nội, hoán vị) | Phải tự quản lý ngăn xếp |
| Bộ nhớ | Mỗi lời gọi một khung Stack | Gần như không tốn thêm |
| Độ sâu lớn | Nguy cơ StackOverflowError |
An toàn |
| Tốc độ | Chậm hơn chút do chi phí gọi hàm | Nhanh hơn |
Quy tắc thực tế: dùng đệ quy khi nó làm lời giải ngắn gọn và đúng bản chất bài toán và độ sâu vừa phải; còn lại dùng vòng lặp. Mọi đệ quy đều viết lại được bằng vòng lặp (kèm ngăn xếp tự quản lý) và ngược lại.
Ví dụ thực tế: duyệt cây thư mục của ổ đĩa là bài toán đệ quy điển hình: "đọc một thư mục = đọc các tệp trong nó, rồi đọc đệ quy từng thư mục con". Tương tự, menu nhiều cấp của một ứng dụng được dựng bằng đệ quy.
Giờ là lúc tự tay viết hàm đệ quy: giai thừa, ước chung, Fibonacci có nhớ, tháp Hà Nội và cả sinh hoán vị.
Vừa học xong đệ quy, hãy làm các bài sau để luyện thói quen "điều kiện dừng trước, bước đệ quy sau", và dùng ghi nhớ khi bài toán con lặp lại.
4. Kiểm thử phương thức
4.1. Vì sao phải kiểm thử
Viết xong một phương thức, bạn phải trả lời được câu: "Làm sao biết nó đúng?". Chạy thử một lần với vài số rồi nhìn kết quả bằng mắt thì không đủ: lần sửa sau có thể âm thầm làm hỏng chỗ cũ. Cách làm chuyên nghiệp là viết sẵn các ca kiểm thử (test case): đầu vào + kết quả mong đợi, để máy tự so sánh.
Một bộ ca kiểm thử tốt gồm: trường hợp thông thường, trường hợp biên (0, 1, rỗng, số lớn nhất) và trường hợp sai/bất thường.
4.2. Kiểm thử thủ công bằng hàm check
Cách đơn giản nhất chưa cần thư viện: viết một phương thức check so sánh kết quả thật với kết quả mong đợi, in PASS/FAIL.
public class Main {
static int passed = 0, failed = 0;
static void check(String name, int expected, int actual) {
if (expected == actual) {
passed++;
} else {
failed++;
System.out.println("FAIL " + name + ": mong " + expected + ", được " + actual);
}
}
static int abs(int x) { return x < 0 ? -x : x; }
static int maxOfThree(int a, int b, int c) { return Math.max(a, Math.max(b, c)); }
public static void main(String[] args) {
check("abs dương", 5, abs(5));
check("abs âm", 5, abs(-5));
check("abs không", 0, abs(0));
check("max thường", 9, maxOfThree(4, 9, 6));
check("max bằng nhau", 3, maxOfThree(3, 3, 3));
check("max âm", -1, maxOfThree(-5, -1, -9));
check("cố tình sai", 10, abs(-5));
System.out.println("Đạt " + passed + ", trượt " + failed);
}
}Dòng cuối của bộ test cố tình sai để bạn thấy thông báo FAIL trông thế nào. Khi viết test, hãy đặt tên ca rõ ràng để dò lỗi nhanh.
4.3. Câu lệnh assert
Java có sẵn từ khóa assert: assert dieuKien : "thông báo"; ném AssertionError nếu điều kiện sai. Điểm cần nhớ: assert mặc định bị tắt, chỉ chạy khi bật cờ java -ea Main. Vì thế nó phù hợp để kiểm tra giả định lúc phát triển, không dùng để kiểm tra dữ liệu người dùng nhập (khi chạy thật không ai bật -ea).
public class Demo {
static int divide(int a, int b) {
assert b != 0 : "b không được bằng 0";
return a / b;
}
}4.4. Ý tưởng của JUnit
Trong dự án thật người ta dùng thư viện JUnit: mỗi ca kiểm thử là một phương thức gắn chú thích @Test, dùng các hàm như assertEquals(mongDoi, thucTe). Khung kiểm thử tự tìm, chạy mọi test, báo cáo cái nào đạt/trượt và chạy lại sau mỗi lần sửa code. Hàm check ở trên chính là phiên bản "tự chế" của ý tưởng đó.
// Chỉ minh họa (cần thư viện JUnit, không chạy trên Codelab)
@Test
void absOfNegativeIsPositive() {
assertEquals(5, MathUtil.abs(-5));
}Mẹo: viết phương thức thuần (cùng đầu vào luôn cùng đầu ra, không phụ thuộc biến toàn cục hay đọc bàn phím) thì kiểm thử rất dễ. Đó là lý do ta tách phần tính toán ra khỏi phần nhập/xuất.
5. Xử lý ngoại lệ: try, catch, finally
5.1. Ba loại lỗi và vị trí của ngoại lệ
Khi lập trình bạn gặp ba nhóm lỗi:
| Loại lỗi | Khi nào phát hiện | Ví dụ | Cách xử lý |
|---|---|---|---|
| Lỗi cú pháp / biên dịch | Lúc javac |
thiếu ;, sai kiểu |
Sửa code cho tới khi biên dịch được |
| Lỗi logic | Lúc chạy, kết quả sai | công thức sai, nhầm < với <= |
Kiểm thử, gỡ lỗi |
| Lỗi lúc chạy (runtime) | Lúc chạy, tình huống bất thường | chia cho 0, chỉ số mảng sai, tệp không tồn tại | Ngoại lệ |
Một ngoại lệ (exception) là một đối tượng mô tả sự cố, được "ném" (throw) ra tại chỗ xảy ra lỗi. Nếu không ai "bắt" (catch), ngoại lệ đi ngược lên chuỗi lời gọi và cuối cùng làm chương trình dừng kèm thông báo lỗi (stack trace). Ví dụ int x = 10 / 0; làm chương trình chết với ArithmeticException: / by zero.
Cơ chế ngoại lệ tốt hơn cách báo lỗi bằng mã trả về (return -1) vì: lỗi không thể bị lờ đi âm thầm, code xử lý lỗi tách khỏi code chính, và thông tin sự cố đi kèm (loại lỗi, thông điệp, vị trí).
5.2. Hệ phân cấp ngoại lệ
Mọi ngoại lệ là đối tượng thuộc lớp con của Throwable. Hình dưới là bản đồ bạn cần nhớ.
Checked (vàng cam, dưới Exception nhưng không thuộc RuntimeException): trình biên dịch bắt buộc catch hoặc throws. Unchecked (xanh lá, RuntimeException) và Error (đỏ): không bắt buộc.
Hãy đọc hình từ trên xuống: Throwable chia thành Error (sự cố nghiêm trọng của JVM như OutOfMemoryError, StackOverflowError — thường không cố bắt) và Exception. Dưới Exception, nhánh RuntimeException cùng các lớp con là unchecked; mọi nhánh còn lại (như IOException) là checked.
| Checked | Unchecked | |
|---|---|---|
| Lớp | Exception và con, trừ RuntimeException |
RuntimeException và con, cộng Error |
| Trình biên dịch | Bắt buộc bắt hoặc khai báo throws |
Không bắt buộc |
| Nguyên nhân thường gặp | Môi trường bên ngoài: tệp, mạng, cơ sở dữ liệu | Lỗi lập trình hoặc dữ liệu sai |
| Ví dụ | IOException, FileNotFoundException, SQLException |
ArithmeticException, NullPointerException, ArrayIndexOutOfBoundsException, NumberFormatException, IllegalArgumentException |
Lưu ý: quy tắc ngón tay cái: nếu người gọi có thể làm gì đó hợp lý khi sự cố xảy ra (thử lại, báo người dùng chọn tệp khác) thì dùng checked; nếu đó là lỗi do chương trình gọi sai (truyền đối số vô lý) thì dùng unchecked.
5.3. Cấu trúc try / catch
Đặt đoạn code có thể gây lỗi trong try; viết cách xử lý trong catch. Khi có ngoại lệ ở dòng nào trong try, phần còn lại của try bị bỏ qua và nhảy sang catch phù hợp.
public class Main {
public static void main(String[] args) {
int a = 10, b = 0;
try {
System.out.println("Bắt đầu chia");
int q = a / b;
System.out.println("Thương = " + q); // không bao giờ chạy tới đây
} catch (ArithmeticException e) {
System.out.println("Lỗi: " + e.getMessage());
}
System.out.println("Chương trình vẫn chạy tiếp");
}
}Đối tượng e cho bạn getMessage() (thông điệp ngắn), toString() (tên lớp + thông điệp) và getClass().getSimpleName() (tên lớp).
5.4. Nhiều khối catch và catch gộp
Một try có thể kèm nhiều catch cho các loại ngoại lệ khác nhau; Java kiểm tra từ trên xuống và chạy khối đầu tiên khớp. Vì vậy phải đặt lớp con trước, lớp cha sau — đặt Exception lên trên cùng làm các catch bên dưới "không bao giờ tới được" và trình biên dịch báo lỗi.
public class Main {
static void process(String text, int[] data, int index) {
try {
int v = Integer.parseInt(text);
System.out.println("Giá trị: " + v + ", phần tử: " + data[index]);
} catch (NumberFormatException e) {
System.out.println("Không phải số: " + e.getMessage());
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Chỉ số sai: " + e.getMessage());
} catch (Exception e) { // dự phòng, đặt cuối
System.out.println("Lỗi khác: " + e);
}
}
public static void main(String[] args) {
int[] data = {10, 20, 30};
process("42", data, 1);
process("abc", data, 1);
process("7", data, 5);
}
}Nếu hai loại lỗi cần xử lý giống hệt nhau, dùng multi-catch (từ Java 7) với dấu | để khỏi lặp code:
public class Main {
static int safeParseDivide(String a, String b) {
try {
return Integer.parseInt(a) / Integer.parseInt(b);
} catch (NumberFormatException | ArithmeticException e) {
System.out.println("Không tính được (" + e.getClass().getSimpleName() + ")");
return -1;
}
}
public static void main(String[] args) {
System.out.println(safeParseDivide("20", "4"));
System.out.println(safeParseDivide("20", "0"));
System.out.println(safeParseDivide("x", "4"));
}
}Cảnh báo: đừng viết
catch (Exception e) { }với thân rỗng — "nuốt" lỗi khiến chương trình sai mà không để lại dấu vết. Tối thiểu hãy in/ghi log, hoặc ném lại.
5.5. Khối finally
Khối finally chạy bất kể try thành công, có ngoại lệ, hay có return. Đây là chỗ để "dọn dẹp": đóng tệp, giải phóng tài nguyên. Chỉ System.exit() hoặc JVM chết đột ngột mới làm finally không chạy.
public class Main {
static int demo(int x) {
try {
System.out.println("try với x = " + x);
if (x == 0) throw new IllegalArgumentException("x = 0");
return 100 / x;
} catch (IllegalArgumentException e) {
System.out.println("catch: " + e.getMessage());
return -1;
} finally {
System.out.println("finally luôn chạy");
}
}
public static void main(String[] args) {
System.out.println("kết quả: " + demo(5));
System.out.println("kết quả: " + demo(0));
}
}Để ý thứ tự in: dù try đã gặp return, JVM vẫn chạy finally rồi mới trả giá trị về. Hãy bấm mô phỏng sau để thấy đường đi của luồng điều khiển qua try, catch, finally với từng loại ngoại lệ.
try – catch – finally chạy thế nào?
Chọn tình huống, theo dõi đường đi và thứ tự in ra consoletry {int r = 10 / 2;System.out.println("Kết quả: " + r);} catch (ArithmeticException e) {System.out.println("Chia cho 0! " + e.getMessage());} finally {System.out.println("Dọn dẹp");}System.out.println("Tiếp tục");
Chọn một ngoại lệ trong mô phỏng và theo dõi đường đi được tô sáng. Ba kịch bản cần nhớ: (1) không lỗi: try → finally; (2) có lỗi được bắt: try (dừng giữa chừng) → catch → finally; (3) có lỗi không ai bắt: try → finally → ngoại lệ tiếp tục lan ra ngoài.
Mẹo: không đặt
returnhoặcthrowbên trongfinally— nó có thể ghi đè giá trị trả về hoặc che mất ngoại lệ gốc, gây lỗi rất khó tìm.
5.6. Ngoại lệ lan truyền ngược lên chuỗi lời gọi
Nếu phương thức không bắt ngoại lệ, nó được "ném tiếp" cho nơi gọi nó, rồi nơi gọi nữa, cho tới khi có catch phù hợp hoặc ra khỏi main (chương trình dừng). Quá trình tháo dần các khung Stack này gọi là stack unwinding.
public class Main {
static void level3() {
System.out.println("level3: sắp chia cho 0");
int x = 1 / 0;
System.out.println("không in được");
}
static void level2() {
try {
level3();
} finally {
System.out.println("level2: finally chạy khi ngoại lệ đi qua");
}
}
static void level1() {
try {
level2();
} catch (ArithmeticException e) {
System.out.println("level1: bắt được " + e.getMessage());
}
}
public static void main(String[] args) {
level1();
System.out.println("main: kết thúc bình thường");
}
}Nguyên tắc: chỉ bắt ngoại lệ ở nơi bạn biết cách xử lý có ý nghĩa (thông báo người dùng, thử lại, dùng giá trị mặc định). Nơi nào không biết xử lý thì cứ để nó lan lên.
5.7. Xử lý dữ liệu nhập sai — một mẫu thực tế
Mẫu rất hay gặp: đọc đầu vào, nếu không hợp lệ thì dùng giá trị thay thế hoặc báo lỗi, rồi tiếp tục các phần tử sau.
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int sum = 0, bad = 0;
while (sc.hasNextLine()) {
String token = sc.nextLine().trim();
try {
sum += Integer.parseInt(token);
} catch (NumberFormatException e) {
bad++;
System.out.println("Bỏ qua: " + token);
}
}
System.out.println("Tổng các số hợp lệ = " + sum + ", sai: " + bad);
}
}Ví dụ thực tế: form đăng ký sinh viên nhận "tuổi" là chuỗi. Gọi
Integer.parseInttrongtry; nếuNumberFormatExceptionthì báo "Tuổi phải là số nguyên" và cho nhập lại — thay vì để cả ứng dụng sập vì một ô nhập sai.
Đến lượt bạn luyện bắt ngoại lệ với các bài đọc số, tổng điểm hợp lệ, máy tính bỏ túi an toàn:
Vừa học xong try / catch / finally, hãy làm các bài sau để tập xác định đúng loại ngoại lệ cần bắt và giữ cho chương trình chạy tiếp khi gặp dữ liệu xấu.
6. Ném ngoại lệ, ngoại lệ tự định nghĩa và try-with-resources
6.1. Câu lệnh throw
Không chỉ JVM ném ngoại lệ — chính bạn cũng ném khi phát hiện dữ liệu sai: throw new TenNgoaiLe("thông điệp");. Ngay khi throw chạy, phương thức dừng và ngoại lệ bắt đầu lan lên. Đây là cách tốt nhất để thực thi hợp đồng của phương thức: nếu tham số không hợp lệ, từ chối thay vì cho ra kết quả vô nghĩa.
public class Main {
static double sqrt(double x) {
if (x < 0) {
throw new IllegalArgumentException("Không lấy căn số âm: " + x);
}
return Math.sqrt(x);
}
public static void main(String[] args) {
System.out.println(sqrt(16));
try {
System.out.println(sqrt(-4));
} catch (IllegalArgumentException e) {
System.out.println("Bị từ chối: " + e.getMessage());
}
}
}Các ngoại lệ có sẵn thường dùng khi tự ném: IllegalArgumentException (đối số sai), IllegalStateException (đối tượng đang ở trạng thái không cho phép thao tác), UnsupportedOperationException, ArithmeticException.
6.2. Khai báo throws và quy tắc "bắt hoặc khai báo"
Với ngoại lệ checked, nếu phương thức có thể ném mà không tự bắt, bạn phải khai báo trong chữ ký bằng throws. Người gọi khi đó buộc phải hoặc bắt, hoặc tiếp tục khai báo throws. Trình biên dịch kiểm tra quy tắc này — thiếu là lỗi biên dịch unreported exception.
public static void docTep(String path) throws IOException { // khai báo: có thể ném IOException
// ...
}Phân biệt hai từ dễ lẫn: throw là câu lệnh ném một đối tượng ngoại lệ cụ thể; throws là khai báo ở chữ ký phương thức về các loại ngoại lệ có thể thoát ra.
6.3. Ngoại lệ tự định nghĩa
Khi ngoại lệ có sẵn không diễn tả đúng nghiệp vụ, bạn tạo lớp riêng bằng cách kế thừa Exception (checked) hoặc RuntimeException (unchecked). Đặt tên kết thúc bằng Exception, và cung cấp hàm tạo nhận thông điệp (và tùy chọn nguyên nhân cause). Trong ví dụ ngân hàng sau, rút quá số dư là tình huống nghiệp vụ mà người gọi nên xử lý, nên ta chọn checked:
class InsufficientFundsException extends Exception {
private final double missing;
InsufficientFundsException(double missing) {
super("Thiếu " + missing + " để thực hiện giao dịch");
this.missing = missing;
}
double getMissing() { return missing; }
}
public class Main {
static double withdraw(double balance, double amount) throws InsufficientFundsException {
if (amount > balance) throw new InsufficientFundsException(amount - balance);
return balance - amount;
}
public static void main(String[] args) {
double balance = 500;
for (double amount : new double[]{200, 400}) {
try {
balance = withdraw(balance, amount);
System.out.println("Rút " + amount + ", còn " + balance);
} catch (InsufficientFundsException e) {
System.out.println(e.getMessage() + " (thiếu " + e.getMissing() + ")");
}
}
}
}Lớp ngoại lệ có thể chứa thêm dữ liệu (ở đây là missing) để người xử lý có thông tin chi tiết hơn chỉ một câu thông báo. Bạn có thể dựng cả hệ phân cấp: BankException là lớp cha chung, còn InsufficientFundsException, InvalidAccountException là các lớp con; nhờ đó người gọi chọn bắt từng loại hoặc bắt chung BankException.
6.4. Xích ngoại lệ (exception chaining)
Đôi khi bạn bắt một ngoại lệ ở tầng thấp rồi muốn ném ngoại lệ mang ý nghĩa của tầng cao hơn, nhưng không được làm mất nguyên nhân gốc. Hãy truyền ngoại lệ gốc làm tham số cause khi tạo ngoại lệ mới; có thể đọc lại bằng getCause().
public class Main {
static int parseAge(String text) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException e) {
throw new IllegalStateException("Dữ liệu tuổi hỏng", e); // giữ nguyên nhân gốc
}
}
public static void main(String[] args) {
try {
parseAge("hai mươi");
} catch (IllegalStateException e) {
System.out.println("Lỗi: " + e.getMessage());
System.out.println("Nguyên nhân: " + e.getCause().getClass().getSimpleName());
System.out.println("Chi tiết: " + e.getCause().getMessage());
}
}
}Mẹo: ném lại (re-throw) với
throw e;khi bạn chỉ muốn ghi log rồi cho ngoại lệ đi tiếp. Dùng chaining khi muốn đổi loại ngoại lệ nhưng vẫn giữ lịch sử lỗi.
6.5. try-with-resources
Tài nguyên (resource) như tệp, kết nối cơ sở dữ liệu, luồng dữ liệu phải được đóng sau khi dùng, kể cả khi có lỗi. Viết bằng finally dài và dễ sai. Từ Java 7, cú pháp try (tài nguyên) { … } giúp tự động gọi close() ngay khi rời khối try. Điều kiện duy nhất: lớp tài nguyên cài đặt giao diện AutoCloseable.
class Resource implements AutoCloseable {
private final String name;
Resource(String name) {
this.name = name;
System.out.println("MỞ " + name);
}
void use() { System.out.println("dùng " + name); }
@Override
public void close() { System.out.println("ĐÓNG " + name); }
}
public class Main {
public static void main(String[] args) {
try (Resource a = new Resource("A"); Resource b = new Resource("B")) {
a.use();
b.use();
throw new IllegalStateException("sự cố giữa chừng");
} catch (IllegalStateException e) {
System.out.println("catch: " + e.getMessage());
} finally {
System.out.println("finally");
}
}
}Quan sát thứ tự: tài nguyên được đóng theo thứ tự ngược lại với lúc mở (B rồi A), việc đóng xảy ra trước catch và finally. Nếu cả thân try lẫn close() cùng ném lỗi, lỗi từ close() được gắn vào ngoại lệ chính dưới dạng suppressed (đọc bằng getSuppressed()), không làm mất lỗi gốc. Ở Buổi 8 bạn sẽ dùng cú pháp này với tệp thật.
Lưu ý: tài nguyên khai báo trong ngoặc của
trymặc định làfinal; không gán lại được. Dùng try-with-resources thay chotry/finallybất cứ khi nào có thể.
6.6. Thực hành tốt khi xử lý ngoại lệ
- Cụ thể trước, chung sau: bắt loại hẹp nhất có thể;
catch (Exception e)chỉ ở tầng ngoài cùng. - Không nuốt lỗi: luôn xử lý, ghi log hoặc ném lại.
- Đừng dùng ngoại lệ thay cho điều khiển luồng bình thường: kiểm tra
if (b != 0)rẻ và rõ hơn là chờArithmeticException— ngoại lệ dành cho tình huống bất thường. - Thông điệp có ích: ghi rõ giá trị sai và điều mong đợi.
- Chọn checked/unchecked có chủ đích: checked cho sự cố người gọi có thể phục hồi; unchecked cho lỗi lập trình.
Cuối cùng là phần ném ngoại lệ, định nghĩa ngoại lệ riêng và tài nguyên tự đóng:
Vừa học xong throw, throws, ngoại lệ tự định nghĩa và try-with-resources, hãy làm các bài sau để thiết kế hệ ngoại lệ nghiệp vụ và kiểm soát thứ tự đóng tài nguyên.
7. JavaDoc — tài liệu hóa phương thức
7.1. Chú thích JavaDoc
Một phương thức tốt cần "tài liệu sống" ngay cạnh code. JavaDoc là công cụ của JDK biến các chú thích đặc biệt dạng /** … */ đặt ngay trước lớp/phương thức/trường thành bộ tài liệu HTML (chính là trang tài liệu API Java bạn vẫn tra cứu). Dòng đầu là mô tả ngắn; tiếp theo là các thẻ (tag):
| Thẻ | Ý nghĩa |
|---|---|
@param ten mô_tả |
Giải thích từng tham số |
@return mô_tả |
Giải thích giá trị trả về |
@throws TenNgoaiLe khi_nào |
Khi nào phương thức ném ngoại lệ |
@see, @since, @author, @version |
Tham chiếu, phiên bản, tác giả |
{@code x} |
Hiển thị x dạng mã trong tài liệu |
/**
* Tính điểm trung bình có trọng số của một sinh viên.
*
* @param scores mảng điểm thành phần, mỗi điểm trong khoảng 0 đến 10
* @param weights mảng trọng số, cùng độ dài với {@code scores}, tổng bằng 1
* @return điểm trung bình có trọng số
* @throws IllegalArgumentException nếu hai mảng khác độ dài
*/
public static double weightedAverage(double[] scores, double[] weights) {
if (scores.length != weights.length) {
throw new IllegalArgumentException("Độ dài hai mảng phải bằng nhau");
}
double total = 0;
for (int i = 0; i < scores.length; i++) total += scores[i] * weights[i];
return total;
}7.2. Sinh tài liệu và dùng trong IDE
Chạy công cụ javadoc (có sẵn trong JDK) để sinh thư mục HTML; mở index.html trong thư mục đích để xem:
javadoc -d docs -encoding UTF-8 -charset UTF-8 MathUtil.javaIntelliJ IDEA và VS Code cũng hiển thị JavaDoc khi bạn rê chuột vào lời gọi phương thức (Quick Documentation), nên viết JavaDoc tốt giúp cả đồng nghiệp lẫn chính bạn sau này. Gõ /** rồi Enter ngay trên phương thức, IDE tự sinh khung với các thẻ @param, @return.
Mẹo: JavaDoc mô tả hợp đồng (làm gì, ràng buộc gì, ném gì), không chép lại từng dòng code. Một chú thích như "cộng a với b" cho hàm
add(a, b)là thừa.
Bài tập đề cương cho phần này là tự tạo JavaDoc cho các phương thức của bạn: chọn một vài bài đã làm ở trên, thêm chú thích /** … */ đầy đủ @param, @return, @throws, rồi chạy javadoc và mở trang kết quả.
Sai lầm thường gặp
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
Nghĩ Java truyền đối tượng "theo tham chiếu" và viết swap(a, b) |
Hoán đổi không có tác dụng với biến bên ngoài | Nhớ Java truyền giá trị; trả về giá trị mới hoặc đổi nội dung đối tượng/mảng |
Quên return trên một nhánh |
Lỗi biên dịch missing return statement | Mọi đường đi đều có return hoặc throw |
| Chồng hàm chỉ khác kiểu trả về | Lỗi biên dịch | Phải khác số lượng hoặc kiểu tham số |
| Hàm đệ quy thiếu điều kiện dừng hoặc tham số không tiến về điều kiện dừng | StackOverflowError |
Viết điều kiện dừng trước; kiểm tra mỗi lần gọi bài toán nhỏ đi |
Fibonacci đệ quy thuần với n lớn |
Chạy cực chậm (độ phức tạp mũ) | Ghi nhớ kết quả (memoization) hoặc dùng vòng lặp |
Đặt catch (Exception e) trước các catch cụ thể |
Lỗi biên dịch (code không tới được) | Lớp con trước, lớp cha sau |
catch (Exception e) { } thân rỗng |
Lỗi bị nuốt, chương trình sai mà không biết | Ghi log, thông báo hoặc ném lại |
Dùng return/throw trong finally |
Ghi đè kết quả hoặc che ngoại lệ gốc | Chỉ để dọn dẹp trong finally |
Ném ngoại lệ mới mà quên truyền cause |
Mất nguyên nhân gốc, khó gỡ lỗi | new X("…", e) |
Dùng assert để kiểm tra dữ liệu người dùng |
Mặc định bị tắt nên không chạy | Dùng if + ném ngoại lệ |
Quên throws cho ngoại lệ checked |
Lỗi biên dịch unreported exception | Bắt hoặc khai báo throws |
Tóm tắt
- Phương thức = khối lệnh có tên, tham số, giá trị trả về; Java truyền theo giá trị (với đối tượng là giá trị của tham chiếu).
- Chồng hàm phân biệt bằng số lượng/kiểu tham số (không phải kiểu trả về); varargs (
int... nums) thực chất là mảng, đặt cuối danh sách. - Đệ quy = điều kiện dừng + bước đệ quy thu nhỏ bài toán; mỗi lời gọi một khung Stack; dùng ghi nhớ khi có bài toán con lặp lại.
- Kiểm thử: viết ca thông thường, biên, bất thường;
assertchỉ chạy khi-ea; JUnit tự động hóa ý tưởng này. - Ngoại lệ:
Throwable→Error/Exception→ checked /RuntimeException(unchecked).try+catch(con trước cha) +finally(luôn chạy). throwném,throwskhai báo; ngoại lệ tự định nghĩa kế thừaExceptionhoặcRuntimeException; truyềncauseđể giữ nguyên nhân; try-with-resources tự đóng tài nguyênAutoCloseabletheo thứ tự ngược.- JavaDoc
/** … */với@param,@return,@throwsrồi chạyjavadocđể sinh tài liệu.