Cài đặt Java và chương trình Hello World

Xem thử

Tình huống mở đầu

Buổi học đầu tiên, bạn viết xong Hello.java, biên dịch thành công và thấy file Hello.class nằm ngay trong thư mục. Bạn gõ java Hello.class vì thấy đúng tên file đó, và nhận về Error: Could not find or load main class Hello.class. Bạn đổi thư mục, gõ lại ba lần, mất 15 phút. File vẫn nằm đó, rõ ràng như vậy.

Đây là chuyện rất hay gặp ở ngày đầu, và thông báo trên là thông báo thật mình đã chạy ra. Máy không hỏng, Java cũng không hỏng: lệnh java chỉ đang nhận một thứ khác với thứ bạn đưa cho nó. Vậy lệnh java muốn nhận gì, và lệnh javac thì sao?

Ôn nhanh bài trước: máy của Hoa in javac command = false nhưng compiler API = true. Máy đó có chạy được java Hello.java không, và vì sao?

Đáp án: có. Lệnh java Hello.java nhờ chính JVM biên dịch trong bộ nhớ bằng module jdk.compiler, nên không cần file lệnh javac. Chỉ khi bạn muốn gõ javac trong terminal thì mới cần JDK.

Mục tiêu bài học

Sau bài này, bạn có thể:

  • Kiểm tra được máy đã có JDK chưa bằng java -version, javac -version và biến JAVA_HOME, và đọc đúng ý nghĩa từng dòng kết quả.
  • Viết, biên dịch và chạy Hello bằng hai cách, rồi nói đúng file nào xuất hiện sau mỗi lệnh.
  • Giải thích được vì sao javac nhận tên file còn java nhận tên lớp, nên java Hello.class bị lỗi (điều gì xảy ra trong bộ khởi chạy).
  • Xếp một thông báo lỗi vào đúng trạm (biên dịch, khởi chạy, thực thi) và đọc được mã thoát.

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.

Bức tranh lớn (Big Picture)

Trước khi gõ lệnh nào, hãy nhìn toàn bộ chặng đường của một chương trình Java nhỏ nhất.

Hình: ba trạm của Hello World, và mỗi trạm có loại lỗi riêng

Cách đọc hình: hàng trên có năm ô nối nhau từ trái sang phải: Hello.java (trạm 1, bạn viết), javac Hello.java (trạm 2, kiểm tra và đóng gói), Hello.class (kết quả), java Hello (trạm 3, chạy) và Output (chữ in ra cùng mã thoát). Hàng dưới có ba ô đỏ nhạt nối bằng mũi tên nét đứt: lỗi biên dịch ở dưới trạm 2, lỗi khởi chạy ở dưới trạm 3, lỗi thực thi ở dưới ô kết quả. Màu chỉ là tín hiệu phụ, tên từng ô mới là thông tin chính.

Mỗi thông báo lỗi đều sinh ra ở một trạm nhất định, và biết trạm nào thì biết nên nhìn vào đâu. Lỗi biên dịch nằm trong mã bạn viết, lỗi khởi chạy nằm ở cách bạn gõ lệnh, lỗi thực thi nằm trong lúc chương trình chạy.

Xuyên suốt bài, mình dùng một hình ảnh: một dây chuyền ba trạm trong xưởng, mỗi trạm có người kiểm hàng riêng. Dự án xuyên suốt là chiếc máy chào khách của quán Cà Phê Gió, bắt đầu bằng Hello và lớn dần thành Welcome. Có chỗ hình ảnh này không còn đúng: ở dây chuyền thật, hàng hỏng ở trạm 2 không đi tiếp được, nhưng ở đây bản Hello.class cũ vẫn nằm trong thư mục và trạm 3 vẫn chạy nó. Mình sẽ cho bạn thấy điều đó bằng một thí nghiệm ở phần sau.

Khái niệm cốt lõi

Bài này có bốn khái niệm. Mỗi khái niệm đi kèm một hình.

Khái niệm 1: kiểm tra JDK bằng ba việc, java -version, javac -version và JAVA_HOME

JDK (bộ công cụ phát triển Java) có đủ cả lệnh java để chạy và lệnh javac để dịch. Để biết máy bạn đã có JDK chưa, mở một terminal mới (terminal cũ chưa đọc cấu hình mới) và làm ba việc: gõ java -version, gõ javac -version, rồi xem biến môi trường JAVA_HOME (một biến chứa đường dẫn thư mục cài JDK mà một số công cụ như IDE, Maven, Gradle dùng để tìm JDK [Suy luận từ tài liệu các công cụ đó, bài này không chạy chúng]). Trên Linux và macOS dùng echo $JAVA_HOME; trên Windows dùng echo %JAVA_HOME% trong Command Prompt hoặc echo $env:JAVA_HOME trong PowerShell [Có thể khác nhau, mình không chạy được trên Windows].

Đây là kết quả trên máy mình, một máy Ubuntu 20.04 chỉ có runtime [Đã chạy]:

text
$ java -version
openjdk version "17.0.15" 2025-04-15
OpenJDK Runtime Environment (build 17.0.15+6-Ubuntu-0ubuntu120.04)
OpenJDK 64-Bit Server VM (build 17.0.15+6-Ubuntu-0ubuntu120.04, mixed mode, sharing)
$ javac -version
bash: javac: command not found
$ echo $JAVA_HOME

$ which java
/usr/bin/java
$ readlink -f $(which java)
/usr/lib/jvm/java-17-openjdk-amd64/bin/java

