Hôm qua ta đã phân biệt được JDK, JRE, JVM và chốt là cứ cài JDK bản LTS của Temurin. Hôm nay làm thật.

Tôi sẽ không dắt bạn qua trình cài đặt bấm next-next, vì cách đó có một điểm yếu chí mạng mà bạn chỉ phát hiện ra sau vài tháng: một ngày nào đó bạn sẽ phải bảo trì một dự án cũ chạy Java 17 trong khi dự án chính đang dùng 21, và lúc đó chuyện đổi qua đổi lại giữa hai phiên bản sẽ trở thành nỗi khổ hằng ngày.

Nên ta làm cho tử tế ngay từ đầu.

SDKMAN: đổi phiên bản Java trong một dòng lệnh

Trên Linux và macOS:

curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"

Xem có những bản nào:

sdk list java

Danh sách khá dài. Cột Identifier là thứ bạn cần, và hậu tố cho biết ai build: -tem là Temurin, -amzn là Corretto của Amazon, -zulu là Azul.

sdk install java 21.0.11-tem

Xong. Kiểm tra:

$ java -version
openjdk version "21.0.11" 2026-04-21 LTS
OpenJDK Runtime Environment Temurin-21.0.11+10 (build 21.0.11+10-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.11+10 (build 21.0.11+10-LTS, mixed mode, sharing)

Cái hay của SDKMAN nằm ở ba lệnh sau:

sdk use java 17.0.11-tem      # chỉ đổi cho cửa sổ terminal hiện tại
sdk default java 21.0.11-tem  # đổi mặc định cho mọi cửa sổ mở sau này
sdk current java              # đang dùng bản nào

sdk use chỉ ảnh hưởng tới phiên làm việc hiện tại. Nghĩa là bạn mở hai cửa sổ terminal, một cái chạy Java 17 cho dự án cũ, một cái chạy Java 21 cho dự án mới, không xung đột gì.

Còn một thứ nữa rất đáng dùng mà ít người biết: file .sdkmanrc đặt trong thư mục dự án.

cd du-an-cu
sdk env init          # sinh ra .sdkmanrc

Sửa nội dung thành java=17.0.11-tem, rồi từ đó về sau mỗi lần cd vào thư mục này, chạy sdk env là tự động đúng phiên bản. Commit file đó vào git thì cả nhóm dùng chung một bản Java, hết cảnh "máy tôi chạy được mà".

Trên Windows, winget install EclipseAdoptium.Temurin.21.JDK là cách nhanh nhất; muốn quản lý nhiều phiên bản thì dùng Scoop.

Chương trình đầu tiên

Tạo file Xin.java:

public class Xin {
    public static void main(String[] args) {
        System.out.println("Chào, đây là Java " + Runtime.version().feature());
    }
}

Rồi chạy thẳng, không biên dịch, không project, không Maven:

$ java Xin.java
Chào, đây là Java 21

Chế độ một file này có từ Java 11. Nó biên dịch trong bộ nhớ rồi chạy luôn — cực hợp để thử một ý tưởng nhỏ mà không muốn dựng cả dự án. Bạn thậm chí có thể biến file Java thành một script chạy được trên Linux bằng cách thêm dòng đầu:

#!/usr/bin/env java --source 21

public class Chao {
    public static void main(String[] args) {
        System.out.println("Chào " + (args.length > 0 ? args[0] : "bạn"));
    }
}

chmod +x chao rồi ./chao Minh. Không phải thứ bạn dùng hằng ngày, nhưng biết thì có lúc cần.

Đường đi truyền thống vẫn là hai bước, và nó mới là thứ diễn ra trong mọi dự án thật:

$ javac Xin.java
$ java Xin
Chào, đây là Java 21

Để ý là lần chạy sau không có đuôi .java. Đây chính là chỗ nhiều người mới vấp. javac Xin.java nhận vào một file. java Xin nhận vào một tên lớp. Gõ java Xin.class là sai, vì JVM sẽ đi tìm một lớp tên là Xin.class trong package Xin.

jshell: cái máy tính bỏ túi của lập trình viên Java

Trước Java 9, muốn thử xem "abc".substring(1) trả về gì thì phải tạo cả một class có main. Bây giờ:

$ jshell
jshell> int a = 5;
a ==> 5

jshell> System.out.println(a * 2);
10

jshell> "Xin chào".toUpperCase()
$3 ==> "XIN CHÀO"

jshell> /exit

Không cần dấu chấm phẩy cuối nếu chỉ muốn xem giá trị. Không cần class bao ngoài. Không cần main.

Tôi dùng jshell nhiều nhất cho hai việc: kiểm tra nhanh một phương thức của thư viện chuẩn mà mình không nhớ chính xác chữ ký, và thử một biểu thức regex trước khi dán vào mã thật. Vài lệnh đáng nhớ:

Lệnh Việc
/vars Liệt kê các biến đang có
/list Xem lại toàn bộ mã đã gõ
/imports Xem những import đang được nạp sẵn
/save bai.jsh Lưu phiên làm việc ra file
/help Danh sách đầy đủ

Ba lỗi ai cũng gặp trong ngày đầu

"class Xin is public, should be declared in a file named Xin.java"

Lớp public bắt buộc phải nằm trong file trùng tên, phân biệt hoa thường. Đặt file là xin.java mà lớp là Xin thì javac từ chối ngay.

"Could not find or load main class Xin"

Lỗi này có ba nguyên nhân, xếp theo tần suất tôi gặp:

Thứ nhất, bạn đang đứng sai thư mục — JVM tìm Xin.class theo classpath, mặc định là thư mục hiện tại.

Thứ hai, lớp có khai package. Nếu file bắt đầu bằng package com.vidu; thì .class phải nằm trong thư mục com/vidu/, và bạn phải chạy từ thư mục gốc bằng java com.vidu.Xin — tên đầy đủ, không phải mỗi Xin.

Thứ ba, bạn gõ java Xin.class.

Chương trình biên dịch được nhưng chạy chẳng thấy gì

Gần như luôn là do chữ ký của main sai. JVM tìm đúng một chữ ký:

public static void main(String[] args)

Thiếu static, đổi String[] thành String, hay viết hoa thành Main — JVM đều không nhận ra và báo không tìm thấy điểm vào. Tên tham số thì đổi thoải mái, args chỉ là quy ước.

Trên Windows, nếu chương trình in ra tiếng Việt bị lỗi font trong Command Prompt, đó không phải lỗi mã của bạn — là do bảng mã của console. Chạy chcp 65001 trước, hoặc dùng Windows Terminal.

Chuẩn bị IDE

Với loạt bài này tôi dùng IntelliJ IDEA Community — miễn phí và đủ dùng. VS Code với bộ Extension Pack for Java cũng ổn nếu bạn đã quen nó.

Sau khi mở dự án, việc đầu tiên nên làm là kiểm tra IDE đang dùng đúng JDK: File → Project Structure → Project SDK. Đây là nguồn gốc của một lớp lỗi rất khó chịu — build từ terminal thì chạy, mở IDE lên thì đỏ lòm, chỉ vì hai bên nhìn vào hai phiên bản Java khác nhau.

Kiểm tra nhanh từ dòng lệnh:

$ echo $JAVA_HOME
/Users/ban/.sdkman/candidates/java/current

$ which java
/Users/ban/.sdkman/candidates/java/current/bin/java

Hai đường dẫn này phải khớp nhau. Nếu which java trỏ vào /usr/bin/java trong khi JAVA_HOME trỏ chỗ khác, bạn có hai bản Java trên máy và sớm muộn gì cũng gặp rắc rối.

Vài phím tắt IntelliJ tiết kiệm được nhiều thời gian nhất, theo thứ tự tôi dùng:

Phím (Windows / macOS) Việc
Shift Shift Tìm mọi thứ — file, lớp, cả cài đặt
Ctrl+B / Cmd+B Nhảy tới định nghĩa
Alt+Enter / Opt+Enter Gợi ý sửa lỗi tại chỗ con trỏ
Ctrl+Alt+L / Cmd+Opt+L Định dạng lại mã
Ctrl+Alt+V / Cmd+Opt+V Tách biểu thức thành biến
sout + Tab Sinh ra System.out.println()

Riêng Alt+Enter là phím đáng học nhất. Con trỏ đang ở chỗ nào có gạch chân đỏ hoặc vàng, bấm nó, IDE đề xuất cách sửa. Rất nhiều thứ tôi biết về Java là do đọc gợi ý của IDE rồi tò mò tìm hiểu tại sao.

Còn cái file .class kia thì sao

Hôm qua tôi có hứa sẽ mổ nó ra. Xem thử phần vỏ trước đã:

$ javap Xin
Compiled from "Xin.java"
public class Xin {
  public Xin();
  public static void main(java.lang.String[]);
}

Có một chi tiết thú vị: tôi không hề viết hàm khởi tạo public Xin() nào, nhưng nó vẫn có mặt. Trình biên dịch tự thêm vào — hàm khởi tạo mặc định. Đây là ví dụ đầu tiên trong rất nhiều thứ mà javac âm thầm làm sau lưng bạn.

Ngày mai ta mở tiếp bằng javap -c để nhìn thẳng vào bytecode, và bạn sẽ thấy một dòng System.out.println đơn giản thực ra được dịch thành bao nhiêu lệnh — cùng với lý do vì sao phép nối chuỗi bằng dấu + lại xuất hiện dưới cái tên lạ hoắc invokedynamic.