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 -versionvà biếnJAVA_HOME, và đọc đúng ý nghĩa từng dòng kết quả. - Viết, biên dịch và chạy
Hellobằ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
javacnhận tên file cònjavanhận tên lớp, nênjava Hello.classbị 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.
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]:
$ 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 ra | Nghĩa là gì | Bạn rút ra điều gì |
|---|---|---|
openjdk version "17.0.15" 2025-04-15 | Số 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à Ubuntu | Cho 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 found | Shell tìm file tên javac trong mọi thư mục của PATH và không thấy file nào | Má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_HOME | Biến JAVA_HOME chưa được đặt | Khô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ết | Chỉ 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]:
$ 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.
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ành | Cách cài | Trạ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] |
| Windows | Tả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] |
| macOS | Tả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.
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ệnhjavanhận tên file. - B.
javac Hello.class, vìjavacdùng để chạy file đã dịch. - C.
javac Hello, vìjavaccũng nhận tên lớp không đuôi. - D.
java Hello, vì lệnhjavanhậ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.
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ợp | Mã thoát | Điều xảy ra |
|---|---|---|
java Hello.java Lan | 0 | In Hello, Lan!, main kết thúc bình thường |
java Welcome.java không kèm tên | 2 | Chương trình tự gọi System.exit(2) sau khi in cách dùng |
java Boom.java | 1 | In Exception in thread "main" java.lang.RuntimeException: boom, ngoại lệ không ai bắt |
java Helo | 1 | Error: 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
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]:
Hello, Java!
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:
$ javac Hello.java
$ java Hello Lan
Hello, Lan!
Đọc code từng dòng:
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
public class Hello { | Khai báo lớp tên Hello | Tê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ỗi | Tá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ữ Java | Giá 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ên | args[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òng | println 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ạy | args | name | In ra | Ghi chú |
|---|---|---|---|---|
bắt đầu java Hello Lan | một từ: Lan | chưa có | chưa có | Bộ khởi chạy tìm lớp Hello |
String name = "Java"; | Lan | Java | chưa có | Giá trị mặc định |
if (args.length > 0) | Lan | Java | chưa có | args.length bằng 1 nên điều kiện đúng |
name = args[0]; | Lan | Lan | chưa có | Tên mặc định bị thay |
println(greeting(name)) | Lan | Lan | Hello, 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.classmới. - B. Trạm khởi chạy, và
Hello.classmớ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
javacvẫn tạoHello.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.exitchỉ đó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.
javacbáo lỗi vìmainthiếupublic. - B.
javacbiên dịch xong bình thường, nhưngjavabáoMain method not found. - C. Chương trình chạy bình thường vì
publickhô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.
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:
// 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:
// 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.
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ỗi | Thông báo lỗi | Nguyên nhân bằng lời thường | Cách sửa |
|---|---|---|---|
Xoá dấu chấm phẩy ở dòng return "Hello, " + name + "!"; | Hello.java:4: error: ';' expected | Câ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ệnh | Thê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 Hello | Hi.java:1: error: class Hello is public, should be declared in a file named Hello.java | Lớp public phải nằm trong file trùng tên | Đổi tên file thành Hello.java |
java Hello.class | Error: Could not find or load main class Hello.class | java nhận tên lớp, và nó đọc Hello.class thành một lớp không tồn tại | java Hello, không có đuôi |
Xoá chữ public khỏi dòng main, rồi java Hello | Error: Main method not found in class Hello | Bộ 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.
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:
$ 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 -versionvà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ếujavacbáo không tìm thấy thì cần cài JDK hoặc sửaPATH(mục tiêu 1). - Có hai cách chạy:
java Hello.javabiên dịch trong bộ nhớ, cònjavac Hello.javarồijava Hellođể lại fileHello.class(mục tiêu 2). javacnhận tên file có đuôi.java,javanhận tên lớp không đuôi, nênjava Hello.classbị 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àm | Lệnh | Ghi chú |
|---|---|---|
| Kiểm tra JDK | java -version và javac -version | Mở terminal mới sau khi cài |
| Xem thư mục JDK | echo $JAVA_HOME | Windows: echo %JAVA_HOME%. Rỗng thì java vẫn chạy |
| Biên dịch | javac Hello.java | Nhận tên file, tạo Hello.class |
| Chạy lớp đã biên dịch | java Hello | Nhận tên lớp, không có đuôi |
| Chạy thẳng file nguồn | java Hello.java | Dịch trong bộ nhớ, chậm khi lặp lại |
| Truyền tham số | java Hello Lan | args[0] bằng Lan |
| Xem mã thoát | echo $? | 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):
- JEP 330: Launch Single-File Source-Code Programs
- Tài liệu lệnh java, JDK 17
- Tài liệu lệnh javac, JDK 17
- Hướng dẫn cài đặt JDK 17 của Oracle
- Eclipse Temurin: hướng dẫn cài đặt
- Microsoft Build of OpenJDK: hướng dẫn cài đặt
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]:
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 code | Má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ông | args.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ã 2 | Mã 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ỗi | Giố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ệnh | File trong thư mục | In ra | Mã thoát |
|---|---|---|---|
java Hello.java | Hello.java | Hello, Java! | 0 |
javac Hello.java | Hello.java, Hello.class | không in gì | 0 |
java Hello Lan | Hello.java, Hello.class | Hello, Lan! | 0 |
java Hello.class | Hello.java, Hello.class | Error: Could not find or load main class Hello.class | 1 |
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]
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 code | Má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ông | Cầ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ã 2 | Cù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 sau | Thứ 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.
java -version,javac -versionvà xemJAVA_HOME. Máy chỉ có runtime khijava -versionin được phiên bản cònjavac -versionbáo không tìm thấy lệnh (JAVA_HOMErỗng cũng chưa chứng minh gì, vìjavavẫ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.Hello, Linh Tran!vàHello, Linh![Đã chạy]. Dấu ngoặc kép gộp hai chữ thành một từ củaargs. Kiểm tra mục tiêu 2.javacdừng ở lỗi và không tạo file mới, nhưngHello.classcũ vẫn còn, nênjava Hello Lanchạy bản cũ và inHello, Lan!với mã thoát 0 [Đã chạy]. Kiểm tra mục tiêu 2.- Lệnh
javanhận tên lớp, vàHello.classbị đọc thành lớpclasstrong góiHello, không tồn tại. Lệnh đúng làjava Hello. Kiểm tra mục tiêu 3. - 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.