Từng dòng nói gì với bạn:

Dòng in raNghĩa là gìBạn rút ra điều gì
openjdk version "17.0.15" 2025-04-15Số phiên bản Java và ngày phát hành bản đó. 17 là bản chính, 0.15 là số bản váLệnh java này là Java 17, bản mà khóa học dùng
OpenJDK Runtime Environment (build 17.0.15+6-Ubuntu-...)Tên bộ runtime và mã build do nhà đóng gói đặt. Ở đây nhà đóng gói là UbuntuCho biết bản này đến từ đâu. Bản Temurin ở dưới sẽ ghi Temurin-17.0.21+9
OpenJDK 64-Bit Server VM (..., mixed mode, sharing)Tên JVM. mixed mode nghĩa là vừa thông dịch vừa biên dịch JIT như bài trước. sharing là JVM dùng một kho lớp dùng chung để khởi động nhanh [Suy luận từ tên cờ, mình không đo]JVM chạy bình thường. Dòng này không cho biết máy có JDK hay không
bash: javac: command not foundShell tìm file tên javac trong mọi thư mục của PATH và không thấy file nàoMáy chưa có JDK, hoặc có JDK nhưng thư mục bin của nó chưa nằm trong PATH
dòng trống sau echo $JAVA_HOMEBiến JAVA_HOME chưa được đặtKhông sao với lệnh java: máy mình vẫn chạy được java khi biến này rỗng
readlink -f $(which java)Đường dẫn thật của lệnh java sau khi đi qua các liên kếtChỉ ra thư mục cài. Trong thư mục bin này không có javac, khớp với bài trước

Để thấy một máy có JDK trông thế nào, mình tải bộ JDK 17 của Eclipse Temurin cho Linux x64 theo liên kết mà API của Adoptium (adoptium.net) trả về, đối chiếu mã băm SHA-256 với giá trị mà API của Adoptium công bố, giải nén vào một thư mục tạm và đặt hai biến cho riêng terminal đó. Ở đây <thư mục giải nén> là chỗ giữ chỗ cho đường dẫn thật (đường dẫn thật của mình nằm trong thư mục tạm của máy) [Đã chạy, Eclipse Temurin 17.0.21]:

text
$ export JAVA_HOME="<thư mục giải nén>/jdk-17.0.21+9"
$ export PATH="$JAVA_HOME/bin:$PATH"
$ javac -version
javac 17.0.21
$ java -version
openjdk version "17.0.21" 2026-10-20
OpenJDK Runtime Environment Temurin-17.0.21+9 (build 17.0.21+9)
OpenJDK 64-Bit Server VM Temurin-17.0.21+9 (build 17.0.21+9, mixed mode, sharing)
$ which javac
<thư mục giải nén>/jdk-17.0.21+9/bin/javac

Dòng export PATH="$JAVA_HOME/bin:$PATH" đặt thư mục bin của JDK lên đầu danh sách. Shell tìm lệnh từ trái sang phải, nên java của JDK này thắng /usr/bin/java cũ, và javac lần đầu có mặt. Lệnh export chỉ có tác dụng trong terminal đó, mở terminal mới thì mất (muốn giữ lâu hơn bạn ghi vào file cấu hình của shell [Suy luận, mình không đổi file cấu hình nào trên máy này]). Cách kiểm tra chắc chắn nhất là gọi thẳng bằng đường dẫn đầy đủ: "$JAVA_HOME/bin/javac" -version cũng in javac 17.0.21 [Đã chạy].

Nhờ có javac thật, mình chạy lại các thí nghiệm của bài (biên dịch, lỗi thiếu dấu chấm phẩy, tên file sai, java Hello.class) bằng nó và đối chiếu với đường vòng java -m jdk.compiler/com.sun.tools.javac.Main của runtime 17.0.15: ba thông báo lỗi javac mình so sánh từng ký tự đều giống nhau [Đã chạy]. Vì vậy các kết quả javac trong bài là kết quả bạn sẽ thấy khi gõ javac trên máy có JDK 17.

Hình: hai lệnh kiểm tra và ba kết quả có thể gặp

Cách đọc hình: hàng trên: java -version dẫn tới ô lục "Prints a Java version". Hàng dưới: javac -version có hai mũi tên, mũi tên liền (works) tới ô lục "Prints javac 17 or newer" và mũi tên nét đứt (fails) tới ô đỏ nhạt "command not found". Ô cuối kèm lời giải thích: chỉ có runtime hoặc PATH thiếu thư mục bin của JDK.

Nếu javac -version báo không tìm thấy lệnh, bạn cần cài JDK hoặc sửa PATH. Dưới đây là các đường cài chính thống. Chỉ dòng Linux là mình đã chạy thật; các dòng còn lại mình không có máy để chạy, nên nhãn nói rõ mức chắc chắn:

