Chương trình Hello World trong Java dài đúng năm dòng, và không dòng nào là thừa. Phần lớn tài liệu chỉ in nó ra, nói "tạm thời chưa cần hiểu chỗ này", rồi đi tiếp — đó là lý do rất nhiều người học mất hàng tuần mà vẫn không biết vì sao main cần static, hay vì sao compiler đột nhiên từ chối file sau khi họ đổi tên một class.
Bài này làm ngược lại: mổ xẻ chương trình Java nhỏ nhất theo từng token, và mọi message lỗi được trích dưới đây đều là output thật của OpenJDK 21.0.6, không phải viết lại theo trí nhớ.
![]()
Toàn bộ nội dung ở đây xoay quanh chính file source — mỗi keyword nghĩa là gì và compiler từ chối những gì. Còn javac với java thực sự biến file đó thành một process đang chạy như thế nào thì là việc của bài tiếp theo.
Chương trình Java nhỏ nhất có thể chạy được
Đặt đoạn này vào một file tên HelloWorld.java:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Compile và chạy:
javac HelloWorld.java && java HelloWorld
Hello, World!
Năm dòng, và không dòng nào là trang trí. Bỏ public khỏi main thì chương trình vẫn compile được nhưng không khởi động được. Bỏ static cũng hỏng, nhưng vì một lý do khác. Đổi tên file thì compiler chặn bạn ngay từ trước đó. Phần còn lại của bài là để giải thích tại sao.
Các thành phần của một file .java, theo đúng thứ tự
Một file .java có thứ tự cố định: khai báo package (không bắt buộc), rồi tới các dòng import, rồi mới tới phần khai báo type. Đây là chính chương trình trên với đầy đủ các phần tùy chọn:
package com.example.hello;
import java.time.LocalDate;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
System.out.println(LocalDate.now());
}
}
Hello, World!
2026-09-04
Dòng thứ hai là ngày bạn chạy chương trình. Toàn bộ file, có chú thích từng phần:

| Thành phần | Bắt buộc | Tác dụng |
|---|---|---|
package | Không | Đặt tên namespace mà class thuộc về. Nhiều nhất một dòng, và phải nằm đầu tiên. |
import | Không | Cho phép viết tên ngắn của một type thay vì tên đầy đủ. |
| Khai báo class | Có | Mọi đoạn code Java ở top level đều nằm trong một class, interface, enum hoặc record. |
Method main | Chỉ khi cần chạy | Entry point mà JVM đi tìm. Một class dạng library thì không cần. |
| Statement | Có | Phần việc thật sự, nằm trong body của method. |
Khai báo package
package com.example.hello; nói rằng class này thuộc namespace com.example.hello. Từ đó có ba quy tắc:
- Nhiều nhất một dòng
package, và nó phải là thứ đầu tiên trong file, chỉ đứng sau comment và khoảng trắng. Một comment license nằm phía trên thì hoàn toàn hợp lệ. - Cấu trúc thư mục phải khớp với package, nên
com.example.hello.HelloWorldnằm ởcom/example/hello/HelloWorld.java. - Tên package viết thường, ngăn cách bằng dấu chấm, và theo quy ước là tên miền đảo ngược mà bạn sở hữu —
com.example.hello,org.apache.commons.lang3. Quy ước này tồn tại để hai library không đụng tên nhau.
Đặt import lên trên package thì compiler không hề báo "sai thứ tự" một cách tử tế — nó đơn giản là không còn nhận ra file này là Java nữa:
import java.util.List;
package com.example;
BadOrder.java:2: error: class, interface, enum, or record expected
package com.example;
^
1 error
Bỏ hẳn dòng package thì class rơi vào default package. Điều đó ổn với một file dùng một lần và với mọi ví dụ trong bài này, nhưng dự án thật thì luôn khai báo package.
Các dòng import
import không hề copy code vào file của bạn. Nó thuần túy là một lối viết tắt về tên: nó báo cho compiler biết rằng khi bạn viết LocalDate, ý bạn là java.time.LocalDate. Bạn luôn có thể bỏ import và viết tên đầy đủ:
public class FullyQualified {
public static void main(String[] args) {
java.util.List<String> names = java.util.List.of("a", "b");
System.out.println(names);
}
}
[a, b]
Vậy vì sao Hello World không cần import nào cả? Vì mọi thứ trong package java.lang được import ngầm vào mọi file source — String, System, Math, Integer, Object, Exception, Thread và phần lõi còn lại của ngôn ngữ:
public class LangDemo {
public static void main(String[] args) {
String s = "no import needed";
System.out.println(s + " " + Math.max(2, 7) + " " + Integer.parseInt("42"));
}
}
no import needed 7 42
Bất cứ thứ gì ngoài java.lang đều cần import. Quên nó là bạn gặp ngay message lỗi phổ biến nhất của người mới học Java:
NoImport.java:3: error: cannot find symbol
List<String> names = List.of("a", "b");
^
symbol: class List
location: class NoImport
Import kiểu wildcard — import java.util.*; — kéo vào mọi type trong package đó, nhưng không kéo theo sub-package: java.util.* không cho bạn java.util.concurrent.ExecutorService. Phần lớn codebase ưu tiên import tường minh vì nó ghi rõ mỗi cái tên đến từ đâu.
Khai báo class
public class HelloWorld { là bốn thứ tách biệt: một access modifier (public), keyword class, tên HelloWorld, và dấu ngoặc nhọn mở ra body của class. Dấu } tương ứng ở cuối file đóng nó lại.
Java không có function trôi nổi bên ngoài và cũng không có statement ở top level. Mọi method, mọi field, mọi hằng số đều thuộc về một type nào đó. Đó là lý do một chương trình chỉ có một dòng vẫn cần một class bao quanh.
Vì sao tên file phải trùng với tên public class
Nếu một class ở top level được khai báo public thì tên của nó và tên file phải giống hệt nhau, kể cả chữ hoa chữ thường. Cố tình phá luật — đặt public class HelloWorld vào một file tên Program.java — và javac từ chối:

