VieTopik
VisaTrường đại họcViệc làmHọc hiệu quả
  • Góc học tập
Tải app
  • Trang chủ
  • Thư viện
  • Luyện thi
  • Cẩm nang
  • Góc học tập
Cẩm nang · Học hiệu quả

Debug có phương pháp

Cách đọc thông báo lỗi và stack trace của Java, sửa lỗi theo vòng lặp giả thuyết thay vì sửa mò, và nhận ra bốn lỗi người mới hay gặp.

Cập nhật 01/10/2026. Tổng hợp từ nghiên cứu về học tập. Mức bằng chứng ghi kèm từng lời khuyên — thử và chỉnh theo nhịp của bạn.

Đã làm: 0/4 việc
Nội dung bài · 8 mục
  1. 1.Debug là một kỹ năng riêng
  2. 2.Đọc stack trace từ trên xuống
  3. 3.Thông báo lỗi "dễ hiểu hơn" có giúp không?
  4. 4.Vòng lặp giả thuyết thay cho sửa mò
  5. 5.Bốn lỗi kinh điển của người mới
  6. 6.Giải thích code thành lời
  7. 7.Áp dụng ngay
  8. 8.Trên VieTopik

Chương trình báo lỗi đỏ cả màn hình, và phản xạ đầu tiên của nhiều người mới là sửa thử một dòng bất kỳ rồi chạy lại. Bài này đi qua cách đọc thông báo lỗi của Java, một quy trình sửa lỗi từng bước, và những lỗi kinh điển của người mới học.

Debug là một kỹ năng riêng

Viết được code và tìm được lỗi trong code là hai việc khác nhau. McCauley và cộng sự (2008) tổng quan các nghiên cứu về dạy và học debug, có những nghiên cứu từ giữa thập niên 1970. Họ kết luận debug vẫn là kỹ năng khó học với người mới lập trình và khó dạy với giáo viên, dù đã có nhiều nghiên cứu.

Debug chậm không có nghĩa là "không hợp với lập trình". Đây là kỹ năng cần luyện riêng.

Mức bằng chứng: vừa — một bài tổng quan tài liệu, không phải phân tích gộp; nhận định "debug khó với người mới" được nhiều nghiên cứu trong đó ủng hộ, nhưng tổng quan không đo một phương pháp luyện cụ thể nào.

Đọc stack trace từ trên xuống

Chương trình dưới đây đếm số chữ cái trong mỗi tên. Mảng có một phần tử null.

public class Greeting {
    public static void main(String[] args) {
        String[] names = { "An", null, "Binh" };
        for (String name : names) {
            System.out.println(countLetters(name));
        }
    }

    static int countLetters(String name) {
        String trimmed = name.trim();
        return trimmed.length();
    }
}

Chạy bằng java Greeting.java trên JDK 25, kết quả là:

2
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.trim()" because "<parameter1>" is null
	at Greeting.countLetters(Greeting.java:10)
	at Greeting.main(Greeting.java:5)

Đọc từng phần:

  • 2: tên đầu tiên chạy đúng, lỗi xảy ra ở phần tử thứ hai.
  • Exception in thread "main": lỗi xảy ra trong luồng chính của chương trình.
  • java.lang.NullPointerException: loại lỗi. Gọi phương thức trên một biến đang là null.
  • Cannot invoke "String.trim()" because "<parameter1>" is null: thông điệp. Không gọi được trim() vì tham số thứ nhất của phương thức là null.
  • Các dòng at: đường đi của lời gọi, dòng trên cùng là nơi lỗi xảy ra. countLetters ở dòng 10 bị lỗi, và countLetters được gọi từ main ở dòng 5.

Ở đây là <parameter1> chứ không phải tên biến name, vì theo JEP 358 của OpenJDK, tên biến chỉ được in khi biên dịch có thông tin debug (javac -g). Biên dịch javac -g Greeting.java rồi chạy java Greeting, thông điệp đổi thành:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.trim()" because "name" is null

Nhiều khi dòng trên cùng nằm trong thư viện Java, không phải code của bạn:

public class Age {
    public static void main(String[] args) {
        String input = "12a";
        int age = Integer.parseInt(input);
        System.out.println(age);
    }
}
Exception in thread "main" java.lang.NumberFormatException: For input string: "12a"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:565)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at Age.main(Age.java:4)

Ba dòng at java.base/... là code của thư viện chuẩn. Thư viện không sai; nó chỉ báo rằng đầu vào "12a" không phải số. Dòng đầu tiên mang tên lớp của bạn, Age.main(Age.java:4), là nơi cần xem: dòng 4 đã đưa chuỗi sai vào parseInt.