Hệ điều hànhCách càiTrạng thái kiểm chứng
Linux (tải thủ công)Vào Eclipse Temurin releases, chọn JDK 17, Linux, x64, bản .tar.gz; tải về, kiểm sha256sum, giải nén bằng tar -xzf, rồi đặt JAVA_HOME và PATH như ở trên[Đã chạy, Temurin 17.0.21 trên Ubuntu 20.04 chạy được javac -version]
Ubuntu và Debian (kho gói Adoptium)Trang hướng dẫn Linux của Adoptium nêu kho apt riêng và quy ước tên gói temurin-<phiên bản>-jdk, ví dụ temurin-17-jdk[Chưa chạy: mình không cài gói và không thêm kho vào máy này. Tên gói theo quy ước ghi trên trang]
Ubuntu và Debian (kho gói của hệ điều hành)Thường là gói tên openjdk-17-jdk[Chưa xác nhận: apt-cache show openjdk-17-jdk trên máy mình báo No packages found, vì danh sách gói của máy này không liệt kê nó. Hãy chạy apt search openjdk-17 trên máy bạn để xem tên gói thật]
WindowsTải bộ cài .msi của Temurin 17 theo hướng dẫn Windows của Adoptium. Trang này nói bộ cài có hai tính năng chọn được: thêm vào PATH và cập nhật JAVA_HOME. Có thể dùng thêm Microsoft Build of OpenJDK hoặc bản JDK của Oracle[Có thể khác nhau: mình đọc trang hướng dẫn, chưa chạy bộ cài trên Windows. Màn hình của bộ cài có thể đổi theo phiên bản]
Windows (trình quản lý gói)Công cụ winget có nhóm gói Temurin 17 JDK với mã EclipseAdoptium.Temurin.17.JDK[Suy luận: mình chỉ thấy thư mục manifest EclipseAdoptium/Temurin/17/JDK trong kho gói winget, chưa chạy winget. Hãy dùng winget search Temurin để xem mã đúng]
macOSTải bộ cài .pkg của Temurin 17 theo hướng dẫn macOS của Adoptium. Hoặc dùng Homebrew với brew install --cask temurin@17 (trang temurin@17 trên Homebrew có tồn tại)[Có thể khác nhau: chưa chạy trên macOS. Mình chỉ xác nhận bộ cài .pkg và trang cask Homebrew tồn tại]

Sau khi cài trên Windows hoặc macOS, mở terminal mới rồi lặp lại ba việc kiểm tra: java -version, javac -version, và xem JAVA_HOME. Nếu java -version ra Java 17 mà javac -version báo không tìm thấy, nghĩa là PATH đang trỏ tới một bản cài khác chỉ có runtime. Trên macOS lệnh /usr/libexec/java_home -V liệt kê các JDK đã cài [Suy luận: mình không có máy macOS để chạy]. Nếu JAVA_HOME rỗng thì java Hello.java vẫn chạy, nhưng công cụ nào đòi JAVA_HOME sẽ báo lỗi.

Khái niệm 2: javac nhận tên file, java nhận tên lớp

Đây là khái niệm quan trọng nhất của bài. javac là lệnh biên dịch, nhận tên file có đuôi .java và tạo ra file .class trong thư mục. java là lệnh khởi chạy, nhận tên lớp không có đuôi, tìm file .class của lớp đó rồi gọi hàm main. Từ Java 11, lệnh java còn có chế độ thứ ba: nhận thẳng file nguồn Hello.java, tự biên dịch trong bộ nhớ rồi chạy, không để lại file .class.

Hình: ba lệnh, mỗi lệnh nhận một loại đầu vào khác nhau

Cách đọc hình: ba hàng, mỗi hàng đọc từ trái sang phải. Hàng một: javac Hello.java nhận tên file có đuôi .java và để lại Hello.class. Hàng hai: java Hello nhận tên lớp không có đuôi và đọc Hello.class rồi gọi main. Hàng ba: java Hello.java nhận file nguồn, biên dịch trong bộ nhớ và không để lại file .class. Dòng ghi chú bên dưới giải thích vì sao java Hello.class sai.

Tên lớp trong Java có thể có dấu chấm để ngăn cách tên gói. Vì vậy java Hello.class bị đọc thành "lớp tên class nằm trong gói tên Hello", và không có lớp nào như vậy. Thông báo thật còn có dòng thứ hai, Caused by: java.lang.ClassNotFoundException: Hello.class, cho thấy JVM đã đi tìm một lớp tên đúng là Hello.class [Đã chạy]. Đó là toàn bộ bí ẩn của tình huống mở đầu.

Concept Check 1. Thư mục có Hello.java và Hello.class vừa biên dịch. Lệnh nào chạy đúng chương trình từ file đã biên dịch?

  • A. java Hello.class, vì lệnh java nhận tên file.
  • B. javac Hello.class, vì javac dùng để chạy file đã dịch.
  • C. javac Hello, vì javac cũng nhận tên lớp không đuôi.
  • D. java Hello, vì lệnh java nhận tên lớp, không có đuôi.

Đáp án: D. A là lỗi ở tình huống mở đầu. B sai vì javac nhận file nguồn, và mình đã chạy thật: javac Hello.class báo error: invalid flag: Hello.class [Đã chạy, JDK 17.0.15]. C cũng sai vì javac Hello báo error: Class names, 'Hello', are only accepted if annotation processing is explicitly requested [Đã chạy].

Khái niệm 3: chữ ký của main

Lệnh java tìm đúng một hàm: public static void main(String[] args). Mỗi từ có một vai trò: public để bộ khởi chạy gọi được từ ngoài, static để gọi mà không cần tạo đối tượng, void vì hàm không trả giá trị, main là tên bộ khởi chạy tìm, và String[] args nhận các từ bạn gõ sau tên lớp.

Hình: năm phần của chữ ký main và vai trò của từng phần

Cách đọc hình: năm ô nằm ngang theo thứ tự trong chữ ký: public, static, void, main, String[] args. Mỗi ô có một dòng giải thích ngắn bên dưới tên. Dòng ghi chú cuối cho biết chỉ cần đổi một từ thì chương trình vẫn biên dịch được nhưng bộ khởi chạy từ chối khởi động nó.