Cùng một tên file, đặt cạnh bốn cách khai báo class khác nhau:
Program.java:1: error: class HelloWorld is public, should be declared in a file named HelloWorld.java
public class HelloWorld {
^
1 error
Hai hệ quả xuất hiện ngay lập tức.
Một file chứa nhiều nhất một public class ở top level. Hai cái thì cả hai đều đòi sở hữu tên file:
public class TwoPublic {
public static void main(String[] args) {
System.out.println("hi");
}
}
public class Helper {
}
TwoPublic.java:7: error: class Helper is public, should be declared in a file named Helper.java
public class Helper {
^
1 error
Class không public thì nằm ở file tên gì cũng được. Quy tắc trên chỉ áp dụng cho type public, nên đoạn dưới compile được dù không class nào trùng tên file Anything.java:
class Helper {
static String greet() {
return "from Helper";
}
}
class Runner {
public static void main(String[] args) {
System.out.println(Helper.greet());
}
}
javac Anything.java # sinh ra Helper.class và Runner.class
java Runner
from Helper
Để ý điều này còn chứng minh thêm hai chuyện: mỗi class sinh ra một file .class riêng đặt theo tên class chứ không theo tên file source, và một class không cần public vẫn có thể chứa main chạy được.
public static void main(String[] args), phân tích từng keyword
Signature này không phải một câu thần chú. Mỗi chữ trong đó đều là một modifier Java bình thường, và ở đây nó bắt buộc vì một lý do cụ thể.
public
public nghĩa là nhìn thấy được từ mọi nơi. Launcher của JVM là code nằm ngoài class và ngoài package của bạn, nên nó cần main hiển thị ra bên ngoài thì mới gọi được. Để private thì launcher không thấy nó — và báo là method không tồn tại chứ không phải bị che:
Error: Main method not found in class Wrong5, please define the main method as:
public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application
static
static nghĩa là method thuộc về chính class chứ không thuộc về một instance nào. Đây mới là chỗ quan trọng về mặt khái niệm: tại thời điểm chương trình bắt đầu, chưa có object nào của class bạn tồn tại, và JVM cũng không có cách nào biết phải tạo ra nó thế nào — class của bạn có thể không có constructor không tham số, hoặc constructor của nó cần một database connection.
Nên launcher gọi thẳng method trên class, và điều đó đòi hỏi static. Bỏ nó đi thì JVM nói đúng y như vậy:
Error: Main method is not static in class Wrong2, please define the main method as:
public static void main(String[] args)
void
void nghĩa là method không trả về gì. Rất dễ nghĩ rằng main nên trả về một exit code giống như main trong C trả về int, nhưng Java không làm vậy — một process Java báo exit status qua System.exit(int), và hàm này gọi được từ bất cứ đâu chứ không riêng gì trong main:
public class Exit {
public static void main(String[] args) {
System.out.println("about to exit");
System.exit(3);
}
}
Chạy đoạn đó rồi hỏi shell giá trị $? sẽ được 3. Khai báo main với bất kỳ return type nào khác thì vẫn compile được nhưng chết lúc khởi động:
Error: Main method must return a value of type void in class Wrong3, please
define the main method as:
public static void main(String[] args)
main
Cái tên này là cố định. Launcher tìm đúng một method tên main — viết thường — và không chấp nhận gì khác. Java phân biệt hoa thường, nên Main là một method hoàn toàn khác, và JVM báo class không có entry point:
Error: Main method not found in class Wrong1, please define the main method as:
public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application
Nhân tiện, main không phải là reserved word. Nó là một tên method bình thường mà launcher tình cờ đi tìm, nên bạn hoàn toàn có thể tự gọi nó như mọi static method khác.
String[] args
args là một array chứa các command-line argument theo đúng thứ tự, và không bao gồm tên chương trình — khác với C, nơi argv[0] chính là file thực thi.
public class Greet {
public static void main(String[] args) {
System.out.println("args.length = " + args.length);
for (int i = 0; i < args.length; i++) {
System.out.println("args[" + i + "] = " + args[i]);
}
}
}
java Greet Alice 42 "hello there"
args.length = 3
args[0] = Alice
args[1] = 42
args[2] = hello there
Ba chi tiết đáng nhớ kỹ:
- Mọi thứ đều là
String.42tới nơi dưới dạng chuỗi hai ký tự"42"chứ không phải một con số. Chuyển đổi là việc của bạn —Integer.parseInt(args[1]). - Chuyện dấu ngoặc kép là của shell, không phải của Java.
"hello there"tới nơi như một argument duy nhất vì shell đã gom lại, không phải vì Java làm điều đó. - Không truyền argument nào thì
argslà array rỗng, không bao giờ lànull. Chạy chính class đó mà không có argument sẽ in raargs.length = 0, và động vàoargs[0]thì ném exception chứ không phải NPE:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
at Tool.main(App.java:10)
Còn hai cách viết nữa cũng hợp lệ. String... args (varargs) được launcher chấp nhận và giúp bạn gọi main gọn hơn từ code của mình:
public class VarargsMain {
public static void main(String... args) {
System.out.println("varargs main ran, args.length = " + args.length);
}
}
varargs main ran, args.length = 2
Và String args[], đặt cặp ngoặc vuông sau tên, là cú pháp kiểu C mà Java thừa hưởng lại. Nó compile và chạy bình thường, nhưng ngày nay không ai viết như vậy nữa. Tên của parameter thì tự do — main(String[] arguments) chạy y hệt.
Điều gì xảy ra khi signature bị sai
Có một điểm chung đáng chú ý: mọi lỗi dưới đây đều compile sạch sẽ và chỉ chết khi bạn chạy. javac hoàn toàn không biết bạn định dùng method đó làm entry point.
| Bạn viết | Kết quả |
|---|---|
public static void Main(String[] args) | Main method not found |
public void main(String[] args) | Main method is not static |
public static int main(String[] args) | Main method must return a value of type void |
private static void main(String[] args) | Main method not found |
public static void main() | Main method not found |
public static void main(String... args) | Chạy được |
Statement, block, phân biệt hoa thường và whitespace
Một statement kết thúc bằng dấu chấm phẩy. System.out.println("Hello, World!"); là một statement. Dấu chấm phẩy là dấu kết thúc chứ không phải dấu ngăn cách, nên statement cuối cùng trong một block cũng cần nó. Một dấu ; thừa là một empty statement hợp lệ và không làm gì cả.
Dấu ngoặc nhọn gom các statement thành một block. Body của class, body của method, body của vòng lặp — tất cả đều là block. Chúng lồng nhau, và mỗi } đóng lại dấu { mở gần nhất còn treo. Quên một cái là compiler chạy hết file mà vẫn đi tìm nó:
E6.java:4: error: reached end of file while parsing
}
^
1 error
Java phân biệt hoa thường ở mọi chỗ. String là một class còn string chẳng là gì cả; System là một class còn system thì compiler nhìn ra như tên một package. Một lỗi gõ sinh ra hai message khác hẳn nhau:
public class E3 {
public static void main(string[] args) {
system.out.println("Hello");
}
}
E3.java:2: error: cannot find symbol
public static void main(string[] args) {
^
symbol: class string
location: class E3
E3.java:3: error: package system does not exist
system.out.println("Hello");
^
2 errors
Whitespace là tự do. Java không quan tâm tới xuống dòng hay thụt lề theo kiểu Python. Cả chương trình gói trên một dòng vẫn compile và chạy:
public class OneLine{public static void main(String[] args){System.out.println("Still valid Java.");}}
Still valid Java.
Chính vì thế mà định dạng code là convention chứ không phải syntax, và convention đó được siết bằng áp lực cộng đồng thay vì bằng compiler: bốn dấu cách mỗi cấp, dấu ngoặc mở nằm cùng dòng, mỗi dòng một statement. Mọi codebase Java bạn sẽ tham gia đều theo một biến thể của nó, và mọi IDE đều tự format lại khi bạn lưu file.
In ra console
System, out và println thực chất là gì?

Khác biệt nằm trọn ở chỗ mỗi hàm ghi thêm cái gì, và do đó con trỏ dừng lại ở đâu:
System.out.println("...") là lần đầu tiên bạn nhìn thấy một chuỗi truy cập chạy xuyên suốt cả ngôn ngữ: một class, một field trên class đó, và một method trên giá trị của field đó.
| Token | Nó là gì |
|---|---|
System | Một class final trong java.lang, nên không cần import. |
out | Một field public static final của System, kiểu java.io.PrintStream. |
println | Một instance method của PrintStream, in ra argument rồi xuống dòng. |
Bạn có thể lôi field đó ra để chứng minh:
import java.io.PrintStream;
public class Chain {
public static void main(String[] args) {
PrintStream out = System.out;
out.println("System is a class, out is a static field, println is a method.");
System.out.println(System.out.getClass().getName());
}
}
System is a class, out is a static field, println is a method.
java.io.PrintStream
print là cùng một method nhưng không xuống dòng ở cuối, còn println() không truyền gì thì in ra một dòng trống:
System.out.print("A");
System.out.print("B");
System.out.println("C");
System.out.println("next line");
ABC
next line
printf và các format specifier
printf nhận một format string cùng các giá trị cần chèn vào. Đây là công cụ đúng cho những chỗ mà bạn định nối chuỗi bằng một đống dấu +:
System.out.printf("%s scored %d points (%.2f%% accuracy)%n", "Alice", 42, 91.5);
System.out.printf("%-10s|%5d|%n", "left", 7);
System.out.printf("%08.3f%n", 3.14159);
Alice scored 42 points (91.50% accuracy)
left | 7|
0003.142
| Specifier | Ý nghĩa |
|---|---|
%s | Object bất kỳ, thông qua toString() |
%d | Các integer type |
%f | Số thực; %.2f cố định hai chữ số thập phân |
%n | Dấu xuống dòng theo platform |
%% | Một dấu phần trăm |
%-10s | Căn trái trong ô rộng 10 ký tự |
%5d | Căn phải trong ô rộng 5 ký tự |
Lưu ý rằng printf không tự thêm dấu xuống dòng. Hãy dùng %n thay vì \n: %n sinh ra dấu xuống dòng của platform, còn \n luôn luôn là đúng một ký tự line feed.
System.err
System.err là một PrintStream thứ hai, trỏ vào standard error thay vì standard output. Thông tin chẩn đoán và message lỗi thuộc về chỗ đó, để người dùng có thể redirect một stream mà không mất stream kia:
System.out.println("this is normal output");
System.err.println("this went to standard error");
java Printing 2>/dev/null # giữ stdout, bỏ stderr
java Printing 1>/dev/null # giữ stderr, bỏ stdout
Escape sequence và text block
Trong một string literal, dấu gạch chéo ngược mở đầu một escape sequence. Bạn cần chúng vì một string literal không thể vắt qua nhiều dòng và không thể chứa dấu nháy kép chưa escape.

Dịch dấu đóng là viết lại mọi dòng của string:
| Escape | Kết quả |
|---|---|
\n | Line feed |
\t | Tab |
\" | Một dấu nháy kép |
\\ | Một dấu gạch chéo ngược |
\' | Một dấu nháy đơn (chỉ cần trong char literal) |
\uXXXX | Ký tự có code point hex tương ứng |
System.out.println("Line one\nLine two");
System.out.println("Name:\tAlice");
System.out.println("She said \"hello\".");
System.out.println("Windows path: C:\\Users\\alice");
System.out.println("Unicode: \u00e0 \u0111");
Line one
Line two
Name: Alice
She said "hello".
Windows path: C:\Users\alice
Unicode: à đ
Từ Java 15, output nhiều dòng nên viết bằng text block: ba dấu nháy kép, xuống dòng, rồi tới nội dung. Dấu nháy bên trong không cần escape, và phần khoảng trắng thụt lề chung được cắt bỏ tự động dựa trên độ thụt của dấu đóng:
public class TextBlockDemo {
public static void main(String[] args) {
String json = """
{
"name": "Alice",
"age": 30
}""";
System.out.println(json);
}
}
{
"name": "Alice",
"age": 30
}
Comment và Javadoc
Java có ba dạng comment, và dạng thứ ba không chỉ là comment:
// line comment: mọi thứ tới hết dòng
/*
block comment,
trải dài nhiều dòng
*/
/**
* Javadoc comment. Mô tả phần khai báo ngay bên dưới nó.
*
* @param name the name to greet
* @return the greeting line
*/
public static String greet(String name) {
return "Hello, " + name + "!";
}
Compiler bỏ qua cả ba. Nhưng JDK có kèm công cụ javadoc đọc đúng dạng /** */ và sinh ra một tài liệu API dạng HTML duyệt được — chạy nó trên class ở trên sẽ tạo ra Greeter.html, index.html, một search index và phần bố cục quen thuộc mà bạn thấy trên trang API chính thức của Java. Đó là lý do nên học sớm cú pháp tag (@param, @return, @throws, @author): các tag này trở thành những mục có cấu trúc trong trang được sinh ra.
⚠️ Block comment không lồng nhau được. Một
/*nằm trong/* ... */không mở ra comment thứ hai, nên comment tạm một vùng code vốn đã chứa block comment sẽ kết thúc sớm và làm hỏng file. Trong trường hợp đó line comment an toàn hơn.
Quy ước đặt tên và quy tắc identifier
Quy tắc của Java về tên thì khá dễ dãi; còn quy ước thì gần như tuyệt đối và mọi team đều coi là bắt buộc.
| Loại | Quy ước | Ví dụ |
|---|---|---|
| Class, interface, enum, record | PascalCase | HelloWorld, LocalDate |
| Method | camelCase, thường là động từ | println, parseInt, greet |
| Variable, parameter, field | camelCase, là danh từ | args, userName, count |
Hằng số (static final) | UPPER_SNAKE_CASE | MAX_VALUE, DEFAULT_TIMEOUT |
| Package | toàn chữ thường, ngăn bằng dấu chấm | com.example.hello |
Quy tắc identifier thật sự là: một identifier bắt đầu bằng chữ cái, _ hoặc $, và tiếp theo là chữ cái, chữ số, _ hoặc $. "Chữ cái" ở đây là mọi chữ cái Unicode, nên đoạn dưới compile được và in ra 10:
int count = 1;
int _count = 2;
int $price = 3;
int giáTrị = 4;
System.out.println(count + _count + $price + giáTrị);
Hợp lệ không có nghĩa là nên dùng. $ theo quy ước được dành cho tên do compiler sinh ra, còn identifier không phải ASCII sẽ gây rắc rối ngay khi ai đó mở file bằng một encoding khác. Hãy bám vào ASCII và bám vào bảng ở trên.
Reserved word thì không thể làm identifier. Dùng nhầm một cái là parser lạc hướng hoàn toàn — chỉ một dòng int class = 5; sinh ra năm lỗi:
Bad.java:3: error: not a statement
int class = 5;
^
Bad.java:3: error: ';' expected
int class = 5;
^
Bad.java:3: error: <identifier> expected
int class = 5;
^
Riêng dấu gạch dưới đứng một mình trước đây là tên hợp lệ, nay thì không. Trong Java 21 nó được giữ lại cho tính năng unnamed variable, và dùng nó làm tên bình thường sẽ báo:
Under.java:3: error: unnamed variables are a preview feature and are disabled by default.
int _ = 5;
^
(use --enable-preview to enable unnamed variables)
Những lỗi compile bạn sẽ gặp ngay ngày đầu
Năm lỗi dưới đây chiếm gần hết tuần đầu tiên của người mới học. Toàn bộ message đều là output nguyên văn của javac.
1. Thiếu dấu chấm phẩy. Dấu mũi tên chỉ vào vị trí sau cuối dòng, vì đó mới là chỗ đáng lẽ phải có dấu chấm phẩy:
E1.java:3: error: ';' expected
System.out.println("Hello, World!")
^
1 error
2. String literal chưa đóng. Dấu mũi tên chỉ vào dấu nháy mở, vì compiler đã đi hết dòng mà không tìm ra dấu nháy còn lại:
E2.java:3: error: unclosed string literal
System.out.println("Hello, World!);
^
1 error
3. Sai hoa thường ở tên method. Println thay vì println — để ý message có nêu type của đối tượng nhận, một gợi ý hữu ích rằng out là một PrintStream:
E4.java:3: error: cannot find symbol
System.out.Println("Hello");
^
symbol: method Println(String)
location: variable out of type PrintStream
1 error
4. Sai hoa thường ở keyword. Public không phải public, nên parser thậm chí chưa đi tới được phần class:
E7.java:1: error: class, interface, enum, or record expected
Public class E7 {
^
1 error
5. Dùng local variable trước khi gán giá trị. Java từ chối đọc một local variable chưa chắc chắn được gán — đây là lỗi compile chứ không phải bất ngờ lúc runtime:
int count;
System.out.println(count);
E5.java:4: error: variable count might not have been initialized
System.out.println(count);
^
1 error
Hãy đọc output của javac từ trên xuống: lỗi đầu tiên thường là lỗi thật, phần còn lại nhiều khi chỉ là parser loạng choạng theo sau nó.
Từ Java 21: instance main method và implicitly declared class
Java đang dần rút ngắn phần nghi thức quanh entry point. JEP 445, ra mắt như một preview feature trong Java 21, cho phép một file source không khai báo class nào cả và có một main dạng instance — không public, không static, không parameter:
void main() {
System.out.println("Hello from an implicitly declared class");
}
Java 21 gọi chúng là "unnamed class"; JEP 463 (Java 22) đổi tên thành implicitly declared class, JEP 477 (Java 23) bổ sung phần import tự động cho các tiện ích thông dụng, và tính năng được chốt chính thức bởi JEP 512 trong Java 25. Trên Java 21 nó vẫn chỉ là preview, nên mặc định compiler từ chối:
Hello.java:1: error: unnamed classes are a preview feature and are disabled by default.
void main() {
^
(use --enable-preview to enable unnamed classes)
1 error
Cả compiler lẫn launcher đều cần flag, và compiler cảnh báo mỗi lần:
javac --enable-preview --release 21 Hello.java
Note: Hello.java uses preview features of Java SE 21.
Note: Recompile with -Xlint:preview for details.
java --enable-preview Hello
Hello from an implicitly declared class
Quên flag lúc chạy thì file class bị từ chối thẳng, vì class file dạng preview được đánh dấu minor version 65535:
Error: LinkageError occurred while loading main class Hello
java.lang.UnsupportedClassVersionError: Preview features are not enabled for Hello (class file version 65.65535). Try running with '--enable-preview'
Đây không phải dạng nên học đầu tiên. Nó sinh ra để rút ngắn giờ học đầu tiên của người mới, nhưng mọi codebase Java thật, mọi tài liệu, mọi câu trả lời trên Stack Overflow và mọi buổi phỏng vấn đều dùng public static void main(String[] args) cổ điển bên trong một class có tên. Hãy học dạng đó, và coi dạng rút gọn là tiện ích cho file nháp sau khi bạn đã hiểu nó đang giấu đi những gì.
FAQ
Class chứa main có bắt buộc phải public không?
Không. Một class package-private vẫn chạy tốt, đúng như ví dụ Runner ở trên. public là điều kiện để class dùng được từ package khác, không phải điều kiện để chạy được.
Một file có thể chứa nhiều main method không?
Có — mỗi class một cái. JVM chạy main của class mà bạn nêu tên trên dòng lệnh, còn bạn vẫn có thể tự gọi main của class khác như mọi static method:
public class App {
public static void main(String[] args) {
System.out.println("App.main");
Tool.main(new String[]{"called by hand"});
}
}
class Tool {
public static void main(String[] args) {
System.out.println("Tool.main, args[0] = " + args[0]);
}
}
App.main
Tool.main, args[0] = called by hand
main có bắt buộc phải là method đầu tiên trong class không?
Không. Thứ tự các method trong body của class không có ý nghĩa gì với compiler, và một method hoàn toàn gọi được method khai báo bên dưới nó. Cứ sắp xếp sao cho dễ đọc nhất.
Vì sao output bị lẫn thứ tự khi dùng cả System.out và System.err?
Vì đó là hai stream độc lập với cơ chế buffer độc lập, nên terminal có thể xen kẽ chúng theo thứ tự bạn không ngờ tới. Nếu thứ tự quan trọng, hãy ghi tất cả vào một stream duy nhất.
Có cần dấu chấm phẩy sau dấu đóng ngoặc nhọn của class không?
Không. Dấu } tự nó đã đóng body của class. Một dấu ; phía sau là khai báo rỗng hợp lệ và compiler bỏ qua — vô hại, nhưng không ai viết vậy.
Khác nhau giữa \n và %n là gì?
\n luôn là đúng một ký tự line feed U+000A. Còn %n, chỉ hợp lệ trong format string của printf, sinh ra dấu xuống dòng của platform — trên Windows là carriage return cộng line feed. Trong printf hãy ưu tiên %n, và nói chung hãy ưu tiên println thay vì nhét \n vào cuối chuỗi.
Kết luận
Mỗi token trong Hello World đều có lý do tồn tại: package đặt tên namespace, import rút gọn tên type cho mọi thứ nằm ngoài java.lang, tên public class bị ràng buộc với tên file, và public static void main(String[] args) là đúng hình dạng mà launcher đi tìm — public để gọi tới được, static vì chưa có object nào tồn tại, void vì exit code đến từ System.exit, main vì cái tên đó được gán cứng, và String[] args vì dòng lệnh tới nơi dưới dạng chuỗi.
Đến giờ bạn đã compile và chạy file đó vài lần bằng javac HelloWorld.java && java HelloWorld mà chưa hề biết hai lệnh đó thực sự làm gì. Đó chính là bài tiếp theo: javac, java và JVM biến một file .java thành một chương trình đang chạy như thế nào — file .class cùng bytecode của nó, classpath dùng để làm gì, và vì sao "compile once, run anywhere" là một tuyên bố về pipeline đó chứ không phải về bản thân ngôn ngữ.