Quy tắc ngắn: đọc loại lỗi, đọc thông điệp, rồi dò các dòng at từ trên xuống cho tới dòng đầu tiên thuộc code của mình.

Thông báo lỗi "dễ hiểu hơn" có giúp không?

Một số nghiên cứu đã thử viết lại thông báo lỗi biên dịch Java cho dễ hiểu hơn, với kết quả chưa thống nhất.

Denny, Luxton-Reilly và Carpenter (2014) thử trong một khoá nhập môn lập trình Java ở Đại học Auckland (New Zealand). 83 sinh viên được chia ngẫu nhiên: 42 người nhận thông báo lỗi gốc, 41 người nhận thông báo mở rộng, có giải thích và ví dụ code sai, code đúng. Nhóm tác giả không thấy khác biệt có ý nghĩa thống kê về số lần nộp code không biên dịch được, cũng như số lần thử cần để sửa ba lỗi cú pháp phổ biến nhất. Họ cho rằng có thể sinh viên không đọc phần giải thích dài thêm.

Becker và cộng sự (2016) làm một nghiên cứu lớn hơn, khoảng 200 sinh viên với gần 50.000 lỗi, dùng một IDE riêng hiển thị thông báo lỗi Java mở rộng, so với nhóm đối chứng nhận thông báo thường. Nhóm can thiệp giảm tổng số lỗi, số lỗi trên mỗi sinh viên và một số chỉ số lỗi lặp lại. Chính bài báo này ghi nhận rằng hiệu quả của việc mở rộng thông báo lỗi đang được tranh luận.

Lưu ý phạm vi: cả hai nghiên cứu đều về lỗi biên dịch (lỗi cú pháp, lỗi kiểu), không phải stack trace lúc chạy, và đều ở sinh viên mới học lập trình. Với người tự học, việc chắc chắn làm được là đọc kỹ thông báo đang có trước khi sửa.

Mức bằng chứng: còn tranh luận — hai nghiên cứu cho kết quả ngược nhau; chưa rõ thông báo mở rộng giúp được ai, trong hoàn cảnh nào.

Vòng lặp giả thuyết thay cho sửa mò

Thay vì sửa ngẫu nhiên, làm theo sáu bước:

  1. Tái hiện lỗi. Chạy lại và chắc chắn lỗi xuất hiện mỗi lần với cùng đầu vào.
  2. Thu hẹp. Dùng stack trace để tìm dòng của mình. Nếu không có stack trace (chương trình chạy nhưng kết quả sai), bỏ bớt dữ liệu hoặc bỏ bớt code cho tới khi còn ví dụ nhỏ nhất vẫn lỗi.
  3. Đặt một giả thuyết. Viết thành một câu, ví dụ "name là null khi tới phần tử thứ hai". Chỉ một giả thuyết mỗi lần.
  4. Kiểm tra giả thuyết. In giá trị ra, hoặc đặt breakpoint trong IDE và xem biến.
  5. Sửa. Chỉ sửa đúng chỗ giả thuyết đã xác nhận.
  6. Chạy lại. Kiểm tra lỗi cũ đã hết và các trường hợp đúng trước đó vẫn đúng.

Với ví dụ Greeting, bước 4 có thể là thêm một dòng in tạm:

        for (String name : names) {
            System.out.println("DEBUG name = " + name);
            System.out.println(countLetters(name));
        }
DEBUG name = An
2
DEBUG name = null
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.trim()" because "<parameter1>" is null
	at Greeting.countLetters(Greeting.java:11)
	at Greeting.main(Greeting.java:6)

Giả thuyết đúng. Số dòng trong stack trace đổi thành 11 và 6 vì đã chèn thêm một dòng. Bước 5: quyết định chương trình nên làm gì với null, ví dụ coi là 0 chữ cái, rồi xoá dòng DEBUG:

    static int countLetters(String name) {
        if (name == null) {
            return 0;
        }
        String trimmed = name.trim();
        return trimmed.length();
    }

Chạy lại ra 2, 0, 4. Nếu giả thuyết sai, quay lại bước 3 với một giả thuyết mới, đừng sửa thêm.

Mức bằng chứng: vừa — suy rộng: tổng quan của McCauley và cộng sự (2008) cho thấy debug khó với người mới, nhưng chưa có thí nghiệm so sánh trực tiếp quy trình sáu bước với cách sửa mò.

Bốn lỗi kinh điển của người mới

Lệch một (off-by-one). Chỉ số mảng chạy từ 0 tới length - 1.