Phần args là cầu nối giữa dòng lệnh và chương trình. Với java Hello Lan, mảng args chứa đúng một từ là Lan. Với java Hello không kèm gì, mảng rỗng và args.length bằng 0, đó là lý do Hello có nhánh if (args.length > 0).

Khái niệm 4: mã thoát cho biết chương trình thành công hay thất bại

Mọi tiến trình kết thúc với một số nguyên gọi là mã thoát (exit code). Quy ước chung: 0 là thành công, khác 0 là lỗi. Trong shell, bạn xem mã của lệnh vừa chạy bằng echo $? (trên Windows là echo %ERRORLEVEL%). Mình đã chạy bốn trường hợp [Đã chạy, JDK 17.0.15]:

Trường hợpMã thoátĐiều xảy ra
java Hello.java Lan0In Hello, Lan!, main kết thúc bình thường
java Welcome.java không kèm tên2Chương trình tự gọi System.exit(2) sau khi in cách dùng
java Boom.java1In Exception in thread "main" java.lang.RuntimeException: boom, ngoại lệ không ai bắt
java Helo1Error: Could not find or load main class Helo, main chưa hề chạy

Cách đọc hình: hàng trên có bốn ô: "main ends normally" (mã 0), "System.exit(2)" (mã 2 do bạn chọn), "Uncaught exception" (mã 1 kèm dấu vết lỗi) và "Launch error" (mã 1, main chưa chạy). Hàng dưới có hai ô nối nhau: lệnh echo $? trong shell và "Scripts and CI" dừng lại khi mã khác 0.

Script và hệ thống CI dựa vào con số này để biết bước vừa rồi có thành công không, nên chương trình tử tế dùng mã khác 0 khi có lỗi.

Ví dụ đầu tiên (Worked Example) và "Xem từng bước"

Đây là chương trình đầu tiên của dự án: chiếc máy chào nhận một cái tên từ dòng lệnh, và dùng tên Java khi bạn không gõ gì.

Hello.java

java
public class Hello {
    // Ghép lời chào từ một cái tên
    static String greeting(String name) {
        return "Hello, " + name + "!";
    }

    // Lệnh java tìm đúng chữ ký này để bắt đầu chương trình
    public static void main(String[] args) {
        String name = "Java";
        // args là các từ gõ sau tên lớp, ví dụ: java Hello Lan
        if (args.length > 0) {
            name = args[0];
        }
        System.out.println(greeting(name));
    }
}

Predict: hãy dừng lại và đoán: java Hello.java in ra dòng nào, và java Hello.java Lan in ra dòng nào? Đoán xong rồi hãy đọc tiếp.

Kết quả mong đợi [Đã chạy, JDK 17.0.15]:

text
Hello, Java!
text
Hello, Lan!

Cách thứ hai là biên dịch rồi chạy. Mình đã chạy hai lệnh này trong một thư mục trống chỉ chứa Hello.java, và mình chạy javac bằng cả hai cách đã nói ở khái niệm 1, javac thật của Temurin 17.0.21 và đường vòng của runtime 17.0.15, với kết quả giống nhau [Đã chạy]. Lệnh đầu không in gì khi thành công, và thư mục có thêm file Hello.class:

text
$ javac Hello.java
$ java Hello Lan
Hello, Lan!

Đọc code từng dòng:

Dòng codeMáy làm gìVì sao viết như vậy
public class Hello {Khai báo lớp tên HelloTên lớp phải trùng tên file Hello.java, nếu không javac báo lỗi
static String greeting(String name)Khai báo hàm nhận một tên và trả về một chuỗiTách việc ghép lời chào ra khỏi main để dễ đọc và dễ thử lại
return "Hello, " + name + "!";Nối ba đoạn chữ lại thành một chuỗi rồi trả vềDấu + giữa các chuỗi là phép nối
public static void main(String[] args)Khai báo hàm mà bộ khởi chạy tìmĐúng từng từ của chữ ký ở khái niệm 3
String name = "Java";Tạo biến name chứa chữ JavaGiá trị mặc định khi người dùng không gõ tên
if (args.length > 0) { name = args[0]; }Nếu có ít nhất một từ sau tên lớp thì lấy từ đầu tiên làm tênargs[0] là từ đầu tiên, đếm từ 0
System.out.println(greeting(name));Gọi greeting, in kết quả ra màn hình và xuống dòngprintln thêm ký tự xuống dòng cuối câu

Bây giờ xem từng bước:

Đ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):

Dòng chạyargsnameIn raGhi chú
bắt đầu java Hello Lanmột từ: Lanchưa cóchưa cóBộ khởi chạy tìm lớp Hello
String name = "Java";LanJavachưa cóGiá trị mặc định
if (args.length > 0)LanJavachưa cóargs.length bằng 1 nên điều kiện đúng
name = args[0];LanLanchưa cóTên mặc định bị thay
println(greeting(name))LanLanHello, Lan!main kết thúc, mã thoát 0

Bạn vừa làm được: chạy một chương trình Java bằng hai cách và nói đúng file nào xuất hiện sau mỗi lệnh.

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. javac Hello.java in ra Hello.java:4: error: ';' expected. Lỗi này thuộc trạm nào, và có file Hello.class mới không?

  • A. Trạm biên dịch, và không có Hello.class mới.
  • B. Trạm khởi chạy, và Hello.class mới vẫn được tạo.
  • C. Trạm thực thi, vì lỗi xảy ra khi chương trình chạy.
  • D. Trạm biên dịch, nhưng javac vẫn tạo Hello.class để bạn gỡ lỗi.

Đáp án: A. Lỗi cú pháp do javac phát hiện, và mình đã kiểm tra thư mục sau lần biên dịch lỗi: không có file .class nào được tạo [Đã chạy]. B và C nhầm trạm: javac đã dừng thì chưa có gì để khởi chạy hay thực thi. D nghe hợp lý nhưng sai: javac không sinh file khi còn lỗi.

Concept Check 3. Chương trình in một dòng rồi gọi System.exit(2). Mã thoát mà shell thấy là bao nhiêu?

  • A. 0, vì dòng in đã chạy thành công.
  • B. 1, vì mọi lần thoát bằng System.exit đều là lỗi.
  • C. 2, đúng con số bạn đưa vào System.exit.
  • D. Không có mã nào, vì System.exit chỉ đóng cửa sổ.

Đáp án: C. System.exit(n) kết thúc tiến trình với mã đúng bằng n, và bảng mã thoát ở khái niệm 4 có đúng trường hợp này với mã 2. A nhầm "in thành công" với "kết thúc thành công". B nhầm mã 1 của ngoại lệ không bắt với System.exit. D sai vì tiến trình nào cũng có mã thoát.

Concept Check 4. Bạn xoá chữ public khỏi dòng main, biên dịch rồi gõ java Hello. Điều gì xảy ra?

  • A. javac báo lỗi vì main thiếu public.
  • B. javac biên dịch xong bình thường, nhưng java báo Main method not found.
  • C. Chương trình chạy bình thường vì public không quan trọng.
  • D. Chương trình chạy rồi ném NullPointerException.

Đáp án: B. Mình đã chạy đúng thí nghiệm này: javac không báo gì, còn java Hello in Error: Main method not found in class Hello, please define the main method as: public static void main(String[] args) [Đã chạy, JDK 17.0.15]. A sai vì thiếu public là hợp lệ với trình biên dịch. C sai vì bộ khởi chạy tìm đúng chữ ký đầy đủ. D sai vì lỗi xảy ra trước khi main chạy.

Làm cùng mình (Guided Practice)

Bạn sẽ dựng dần class Welcome của quán Cà Phê Gió qua năm bước nhỏ. Gõ code vào file Welcome.java rồi chạy bằng java Welcome.java Lan. Hỗ trợ giảm dần: bước 1 gần như làm sẵn, bước 5 chỉ còn đề.

Bước 1: in lời chào cố định.

java
public class Welcome {
    public static void main(String[] args) {
        // TODO 1: in dòng "Welcome to Cafe Gio"
    }
}

Chạy java Welcome.java xong bạn sẽ thấy: Welcome to Cafe Gio.

Bước 2: chào đúng tên khách. Thay thân main bằng:

java
        // TODO 2: in "Welcome to Cafe Gio, " rồi tên khách lấy từ args[0], rồi dấu chấm than

Chạy java Welcome.java Lan xong bạn sẽ thấy: Welcome to Cafe Gio, Lan!. Gợi ý: args[0] là từ đầu tiên sau tên file.

Bước 3: chưa có tên thì báo cách dùng. Thêm lên đầu thân main, trước dòng in:

java
        // TODO 3: nếu args.length bằng 0 thì in "usage: java Welcome.java <name>" rồi gọi System.exit(2)

Chạy java Welcome.java không kèm tên xong bạn sẽ thấy: usage: java Welcome.java <name>, và echo $? cho 2.

Vẽ ra giấy: kẻ ba trạm viết, biên dịch, chạy, rồi đánh dấu trạm nào chạy khi bạn gõ java Welcome.java không kèm tên. Đối chiếu: cả ba trạm đều qua, vì lệnh java Welcome.java vừa dịch vừa chạy. Dòng System.exit(2) chạy ở trạm thực thi, nên mã 2 là do chính chương trình chọn, khác với mã 1 của lỗi khởi chạy.

Bước 4: tách thành hàm. Bạn tự quyết cách viết.

java
    static String welcome(String name) {
        // TODO 4: trả về "Welcome to Cafe Gio, " + name + "!"
        return "";
    }

Rồi đổi dòng in trong main để gọi welcome(args[0]). Chạy xong bạn sẽ thấy cùng kết quả bước 2. Gợi ý: tham khảo hàm greeting trong Hello.

Bước 5: chạy hai cách. Không có code mới. Chạy java Welcome.java Lan, rồi biên dịch bằng javac Welcome.java và chạy java Welcome Lan. Chạy xong bạn sẽ thấy hai lần in giống nhau, và chỉ lần biên dịch trước mới để lại file Welcome.class. Lời giải đầy đủ nằm ở cuối bài.

Bạn vừa làm được: tự viết một chương trình nhận tham số, báo cách dùng và trả mã thoát riêng. Toàn bộ mã nguồn của bài cũng nằm trong thư mục 01-nhap-mon/03-hello-world 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 ở ngày đầu. Mỗi dòng đã được chạy thật với Hello.java của bài [Đã chạy, JDK 17.0.15; chữ của thông báo có thể khác ở phiên bản Java khác].