public class OffByOne {
    public static void main(String[] args) {
        int[] scores = { 7, 8, 9 };
        int sum = 0;
        for (int i = 0; i <= scores.length; i++) {
            sum += scores[i];
        }
        System.out.println(sum);
    }
}
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at OffByOne.main(OffByOne.java:6)

Thông điệp nói đúng vấn đề: mảng dài 3 nhưng truy cập chỉ số 3. Đổi <= thành <, kết quả là 24.

So sánh chuỗi bằng ==. Với String, == kiểm tra hai biến có trỏ cùng một đối tượng không. Muốn so nội dung thì dùng equals.

import java.util.Scanner;

public class CompareText {
    public static void main(String[] args) {
        String expected = "java";
        String typed = new Scanner("java").next();
        System.out.println(typed == expected);
        System.out.println(typed.equals(expected));
    }
}
false
true

Hai chuỗi cùng nội dung java, nhưng chuỗi đọc từ Scanner là một đối tượng khác, nên == cho false.

Chia số nguyên. Khi cả hai toán hạng đều là số nguyên (int), Java bỏ phần thập phân (làm tròn về phía 0).

public class Average {
    public static void main(String[] args) {
        int total = 5;
        int count = 2;
        System.out.println(total / count);
        System.out.println((double) total / count);
    }
}
2
2.5

Không có thông báo lỗi nào, chỉ có kết quả sai. Đây là lúc bước "đặt giả thuyết và in giá trị" có ích hơn đọc stack trace.

NullPointerException. Đã gặp ở ví dụ Greeting. Trên JDK 25 dùng trong bài, thông điệp cho biết lời gọi nào gặp null và giá trị đó nằm ở tham số hay biến nào (tên biến chỉ hiện khi biên dịch với javac -g). Câu hỏi tiếp theo luôn là: giá trị null này đến từ đâu?

Mức bằng chứng: mạnh — cách chia số nguyên, phép so sánh == và việc ném exception khi vượt chỉ số mảng được quy định trong đặc tả ngôn ngữ Java (JLS); nội dung thông điệp lỗi là của JDK (JEP 358) và đã chạy lại trên JDK 25.

Giải thích code thành lời

Khi bí, giải thích từng dòng code thành lời, cho một người bạn hoặc thậm chí cho một con vịt cao su đặt trên bàn. Lập trình viên gọi đây là "rubber duck debugging". Khi phải nói rõ "dòng này lấy phần tử thứ i, mà i chạy tới length", chỗ hiểu sai thường lộ ra.

Cách này gần với tự giải thích (self-explanation) đã nói trong bài Đọc code trước, viết code sau. Các nghiên cứu về tự giải thích được làm ở vật lý và sinh học, không phải ở debug.

Mức bằng chứng: vừa — suy rộng từ nghiên cứu tự giải thích ở vật lý và sinh học; chưa có nghiên cứu kiểm chứng trực tiếp rubber duck debugging.

Áp dụng ngay

Trên VieTopik

  • Khoá Java và Java Core: mỗi bài có giải thích và ví dụ code. Chạy thử ví dụ, cố ý sửa sai một dòng và đọc thông báo lỗi Java đưa ra.
  • Khoá C# .NET: cùng cấu trúc; stack trace của C# cũng đọc theo cách từ trên xuống tương tự.
  • Nhiều bài có câu hỏi kiểm tra cuối bài. Xem thêm Tự kiểm tra thay vì đọc lại.

Nguồn

  • McCauley, Fitzgerald, Lewandowski, Murphy, Simon, Thomas & Zander (2008). Debugging: a review of the literature from an educational perspective. Computer Science Education doi.org
  • Denny, Luxton-Reilly & Carpenter (2014). Enhancing syntax error messages appears ineffectual. ITiCSE 2014 doi.org
  • Becker, Glanville, Iwashima, McDonnell, Goslin & Mooney (2016). Effective compiler error message enhancement for novice programming students. Computer Science Education doi.org
  • OpenJDK: JEP 358: Helpful NullPointerExceptions openjdk.org
  • Oracle: The Java Language Specification, Java SE 25, Chương 15 (15.17.2 phép chia, 15.21.3 so sánh tham chiếu) docs.oracle.com

Nội dung bài

  1. 1.Debug là một kỹ năng riêng
  2. 2.Đọc stack trace từ trên xuống
  3. 3.Thông báo lỗi "dễ hiểu hơn" có giúp không?
  4. 4.Vòng lặp giả thuyết thay cho sửa mò
  5. 5.Bốn lỗi kinh điển của người mới
  6. 6.Giải thích code thành lời
  7. 7.Áp dụng ngay
  8. 8.Trên VieTopik