Code lỗiThông báo lỗiNguyên nhân bằng lời thườngCách sửa
Xoá dấu chấm phẩy ở dòng return "Hello, " + name + "!";Hello.java:4: error: ';' expectedCâu lệnh phải kết thúc bằng dấu chấm phẩy, và javac chỉ ra dòng chứa câu lệnhThêm ; ở cuối dòng, rồi biên dịch lại
Lưu file là Hi.java nhưng lớp vẫn là public class HelloHi.java:1: error: class Hello is public, should be declared in a file named Hello.javaLớp public phải nằm trong file trùng tênĐổi tên file thành Hello.java
java Hello.classError: Could not find or load main class Hello.classjava nhận tên lớp, và nó đọc Hello.class thành một lớp không tồn tạijava Hello, không có đuôi
Xoá chữ public khỏi dòng main, rồi java HelloError: Main method not found in class HelloBộ khởi chạy tìm đúng chữ ký public static void main(String[] args)Viết lại đúng chữ ký

Có một điểm lạ ở lỗi đầu tiên mà mình đã thử: sau khi biên dịch lỗi, nếu thư mục còn Hello.class cũ thì java Hello Lan vẫn chạy bản cũ và in Hello, Lan! [Đã chạy]. Vì vậy hãy đọc thông báo của javac trước khi chạy java. Hình dưới đây gom mô hình sai và mô hình đúng về lỗi thứ ba.

Hình: mô hình sai và mô hình đúng về thứ mà lệnh java nhận

Cách đọc hình: hàng trên màu đỏ nhạt là mô hình sai: từ ô đầu "Hello.class exists", mũi tên nét đứt "expected" tới "java takes the file name Hello.class" rồi tới dự đoán sai "java Hello.class runs the program". Hàng dưới màu lục là mô hình đúng: mũi tên "reality" tới "java takes a class name: Hello" rồi tới dự đoán đúng "java Hello.class says: could not find main class". Hai hàng cùng xuất phát từ một thư mục có sẵn file Hello.class.

Bạn vừa làm được: đọc bốn thông báo lỗi đầu tiên và nói được thông báo nào thuộc trạm nào.

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 trạng thái. Dữ liệu vào: thư mục ban đầu chỉ có Hello.java. Chạy lần lượt bốn lệnh: java Hello.java, rồi javac Hello.java, rồi java Hello Lan, rồi java Hello.class. Kẻ bảng bốn hàng, mỗi hàng ghi: các file có trong thư mục sau lệnh, dòng in ra, mã thoát. Kết quả mong đợi: một bảng bốn hàng. Tiêu chí nghiệm thu: bạn chỉ ra đúng lệnh nào làm xuất hiện Hello.class và lệnh nào lỗi. Gợi ý: lệnh đầu tiên dịch trong bộ nhớ nên không để lại file.

Bài 2. Xếp lỗi vào trạm. Dữ liệu vào: bốn thông báo: (a) error: ';' expected, (b) Error: Could not find or load main class Helo, (c) Exception in thread "main" java.lang.RuntimeException: boom, (d) Error: Main method not found in class Hello. Kết quả mong đợi: mỗi thông báo gắn với một trạm trong ba trạm. Tiêu chí nghiệm thu: bạn nói được trong mỗi trường hợp hàm main đã chạy chưa. Gợi ý: hãy hỏi "thông báo này do lệnh nào in ra".

Bài 3. Tên có dấu cách. Dữ liệu vào: Hello.java. Chạy java Hello.java "Linh Tran" rồi java Hello.java Linh Tran. Kết quả mong đợi: hai dòng khác nhau. Tiêu chí nghiệm thu: bạn giải thích được vì sao hai lệnh cho hai kết quả bằng khái niệm args. Gợi ý: shell tách các từ ngăn cách bởi dấu cách thành các phần tử riêng của args, trừ khi có dấu ngoặc kép.

Mức Vận dụng

Bài 4. Class Door cho quán Cà Phê Gió. Viết class Door nhận hai từ trong dòng lệnh: tên đồ uống và số bàn. Nếu có đủ hai từ thì in Table <số bàn>: <đồ uống>. Nếu thiếu thì in usage: java Door.java <drink> <table> rồi thoát với mã 2.

Kết quả mong đợi:

text
$ java Door.java Latte 5
Table 5: Latte
$ java Door.java Latte
usage: java Door.java <drink> <table>
$ echo $?
2

Tiêu chí nghiệm thu: ba lệnh khớp từng ký tự, và mã thoát khi thiếu từ là 2. Gợi ý: dùng args.length và System.exit, giống bước 3 của phần Làm cùng mình.

Mức Thử thách (tuỳ chọn)

Bài 5. Đo hai cách chạy. Cần máy có lệnh javac. Chạy mỗi cách năm lần và đo thời gian bằng lệnh time: cách một là java Hello.java Lan, cách hai là java Hello Lan sau khi đã javac Hello.java. Kết quả mong đợi: hai nhóm năm con số của riêng máy bạn [Có thể khác nhau]. Tiêu chí nghiệm thu: bạn xếp được cách nào nhanh hơn và giải thích bằng trạm nào phải làm lại ở mỗi lần chạy. Gợi ý: cách một dịch lại file nguồn ở mọi lần chạy.

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). Ba việc nào kiểm tra máy đã có JDK chưa, và kết quả nào cho biết máy chỉ có runtime? Dòng OpenJDK 64-Bit Server VM (..., mixed mode, sharing) có cho biết máy có javac không?

Câu 2 (dự đoán kết quả code, mục tiêu 2). Với Hello.java của bài, hai lệnh java Hello.java "Linh Tran" và java Hello.java Linh Tran in ra gì?

Câu 3 (dự đoán trạng thái sau nhiều bước, mục tiêu 2). Bạn biên dịch Hello.java thành công. Sau đó bạn xoá một dấu chấm phẩy, chạy javac Hello.java (báo lỗi), rồi chạy java Hello Lan. Điều gì xảy ra, và mã thoát là bao nhiêu?

Câu 4 (tìm lỗi, mục tiêu 3). Một bạn gõ java Hello.class và nhận lỗi không tìm thấy lớp chính. Chỉ ra lỗi, nói vì sao lệnh đó sai và viết lệnh đúng.

Câu 5 (giải thích cho bạn chưa học, mục tiêu 4). Bạn của bạn chưa biết lập trình hỏi: "Mã thoát là gì mà script cứ bắt nhìn nó?" Hãy giải thích bằng hình ảnh đèn tín hiệu ở cuối mỗi trạm.

Điều cần nhớ

  • JDK sẵn sàng khi java -version và javac -version đều in ra phiên bản và JAVA_HOME (nếu công cụ cần) trỏ đúng thư mục JDK; nếu javac báo không tìm thấy thì cần cài JDK hoặc sửa PATH (mục tiêu 1).
  • Có hai cách chạy: java Hello.java biên dịch trong bộ nhớ, còn javac Hello.java rồi java Hello để lại file Hello.class (mục tiêu 2).
  • javac nhận tên file có đuôi .java, java nhận tên lớp không đuôi, nên java Hello.class bị lỗi (mục tiêu 3).
  • Mỗi lỗi thuộc một trạm, và mã thoát 0 là thành công còn khác 0 là lỗi (mục tiêu 4).

Cheat sheet một trang.

Việc cần làmLệnhGhi chú
Kiểm tra JDKjava -version và javac -versionMở terminal mới sau khi cài
Xem thư mục JDKecho $JAVA_HOMEWindows: echo %JAVA_HOME%. Rỗng thì java vẫn chạy
Biên dịchjavac Hello.javaNhận tên file, tạo Hello.class
Chạy lớp đã biên dịchjava HelloNhận tên lớp, không có đuôi
Chạy thẳng file nguồnjava Hello.javaDịch trong bộ nhớ, chậm khi lặp lại
Truyền tham sốjava Hello Lanargs[0] bằng Lan
Xem mã thoátecho $?Windows dùng echo %ERRORLEVEL%

Cách đọc hình: ba hàng, mỗi hàng một lệnh ở bên trái và điều cần kiểm tra khi lệnh đó lỗi ở bên phải: javac Hello.java lỗi thì đọc số dòng và biết là chưa có .class mới, java Hello báo không thấy lớp chính thì kiểm tra tên và thư mục, java Hello.java chạy chậm khi lặp lại thì biên dịch một lần bằng javac.

Câu hỏi để dành cho ngày mai: bạn vừa thấy một dấu chấm phẩy thiếu làm javac báo lỗi. Vậy một dấu chấm phẩy thừa, đặt sai chỗ, có làm máy báo lỗi không? Hãy đoán 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.

Cơ chế bên trong. Khi bạn gõ java Hello, bộ khởi chạy tìm lớp Hello trong classpath, danh sách thư mục chứa file .class. Theo tài liệu của lệnh java, nếu bạn không đặt classpath thì mặc định là thư mục hiện tại, nên bạn phải đứng đúng thư mục. Bạn có thể chỉ thư mục khác bằng -cp: mình chạy javac -d out Hello.java rồi java -cp out Hello Lan và nhận Hello, Lan! [Đã chạy]. Chế độ chạy file nguồn có từ Java 11 (xem JEP 330 ở phần đọc thêm).

So với lựa chọn khác. Trên máy mình, ba lần đo mỗi cách cho java Hello Lan (đã biên dịch) mất 0,68, 1,01 và 1,29 giây, còn java Hello.java Lan mất 7,55, 7,84 và 10,84 giây [Đã chạy, JDK 17.0.15; Có thể khác nhau: máy lúc đó rất bận, load average khoảng 28 trên máy 4 nhân, nên cả hai nhóm đều chậm hơn bình thường và chỉ nên tin vào chiều chênh lệch]. Chênh lệch này đến từ việc cách thứ hai phải dịch lại file nguồn ở mỗi lần chạy. Vì vậy cách chạy thẳng file nguồn hợp để thử nhanh, còn chương trình chạy nhiều lần thì biên dịch một lần.

Hiểu lầm nâng cao. Nhiều người nghĩ "javac báo lỗi thì chạy java cũng sẽ lỗi". Không đúng: nếu bản Hello.class cũ còn trong thư mục thì java Hello vẫn chạy bản cũ và bạn tưởng sửa mã không có tác dụng. Đó là chỗ ẩn dụ dây chuyền không còn đúng ở đầu bài.

Giới hạn của bài. Bài này chưa nói về package và cấu trúc thư mục theo gói, về cách đặt CLASSPATH chi tiết, về IDE hay Maven, và chưa nói cách ghi JAVA_HOME hay PATH vĩnh viễn vào file cấu hình của hệ điều hành. Chỉ phần Linux của bảng cài đặt được chạy thật; Windows và macOS mình mới đối chiếu với trang hướng dẫn chính thức, chưa chạy bộ cài.

Đọc thêm (tài liệu chính thống):

Bài tiếp theo

Bài sau đi sâu vào những gì bạn vừa gõ: các quy tắc cú pháp cơ bản như dấu chấm phẩy và khối lệnh, tên hợp lệ khác tên đúng quy ước, và lý do một dấu chấm phẩy thừa có thể làm chương trình chạy sai mà không có thông báo lỗi nào.

Mini-dự án nối tiếp: ở bài sau, chiếc máy chào của quán sẽ thêm một cổng soát vé theo tuổi. Hãy giữ lại Welcome.java bạn vừa viết, vì mình sẽ dùng lại cách nhận tham số.

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ả ở từng bước [Đã chạy, JDK 17.0.15]:

java
public static void main(String[] args) {
    // Chưa có tên khách thì in cách dùng rồi thoát với mã 2
    if (args.length == 0) {
        System.out.println("usage: java Welcome.java <name>");
        System.exit(2);
    }
    System.out.println(welcome(args[0]));
}

static String welcome(String name) {
    return "Welcome to Cafe Gio, " + name + "!";
}
Dòng codeMáy làm gìLý do chọn cách này
if (args.length == 0) { ... }Kiểm tra khách có gõ tên khôngargs.length bằng 0 nghĩa là không có từ nào sau tên file
System.exit(2);Kết thúc tiến trình ngay với mã 2Mã khác 0 báo cho script biết là thiếu tham số
System.out.println(welcome(args[0]));Gọi welcome với từ đầu tiên rồi in kết quảĐã chặn trường hợp rỗng ở trên nên args[0] luôn tồn tại
return "Welcome to Cafe Gio, " + name + "!";Nối ba đoạn chữ thành một chuỗiGiống greeting trong Hello, chỉ đổi câu chào

Lời giải Bài 1. [Suy luận, khớp với kết quả chạy thật]

LệnhFile trong thư mụcIn raMã thoát
java Hello.javaHello.javaHello, Java!0
javac Hello.javaHello.java, Hello.classkhông in gì0
java Hello LanHello.java, Hello.classHello, Lan!0
java Hello.classHello.java, Hello.classError: Could not find or load main class Hello.class1

Lời giải Bài 2. (a) trạm biên dịch, main chưa chạy vì chưa có file .class. (b) trạm khởi chạy, main chưa chạy. (c) trạm thực thi, main đã chạy và dòng ném lỗi đã được thực hiện. (d) trạm khởi chạy, main chưa chạy vì bộ khởi chạy không tìm thấy hàm có đúng chữ ký. [Suy luận]

Lời giải Bài 3. [Đã chạy] java Hello.java "Linh Tran" in Hello, Linh Tran!, còn java Hello.java Linh Tran in Hello, Linh!. Dấu ngoặc kép giữ hai chữ thành một từ nên args[0] là Linh Tran. Khi không có ngoặc kép, shell tách thành hai từ Linh và Tran, và Hello chỉ dùng args[0].

Lời giải Bài 4. [Đã chạy, ba lệnh khớp kết quả mong đợi, mã thoát 2]

java
public static void main(String[] args) {
    // Cần đủ hai từ: đồ uống và số bàn
    if (args.length < 2) {
        System.out.println("usage: java Door.java <drink> <table>");
        System.exit(2);
    }
    System.out.println("Table " + args[1] + ": " + args[0]);
}
Dòng codeMáy làm gìLý do chọn cách này
if (args.length < 2)Kiểm tra có ít hơn hai từ khôngCần cả args[0] và args[1], thiếu một cái là báo cách dùng
System.exit(2);Thoát với mã 2Cùng quy ước với Welcome: thiếu tham số là mã 2
"Table " + args[1] + ": " + args[0]Ghép số bàn trước, đồ uống sauThứ tự in khác thứ tự gõ, vì đề bài yêu cầu Table <số bàn>: <đồ uống>

Gợi ý lời giải Bài 5. Khuôn mẫu đo: time java Hello.java Lan lặp năm lần, rồi javac Hello.java một lần và time java Hello Lan lặp năm lần. Trên máy mình cách thứ hai nhanh hơn hẳn [Có thể khác nhau]. Giải thích: cách thứ nhất chạy lại trạm biên dịch (trong bộ nhớ) ở mọi lần, cách thứ hai chỉ chạy trạm khởi chạy.

Đáp án Tự kiểm tra.

  1. java -version, javac -version và xem JAVA_HOME. Máy chỉ có runtime khi java -version in được phiên bản còn javac -version báo không tìm thấy lệnh (JAVA_HOME rỗng cũng chưa chứng minh gì, vì java vẫn chạy khi biến này rỗng). Dòng về JVM chỉ nói tên JVM và chế độ chạy, không nói gì về javac. Kiểm tra mục tiêu 1.
  2. Hello, Linh Tran! và Hello, Linh! [Đã chạy]. Dấu ngoặc kép gộp hai chữ thành một từ của args. Kiểm tra mục tiêu 2.
  3. javac dừng ở lỗi và không tạo file mới, nhưng Hello.class cũ vẫn còn, nên java Hello Lan chạy bản cũ và in Hello, Lan! với mã thoát 0 [Đã chạy]. Kiểm tra mục tiêu 2.
  4. Lệnh java nhận tên lớp, và Hello.class bị đọc thành lớp class trong gói Hello, không tồn tại. Lệnh đúng là java Hello. Kiểm tra mục tiêu 3.
  5. Mã thoát như đèn tín hiệu ở cuối mỗi trạm: 0 là đèn xanh, cho phép trạm sau làm tiếp, còn số khác 0 là đèn đỏ kèm lý do. Script nhìn đèn này để quyết định dừng hay chạy tiếp. Kiểm tra mục tiêu 4.