- Phân biệt dứt điểm JVM, JRE, JDK — và biết mình cần cài cái nào.
- Hiểu chuỗi mã nguồn → bytecode → JVM → mã máy, và vì sao Java vừa biên dịch vừa thông dịch.
- Cài Java 21 LTS bằng SDKMAN; biết chọn giữa Temurin, Corretto, Zulu, Oracle JDK.
- Biên dịch và chạy bằng
javac/java; hiểu chế độ chạy một tệp (Java 11+). - Đọc bytecode bằng
javap— nhìn thấy thứ JVM thực sự thực thi. - Dùng JShell để thử nghiệm nhanh mà không cần tạo class.
- Nắm quy tắc bắt buộc: tên tệp phải trùng tên class public; cấu trúc
package↔ thư mục. - Hiểu vai trò của Maven và Gradle; đọc được
pom.xml. - Biết lịch phát hành và ý nghĩa của LTS — chọn phiên bản cho dự án thật.
1. Java là gì — và vì sao nó được thiết kế như vậy
Java ra đời năm 1995 tại Sun Microsystems, do nhóm của James Gosling phát triển. Bài toán họ nhắm tới ban đầu không phải web hay doanh nghiệp, mà là thiết bị điện tử gia dụng: đầu thu truyền hình, lò vi sóng, thiết bị cầm tay. Mỗi hãng dùng một loại chip khác nhau, và viết lại phần mềm cho từng chip là ác mộng.
Giải pháp của họ về sau trở thành khẩu hiệu nổi tiếng nhất ngành phần mềm: "Write Once, Run Anywhere" — viết một lần, chạy mọi nơi. Cách làm: đừng biên dịch thẳng ra mã máy của một CPU cụ thể. Hãy biên dịch ra một ngôn ngữ trung gian, rồi viết một chương trình nhỏ (máy ảo) cho từng nền tảng để dịch ngôn ngữ trung gian đó sang mã máy tại chỗ.
bạn viết
một lần duy nhất
độc lập nền tảng
riêng mỗi OS
CPU thực thi
.class mang từ macOS sang Linux chạy được ngay, không cần
biên dịch lại.
| Mô hình | Ngôn ngữ | Cách chạy | Đánh đổi |
|---|---|---|---|
| Biên dịch trước (AOT) | C, C++, Go, Rust | Mã nguồn → mã máy trực tiếp | Nhanh nhất, khởi động tức thì — nhưng mỗi nền tảng cần một bản build riêng |
| Thông dịch | Python, JavaScript, PHP | Đọc và thực thi từng dòng lúc chạy | Linh hoạt, chạy ngay — nhưng chậm hơn và lỗi kiểu chỉ lộ ra lúc chạy |
| Lai (bytecode + JIT) | Java, C#, Kotlin, Scala | Mã nguồn → bytecode → JVM thông dịch, phần nóng thì biên dịch JIT sang mã máy | Khởi động chậm hơn AOT, nhưng chạy lâu thì nhanh gần bằng — và một bản build chạy mọi nơi |
JIT — vì sao Java chạy lâu lại nhanh lên
JVM không chỉ thông dịch bytecode. Nó quan sát chương trình đang chạy: phương thức nào được gọi hàng nghìn lần, vòng lặp nào chạy nóng. Những phần đó được trình biên dịch JIT (Just-In-Time) dịch thẳng sang mã máy và tối ưu — với lợi thế mà trình biên dịch AOT không có: nó biết dữ liệu thật đang chạy qua đó. Kết quả là một dịch vụ Java chạy vài phút có thể nhanh ngang C++ ở những đoạn nóng nhất. Đổi lại, mấy giây đầu tiên luôn chậm — đây là lý do Java từng không hợp với hàm serverless khởi động liên tục (và là lý do GraalVM Native Image ra đời).
1.1. Ai đang dùng Java
| Lĩnh vực | Ví dụ cụ thể | Vì sao chọn Java |
|---|---|---|
| Ngân hàng & tài chính | Hệ thống core banking, thanh toán, giao dịch chứng khoán | Ổn định nhiều thập kỷ, kiểu tường minh, hệ sinh thái kiểm toán trưởng thành |
| Backend doanh nghiệp | Spring Boot, Jakarta EE | Hệ sinh thái lớn nhất, dễ tuyển người, công cụ giám sát tốt |
| Dữ liệu lớn | Hadoop, Spark, Kafka, Elasticsearch, Flink | Chạy trên JVM, đa luồng mạnh, quản lý bộ nhớ tự động |
| Android | Toàn bộ nền tảng Android | Java là ngôn ngữ gốc; Kotlin nay phổ biến hơn nhưng vẫn chạy trên cùng máy ảo |
| Công cụ lập trình | IntelliJ IDEA, Eclipse, Jenkins, SonarQube | Đa nền tảng thật sự — một bản build cho cả ba hệ điều hành |
2. JVM, JRE, JDK — ba tầng lồng nhau
Đây là phần bị hiểu nhầm nhiều nhất. Ba khái niệm này lồng vào nhau như búp bê Nga, mỗi tầng bao gồm tầng bên trong cộng thêm một số thứ.
| JVM | JRE | JDK | |
|---|---|---|---|
| Viết tắt của | Java Virtual Machine | Java Runtime Environment | Java Development Kit |
| Là gì | Máy ảo thực thi bytecode | JVM + thư viện chuẩn | JRE + công cụ phát triển |
| Chạy được app? | Không (thiếu thư viện) | Có | Có |
| Biên dịch được? | Không | Không | Có |
| Ai cần | — | Người dùng cuối chạy app Java | Lập trình viên — chính là bạn |
Trước Java 11, Oracle phát hành JRE thành gói cài đặt riêng cho người dùng cuối. Từ Java 11 điều đó chấm
dứt — nay cách phân phối hiện đại là dùng jlink để tạo một runtime gọn nhẹ chỉ chứa
đúng module ứng dụng cần, rồi đóng gói kèm ứng dụng. Vì vậy trong thực tế hôm nay bạn chỉ cần nhớ:
cài JDK là xong. Khái niệm JRE vẫn cần hiểu vì nó xuất hiện dày đặc trong tài liệu cũ
và câu hỏi phỏng vấn.
3. Cài Java 21 — chọn bản phân phối nào
Java có một điểm khác thường: bản đặc tả (specification) là chuẩn mở, nên nhiều tổ chức cùng phát hành JDK từ cùng một mã nguồn OpenJDK. Chúng gần như giống hệt nhau về tính năng, khác nhau ở giấy phép và chính sách hỗ trợ.
| Bản phân phối | Do ai | Giấy phép | Nên chọn khi |
|---|---|---|---|
| Eclipse Temurin ⭐ | Eclipse Adoptium | Miễn phí hoàn toàn, kể cả thương mại | Mặc định — trung lập, cộng đồng lớn nhất |
| Amazon Corretto | Amazon | Miễn phí | Triển khai trên AWS |
| Azul Zulu | Azul Systems | Miễn phí (có bản trả phí) | Cần hỗ trợ thương mại |
| Microsoft Build of OpenJDK | Microsoft | Miễn phí | Triển khai trên Azure |
| Oracle JDK | Oracle | ⚠️ Giấy phép có điều kiện, đã đổi nhiều lần | Chỉ khi công ty có hợp đồng với Oracle |
| GraalVM | Oracle | Bản Community miễn phí | Cần biên dịch ra binary gốc (native image) |
Oracle đã thay đổi giấy phép JDK nhiều lần trong những năm gần đây, có giai đoạn yêu cầu trả phí cho sử dụng thương mại. Nhiều công ty đã dính rắc rối pháp lý vì lập trình viên tải nhầm bản này. Với dự án thật, hãy dùng Temurin hoặc bản của nhà cung cấp đám mây bạn đang dùng — chúng cùng xuất phát từ OpenJDK và không có ràng buộc giấy phép.
3.1. SDKMAN — cách cài tốt nhất trên macOS/Linux
Bạn sẽ cần nhiều phiên bản Java cùng lúc (dự án cũ dùng Java 8, dự án mới dùng 21).
SDKMAN quản lý việc đó — tương đương pyenv của Python hay nvm
của Node.
$ curl -s "https://get.sdkman.io" | bash
$ source "$HOME/.sdkman/bin/sdkman-init.sh"
# Xem các bản Java có sẵn (danh sách rất dài)
$ sdk list java
# Cài Java 21 LTS bản Temurin
$ sdk install java 21.0.5-tem
# Cài thêm bản khác để chuyển qua lại
$ sdk install java 17.0.13-tem
$ sdk install java 8.0.432-tem
# Chuyển bản — CHỈ trong phiên terminal hiện tại
$ sdk use java 17.0.13-tem
# Đặt bản mặc định cho mọi phiên
$ sdk default java 21.0.5-tem
# Kiểm tra
$ java -version
openjdk version "21.0.5" 2024-10-15 LTS
OpenJDK Runtime Environment Temurin-21.0.5+11 (build 21.0.5+11-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.5+11 (build 21.0.5+11-LTS, mixed mode)
.sdkmanrc$ cd du-an-cua-toi
$ sdk env init # tạo tệp .sdkmanrc ghi bản hiện tại
$ cat .sdkmanrc
java=21.0.5-tem
$ sdk env # chuyển sang bản ghi trong .sdkmanrc
Commit .sdkmanrc vào git để cả nhóm dùng đúng một phiên bản. Bật
sdkman_auto_env=true trong ~/.sdkman/etc/config để SDKMAN tự chuyển mỗi khi
bạn cd vào thư mục.
3.2. Windows
# Cách 1: winget (có sẵn trên Windows 11)
> winget install EclipseAdoptium.Temurin.21.JDK
# Cách 2: Scoop (quản lý gói kiểu Unix)
> scoop bucket add java
> scoop install temurin21-jdk
# Kiểm tra
> java -version
> javac -version
# Nếu lệnh không nhận: kiểm tra biến môi trường
> echo $env:JAVA_HOME
> echo $env:PATH
JAVA_HOME — biến môi trường hay gây rắc rối
Nhiều công cụ (Maven, Gradle, Tomcat, IDE) không tìm Java qua PATH mà qua biến
JAVA_HOME. Triệu chứng kinh điển: java -version báo 21 nhưng Maven lại build
bằng Java 8, hoặc báo JAVA_HOME is not set.
# Kiểm tra hai thứ này có khớp nhau không
$ echo $JAVA_HOME
$ which java
# SDKMAN tự đặt JAVA_HOME đúng — đây là lý do lớn để dùng nó
$ sdk use java 21.0.5-tem && echo $JAVA_HOME
/Users/ban/.sdkman/candidates/java/21.0.5-tem
4. Hello World — và những quy tắc bắt buộc
public class Hello {
public static void main(String[] args) {
System.out.println("Xin chào Java!");
}
}
Bốn dòng, nhưng có tới sáu quy tắc đang được áp dụng cùng lúc. Hãy mổ xẻ từng phần — mỗi phần sẽ được đào sâu ở các chương sau, giờ chỉ cần biết nó ở đó vì lý do gì.
| Thành phần | Nghĩa là gì | Bỏ đi thì sao | Học kỹ ở |
|---|---|---|---|
public class Hello | Khai báo một class tên Hello, ai cũng truy cập được | Java bắt buộc mọi code phải nằm trong class | Chương 4 |
public (ở method) | JVM ở ngoài class phải gọi được main | main không tìm thấy → không chạy | Chương 4 |
static | Thuộc về class, không cần tạo object mới gọi được | JVM sẽ phải tạo object trước — mà nó không biết tạo thế nào | Chương 4 |
void | Phương thức không trả về giá trị | Chữ ký sai → JVM không nhận | Chương 4 |
String[] args | Mảng tham số dòng lệnh | Chữ ký sai → JVM không nhận | Chương 3 |
System.out.println | Ghi một dòng ra luồng đầu ra chuẩn | — | Chương 3 |
Vì sao main phải là public static void main(String[])
Đây là hợp đồng giữa bạn và JVM. Khi bạn gõ java Hello, JVM nạp class
Hello rồi tìm đúng một phương thức có chữ ký này để gọi. Nó phải là
static vì lúc đó chưa có object nào tồn tại — JVM không thể tạo instance
của Hello khi chưa biết constructor nào phù hợp. Nó phải public vì JVM gọi từ
bên ngoài. Sai một chữ, bạn nhận:
Error: Main method not found in class Hello, please define the main method as:
public static void main(String[] args)
Từ Java 21, bạn có thể bỏ bớt (JEP 445 — tính năng xem trước): class ẩn danh và
void main() không tham số. Nhưng chữ ký đầy đủ vẫn là thứ bạn gặp ở 99% code thật.
# Class là "Hello" nhưng tệp đặt tên "hello.java"
$ javac hello.java
hello.java:1: error: class Hello is public, should be declared
in a file named Hello.java
public class Hello {
^
Quy tắc: một tệp .java chứa tối đa một class public, và tên
tệp phải trùng khít tên class đó — kể cả chữ hoa chữ thường. Các class không public thì
được đặt chung tệp thoải mái. Đây là ràng buộc của trình biên dịch, không phải quy ước —
khác hẳn Python và JavaScript, nơi tên tệp và tên class hoàn toàn độc lập.
5. Biên dịch và chạy
$ ls
Hello.java
# Bước 1: biên dịch — sinh ra Hello.class
$ javac Hello.java
$ ls
Hello.class Hello.java
# Bước 2: chạy — LƯU Ý: KHÔNG có đuôi .class
$ java Hello
Xin chào Java!
# ❌ Sai — đây là lỗi kinh điển của người mới
$ java Hello.class
Error: Could not find or load main class Hello.class
java Hello chứ không phải java Hello.class
Vì tham số bạn truyền cho lệnh java không phải tên tệp mà là tên đủ điều
kiện của class (fully-qualified class name). JVM tự tìm tệp .class tương ứng trên
classpath. Với class nằm trong package com.vidu, bạn phải gõ đầy đủ:
java com.vidu.Hello — dùng dấu chấm, không phải dấu gạch chéo.
# Từ Java 11: chạy thẳng .java, không cần javac
$ java Hello.java
Xin chào Java!
# JVM biên dịch trong bộ nhớ, KHÔNG sinh ra tệp .class
$ ls
Hello.java # không có Hello.class
Chế độ một tệp rất tiện để học và viết script, nhưng chỉ dùng được khi toàn bộ chương trình nằm gọn trong
một tệp. Dự án nhiều tệp vẫn phải qua javac hoặc — thực tế hơn — qua công cụ build.
# Đưa tệp .class vào thư mục riêng, không lẫn với mã nguồn
$ javac -d out src/Hello.java
$ java -cp out Hello
# Biên dịch nhiều tệp cùng lúc
$ javac -d out src/*.java
# Bật cảnh báo đầy đủ — nên dùng khi học
$ javac -Xlint:all -d out src/*.java
# Chỉ định phiên bản đích (biên dịch bằng JDK 21, chạy được trên Java 17)
$ javac --release 17 -d out src/*.java
# Chỉ định bảng mã tệp nguồn — QUAN TRỌNG với tiếng Việt
$ javac -encoding UTF-8 -d out src/*.java
Trên Windows, bảng mã mặc định của javac có thể không phải UTF-8, khiến chuỗi tiếng Việt
trong mã nguồn biến thành ký tự rác. Luôn thêm -encoding UTF-8, hoặc từ Java 18 trở đi thì
UTF-8 đã là mặc định (JEP 400). Nếu ký tự vẫn sai khi in ra terminal, đó lại là vấn đề của
terminal chứ không phải Java — thử chcp 65001 trên Windows.
6. Nhìn tận mắt bytecode với javap
Bytecode không phải khái niệm trừu tượng — bạn xem được nó ngay bây giờ. Đây là cách nhanh nhất để hiểu "JVM thực sự chạy cái gì", và nó giải thích được rất nhiều hành vi kỳ lạ của Java mà chúng ta sẽ gặp ở các chương sau.
$ javac Hello.java
$ javap -c Hello
Compiled from "Hello.java"
public class Hello {
public Hello(); // ← constructor mặc định, javac TỰ THÊM
Code:
0: aload_0 // nạp 'this' lên ngăn xếp
1: invokespecial #1 // gọi Object.<init>() — constructor lớp cha
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #7 // lấy trường System.out
3: ldc #13 // nạp hằng chuỗi "Xin chào Java!"
5: invokevirtual #15 // gọi println(String)
8: return
}
Ba điều bytecode vừa tiết lộ
- JVM là máy ngăn xếp. Không có thanh ghi như CPU thật. Mọi thao tác đều là "đẩy giá trị lên ngăn xếp" rồi "lấy ra và xử lý". Đây là lý do bytecode chạy được trên mọi kiến trúc CPU — nó không giả định gì về phần cứng.
-
javactự thêm constructor. Bạn không viết constructor nào, nhưngpublic Hello()vẫn xuất hiện. Đó là constructor mặc định — Chương 4 sẽ nói kỹ, kể cả trường hợp nó không được thêm. -
Chuỗi nằm trong "constant pool". Lệnh
ldc #13nạp hằng số ở vị trí 13 của bảng hằng. Đây chính là nền tảng của String pool — thứ giải thích vì sao"abc" == "abc"trả vềtruecònnew String("abc") == "abc"lại trảfalse. Chương 2 sẽ mổ xẻ.
$ javap Hello # chỉ chữ ký public — xem API của class
$ javap -p Hello # kèm cả thành viên private
$ javap -c Hello # kèm bytecode
$ javap -v Hello # đầy đủ: constant pool, phiên bản class file...
# Xem API của class trong thư viện chuẩn — không cần tải mã nguồn
$ javap java.util.ArrayList
$ javap java.lang.String
7. JShell — thử nghiệm không cần class
Từ Java 9, JDK có sẵn một REPL. Không cần tạo class, không cần main, không cần biên dịch —
gõ biểu thức là thấy kết quả ngay. Đây là công cụ tốt nhất để kiểm chứng nhanh một nghi vấn về ngôn ngữ.
$ jshell
| Welcome to JShell -- Version 21.0.5
| For an introduction type: /help intro
jshell> 2 + 3 * 4
$1 ==> 14
jshell> String ten = "Việt Anh"
ten ==> "Việt Anh"
jshell> ten.toUpperCase()
$3 ==> "VIỆT ANH"
jshell> var ds = new java.util.ArrayList<String>()
ds ==> []
jshell> ds.add("a"); ds.add("b"); ds
ds ==> [a, b]
jshell> int tong(int a, int b) { return a + b; } // định nghĩa method ở cấp cao nhất!
| created method tong(int,int)
jshell> tong(3, 4)
$8 ==> 7
jshell> 0.1 + 0.2 // kiểm chứng nghi vấn về số thực
$9 ==> 0.30000000000000004
jshell> /vars // liệt kê biến đã khai báo
jshell> /methods // liệt kê method đã định nghĩa
jshell> /imports // xem import mặc định
jshell> /list // xem lại mọi lệnh đã gõ
jshell> /save bai.jsh // lưu phiên làm việc ra tệp
jshell> /exit
Xuyên suốt sub-pillar này, mỗi khi gặp một khẳng định về hành vi của Java ("Integer nhỏ
được cache", "HashMap không giữ thứ tự", "chia số nguyên thì cắt phần thập phân"),
hãy mở JShell và tự kiểm chứng. Mất năm giây và bạn nhớ được lâu hơn nhiều so với đọc
suông. JShell còn tự động import sẵn java.util.*, java.io.*,
java.math.* và vài package khác nên bạn gõ ít hơn.
8. package — không gian tên và cấu trúc thư mục
Dự án thật có hàng trăm class. package nhóm chúng lại và tránh trùng tên — hai thư viện đều
có class Logger vẫn sống chung được vì chúng ở
org.slf4j.Logger và java.util.logging.Logger.
package com.vidu.quanly; // PHẢI là câu lệnh đầu tiên (sau comment)
import java.util.List; // nhập một class cụ thể
import java.util.ArrayList;
// import java.util.*; // nhập cả package — tránh dùng, xem callout
public class Main {
public static void main(String[] args) {
List<String> ds = new ArrayList<>();
ds.add("Xin chào");
System.out.println(ds);
}
}
Cấu trúc thư mục bắt buộc phải phản chiếu tên package — mỗi dấu chấm là một cấp thư mục:
du-an/
└── src/
└── com/
└── vidu/
└── quanly/
├── Main.java // package com.vidu.quanly
├── SinhVien.java // package com.vidu.quanly
└── kho/
└── SinhVienDao.java // package com.vidu.quanly.kho
$ javac -d out src/com/vidu/quanly/*.java
$ find out -name "*.class"
out/com/vidu/quanly/Main.class # javac tự tạo cây thư mục theo package
out/com/vidu/quanly/SinhVien.class
# Chạy: tên ĐẦY ĐỦ, dùng dấu chấm
$ java -cp out com.vidu.quanly.Main
Xin chào
# ❌ Sai — thiếu package
$ java -cp out Main
Error: Could not find or load main class Main
| Quy ước | Ví dụ | Lý do |
|---|---|---|
| Toàn chữ thường | com.vidu.quanly | Tránh xung đột với tên class (PascalCase) và với hệ tệp không phân biệt hoa thường |
| Tên miền đảo ngược | Công ty vidu.com → com.vidu | Đảm bảo duy nhất toàn cầu — không ai khác sở hữu tên miền đó |
| Không dùng từ khoá | Không đặt com.vidu.new | new, int, class… là từ khoá của ngôn ngữ |
| Nhóm theo tính năng | ...donhang, ...thanhtoan | Dễ tìm hơn nhóm theo tầng kỹ thuật (...dao, ...service) |
import java.util.*;
Nó không làm chương trình chậm hơn (trình biên dịch chỉ nạp thứ thực sự dùng), nhưng gây hai
vấn đề thật: (1) người đọc không biết List đến từ đâu —
java.util.List hay java.awt.List? (2) xung đột tên: nếu bạn
import cả java.util.* và java.sql.*, chữ Date trở nên nhập nhằng
và trình biên dịch báo lỗi. IDE tự sinh import cụ thể chỉ với một phím tắt — không có lý do gì dùng
dấu sao.
9. Maven và Gradle — vì sao không thể chỉ dùng javac
javac đủ cho vài tệp. Dự án thật cần: tải thư viện phụ thuộc (và phụ thuộc của thư
viện đó), biên dịch theo thứ tự đúng, chạy test, đóng gói JAR, quản lý phiên bản. Đó là việc của
công cụ build.
| Maven | Gradle | |
|---|---|---|
| Tệp cấu hình | pom.xml (XML) | build.gradle.kts (Kotlin) hoặc .gradle (Groovy) |
| Triết lý | Quy ước hơn cấu hình — làm theo chuẩn thì rất gọn | Kịch bản linh hoạt — muốn gì cũng viết được |
| Cú pháp | Dài dòng nhưng đoán được | Ngắn gọn nhưng cần biết ngôn ngữ kịch bản |
| Tốc độ | Chậm hơn | Nhanh hơn (build tăng dần, daemon, cache) |
| Phổ biến ở | Doanh nghiệp, Spring Boot, thư viện | Android (mặc định), dự án đa module lớn |
| Nên học trước | Maven — đơn giản hơn, tài liệu nhiều hơn | Học sau, khi cần |
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<!-- Toạ độ định danh dự án: groupId:artifactId:version -->
<groupId>com.vidu</groupId>
<artifactId>quan-ly-sinh-vien</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Thư viện test — chỉ dùng khi chạy test, không đóng gói vào JAR -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
quan-ly-sinh-vien/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/ // mã nguồn chính
│ │ │ └── com/vidu/quanly/Main.java
│ │ └── resources/ // tệp cấu hình, dữ liệu tĩnh
│ │ └── application.properties
│ └── test/
│ ├── java/ // mã nguồn test
│ │ └── com/vidu/quanly/MainTest.java
│ └── resources/
└── target/ // Maven sinh ra — thêm vào .gitignore
├── classes/
└── quan-ly-sinh-vien-1.0.0.jar
$ mvn compile # biên dịch src/main/java → target/classes
$ mvn test # biên dịch + chạy test
$ mvn package # biên dịch + test + đóng gói JAR
$ mvn clean # xoá thư mục target/
$ mvn clean package -DskipTests # build nhanh, bỏ qua test
# Các pha CHẠY TUẦN TỰ: mvn package tự chạy compile và test trước.
# Đây là ý nghĩa của "vòng đời" (lifecycle) trong Maven.
~/.m2 và Maven Central
Khi bạn khai báo một dependency, Maven tải nó từ Maven Central
(central.sonatype.com) về kho cục bộ ~/.m2/repository/, rồi mọi dự án trên máy
dùng chung bản đã tải. Đây là lý do lần build đầu tiên rất lâu còn các lần sau thì nhanh. Muốn build
hoàn toàn ngoại tuyến: mvn -o package. Muốn xoá cache một thư viện lỗi: xoá thư mục tương
ứng trong ~/.m2/repository/.
10. Phiên bản Java — LTS nghĩa là gì
Từ 2018, Java phát hành phiên bản mới mỗi 6 tháng (tháng 3 và tháng 9). Nhưng chỉ một số bản được đánh dấu LTS (Long-Term Support) — có bản vá bảo mật trong nhiều năm. Dự án production gần như luôn chọn LTS.
| Phiên bản | Phát hành | Loại | Tính năng đáng nhớ |
|---|---|---|---|
| Java 8 | 2014 | LTS (rất cũ) | Lambda, Stream API, Optional, java.time — bước ngoặt lớn nhất lịch sử Java |
| Java 11 | 2018 | LTS | var cho tham số lambda, chạy tệp .java trực tiếp, HTTP Client mới |
| Java 17 | 2021 | LTS | sealed class, pattern matching cho instanceof, record chính thức, text block |
| Java 21 | 2023 | LTS ⭐ | Virtual thread, pattern matching cho switch, record pattern, sequenced collection |
| Java 25 | 2025 | LTS | LTS mới nhất — nâng cấp dần các tính năng của 21 |
Java 21 là bản LTS được triển khai rộng rãi nhất hiện nay và chứa đủ mọi tính năng hiện đại quan trọng (record, sealed, pattern matching, virtual thread, text block). Mọi code trong 14 chương đều chạy trên Java 21 trở lên. Nơi nào dùng tính năng chỉ có ở bản mới, chúng ta ghi rõ phiên bản tối thiểu — để bạn biết code của mình có chạy được trên hệ thống công ty (nhiều nơi vẫn ở Java 8 hoặc 11) hay không.
11. IDE — dùng gì để viết
| Công cụ | Chi phí | Điểm mạnh | Phù hợp |
|---|---|---|---|
| IntelliJ IDEA Community ⭐ | Miễn phí | Gợi ý code và refactor tốt nhất trong ngành Java | Khuyên dùng cho mọi mức |
| IntelliJ IDEA Ultimate | Trả phí (miễn phí cho sinh viên) | Thêm Spring, JPA, công cụ database, HTTP client | Làm web/doanh nghiệp |
| VS Code + Extension Pack for Java | Miễn phí | Nhẹ, quen thuộc nếu bạn đến từ JS/Python | Dự án nhỏ, đa ngôn ngữ |
| Eclipse | Miễn phí | Lâu đời, nhiều plugin doanh nghiệp | Dự án kế thừa |
| macOS | Windows/Linux | Làm gì |
|---|---|---|
⌘ B | Ctrl B | Nhảy tới định nghĩa — kể cả vào mã nguồn thư viện chuẩn |
⌥ ↵ | Alt Enter | Sửa nhanh: tự import, tạo method còn thiếu, chuyển sang cách viết hiện đại |
⇧⇧ | Shift Shift | Tìm mọi thứ: tệp, class, hành động, cài đặt |
Mẹo học rất hiệu quả: ⌘ B vào ArrayList, HashMap,
String để đọc mã nguồn thật của thư viện chuẩn. Nó được viết bởi những người giỏi
nhất và có comment giải thích rất kỹ.
12. Cheat sheet chương 1
| Việc cần làm | Lệnh |
|---|---|
| Kiểm tra phiên bản | java -version · javac -version |
| Biên dịch một tệp | javac Hello.java |
| Biên dịch vào thư mục riêng | javac -d out src/*.java |
| Chạy | java -cp out com.vidu.Main (không có .class) |
| Chạy thẳng một tệp (11+) | java Hello.java |
| Xem bytecode | javap -c Hello |
| Xem API một class | javap java.util.ArrayList |
| REPL | jshell |
| Cài / đổi phiên bản Java | sdk install java 21.0.5-tem · sdk use java … |
| Đóng gói JAR | mvn package → target/*.jar |
| Chạy JAR | java -jar ten.jar |
| Bảng mã UTF-8 | javac -encoding UTF-8 … (mặc định từ Java 18) |
javac ra bytecode độc lập nền tảng, JVM mới là thứ phụ thuộc hệ điều hành.
(3) Tên tệp phải trùng tên class public, và cây thư mục phải khớp package.
Bài tập
Dựng môi trường và xác minh
Cài SDKMAN (hoặc winget trên Windows), rồi thực hiện đủ các bước sau và ghi lại kết quả:
- Cài ba bản Java: 8, 17, 21 (Temurin).
- Chạy
java -versionvới từng bản, ghi lại chuỗi phiên bản đầy đủ. - In
$JAVA_HOMEsau mỗi lầnsdk use— xác nhận nó đổi theo. - Tạo tệp
.sdkmanrcghim Java 21 cho một thư mục dự án. - Viết
Hello.java, thử biên dịch bằng Java 21 với--release 8, rồi chạy nó bằng JVM 8. Nó chạy được không?
Gợi ý
Câu 5 minh hoạ tính tương thích ngược của Java: bytecode biên dịch cho phiên bản
thấp hơn chạy được trên JVM mới hơn, nhưng chiều ngược lại thì không. Thử biên dịch
bằng Java 21 không có --release rồi chạy bằng JVM 8 — bạn sẽ gặp
UnsupportedClassVersionError kèm hai con số "class file version". Hãy tra xem
version 65 và version 52 tương ứng với Java mấy.
Đọc bytecode của ba chương trình
Viết ba tệp sau, biên dịch, rồi chạy javap -c với từng tệp:
// A.java — cộng hai hằng số
public class A { public static void main(String[] a) { int x = 2 + 3; } }
// B.java — nối hai chuỗi trong vòng lặp
public class B {
public static void main(String[] a) {
String s = "";
for (int i = 0; i < 3; i++) s = s + i;
}
}
// C.java — dùng StringBuilder
public class C {
public static void main(String[] a) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 3; i++) sb.append(i);
}
}
Trả lời:
- Trong
A, phép2 + 3có xuất hiện trong bytecode không? Vì sao? - Trong
B, phéps + iđược dịch thành cái gì? Nó nằm trong hay ngoài vòng lặp? - So sánh bytecode của
BvàC— chúng khác nhau ở đâu, và điều đó nói gì về hiệu năng?
Gợi ý
Câu 1: tìm chữ iconst_5 hoặc bipush 5 — trình biên dịch đã tính sẵn
(constant folding).
Câu 2-3: bạn sẽ thấy B tạo một StringBuilder mới ở mỗi vòng lặp
(hoặc gọi makeConcatWithConstants qua invokedynamic, tuỳ phiên bản), còn
C chỉ tạo một lần. Đây chính là lý do quy tắc "đừng nối chuỗi bằng +
trong vòng lặp" tồn tại. Chương 2 sẽ quay lại vấn đề này với số đo cụ thể.
Dự án nhiều package đầu tiên
Tạo cấu trúc sau và làm cho nó chạy được chỉ bằng javac và java (chưa dùng Maven):
src/
└── com/vidu/cuahang/
├── Main.java // có main(), gọi hai class kia
├── model/SanPham.java // class có tên, giá; có toString()
└── tienich/DinhDang.java // method static: đinhDangTien(long) → "1.500.000đ"
- Biên dịch tất cả vào
out/bằng một lệnhjavac. - Chạy bằng
java -cp out com.vidu.cuahang.Main. - Đóng gói thành JAR chạy được:
java -jar cuahang.jarphải hoạt động.
Gợi ý
Biên dịch đệ quy: find src -name "*.java" | xargs javac -d out (macOS/Linux) hoặc
javac -d out $(find src -name "*.java").
Để JAR chạy được, tệp manifest phải khai báo lớp chính:
jar --create --file cuahang.jar --main-class com.vidu.cuahang.Main -C out .
Kiểm chứng bằng cách giải nén và đọc META-INF/MANIFEST.MF.
Định dạng tiền: String.format("%,dđ", tien) — dấu phẩy cho phân cách hàng nghìn.
Chuyển dự án sang Maven
Lấy dự án ở bài trên và tái cấu trúc theo chuẩn Maven:
- Chuyển mã nguồn vào
src/main/java/. - Viết
pom.xmlvớimaven.compiler.release = 21và encoding UTF-8. - Thêm
maven-jar-pluginđể cấu hìnhmainClass. - Chạy
mvn clean packagerồijava -jar target/*.jar. - Thêm dependency
org.apache.commons:commons-lang3:3.17.0và dùng thử một hàm của nó (ví dụStringUtils.capitalize). Quan sát Maven tải về~/.m2.
Gợi ý
JAR do maven-jar-plugin tạo không chứa dependency — chạy sẽ gặp
NoClassDefFoundError cho StringUtils. Đây là điểm quan trọng cần hiểu:
muốn một JAR chạy độc lập ("fat JAR"/"uber JAR") phải dùng
maven-shade-plugin hoặc maven-assembly-plugin. Chương 14 sẽ làm kỹ.
Chạy mvn dependency:tree để xem cây phụ thuộc.
Kiểm chứng bằng JShell
Mở JShell và tự kiểm chứng năm khẳng định sau. Với mỗi cái, ghi lại kết quả thật và giải thích ngắn gọn:
jshell> 7 / 2
jshell> 7.0 / 2
jshell> 0.1 + 0.2 == 0.3
jshell> Integer a = 127, b = 127; a == b
jshell> Integer c = 128, d = 128; c == d
jshell> Integer.MAX_VALUE + 1
jshell> "abc" == "abc"
jshell> new String("abc") == "abc"
Đừng lo nếu chưa giải thích được — chương 2 sẽ trả lời tất cả. Mục đích bài này là để bạn tự mình nhìn thấy kết quả bất ngờ trước, rồi mới đọc lời giải thích. Ghi lại phỏng đoán của bạn ngay bây giờ và đối chiếu sau khi học xong chương 2.
Gợi ý
Bốn hiện tượng bạn vừa gặp có tên riêng: chia số nguyên, sai số dấu phẩy động IEEE 754, bộ nhớ đệm Integer (từ −128 đến 127), tràn số, và String pool. Google từng cái sau khi học xong chương 2 để củng cố.
Quiz
JDK, JRE, JVM khác nhau thế nào? Lập trình viên cần cài cái nào?
Xem đáp án
Chúng lồng vào nhau: JDK ⊃ JRE ⊃ JVM.
JVM là máy ảo — nạp và thực thi bytecode, quản lý bộ nhớ, chạy garbage collector và
trình biên dịch JIT. Một mình nó không chạy được ứng dụng vì thiếu thư viện.
JRE = JVM + thư viện chuẩn (java.lang, java.util,
java.io…). Đủ để chạy ứng dụng, không đủ để biên dịch.
JDK = JRE + công cụ phát triển (javac, javap,
jar, jshell, javadoc).
Lập trình viên cài JDK. Lưu ý từ Java 11, JRE không còn được phát hành riêng —
cách phân phối hiện đại là dùng jlink tạo runtime tối giản đóng gói kèm ứng dụng.
"Write once, run anywhere" hoạt động nhờ cơ chế nào? Phần nào của Java không đa nền tảng?
Xem đáp án
Nhờ tầng trung gian bytecode. javac không biên dịch ra mã máy của một
CPU cụ thể mà ra bytecode — tập lệnh của một máy ảo trừu tượng, giống nhau trên mọi nền tảng. Tệp
.class mang từ macOS sang Linux hay Windows đều chạy được, không cần biên dịch lại.
Phần không đa nền tảng chính là JVM: mỗi hệ điều hành và kiến trúc CPU cần một bản JVM riêng (macOS ARM64, Linux x86-64, Windows x86-64…). Nói cách khác, gánh nặng đa nền tảng được chuyển từ lập trình viên ứng dụng sang nhà cung cấp JVM — và đó chính là ý tưởng thiên tài của thiết kế này.
Vì sao main phải là public static void main(String[] args)?
Xem đáp án
Đây là hợp đồng cố định giữa chương trình và JVM. Từng phần có lý do riêng:
public— JVM gọi phương thức này từ bên ngoài class, nên nó phải truy cập được.static— lý do quan trọng nhất: tại thời điểm khởi động chưa có object nào tồn tại. JVM không biết phải gọi constructor nào với tham số gì để tạo instance, nên phương thức phải thuộc về class chứ không phải instance.void— mã thoát của chương trình được đặt quaSystem.exit(n), không qua giá trị trả về.String[] args— nơi JVM đưa các tham số dòng lệnh vào.
Sai bất kỳ phần nào, bạn nhận Error: Main method not found in class …. Tên tham số
(args) thì đổi tuỳ ý, và String... args cũng hợp lệ vì varargs biên dịch
ra đúng một mảng.
Vì sao java Hello.class báo lỗi trong khi tệp Hello.class rõ ràng đang nằm đó?
Xem đáp án
Vì tham số của lệnh java không phải tên tệp mà là tên đủ điều
kiện của class. JVM nhận tên đó, rồi tự đi tìm tệp .class tương ứng trên
classpath. Khi bạn gõ java Hello.class, JVM hiểu là "tìm class tên
class trong package Hello" — dấu chấm là dấu phân cách package!
Hệ quả: với class trong package, phải gõ tên đầy đủ với dấu chấm —
java -cp out com.vidu.Main, chứ không phải
java out/com/vidu/Main.class.
Ngoại lệ duy nhất: từ Java 11, java Hello.java (đuôi .java, không phải
.class) là chế độ chạy một tệp — JVM biên dịch trong bộ nhớ rồi chạy luôn.
Một tệp .java chứa được bao nhiêu class? Quy tắc đặt tên tệp là gì?
Xem đáp án
Chứa được bao nhiêu class cũng được, nhưng tối đa một class
public — và nếu có, tên tệp phải trùng khít tên class đó (phân biệt hoa
thường).
public class Hello { ... } // public → tên tệp phải là Hello.java
class TroGiup { ... } // không public → đặt chung tệp được
class CauHinh { ... } // cũng được
Sau khi biên dịch, javac sinh ra ba tệp .class riêng biệt
— mỗi class một tệp. Lý do của quy tắc này là kỹ thuật: trình biên dịch cần tìm được mã nguồn của
một class chỉ từ tên nó, không phải quét toàn bộ dự án.
Trong dự án thật, quy ước là mỗi class một tệp cho dễ tìm — nhiều class chung tệp
chỉ dùng cho class trợ giúp rất nhỏ, chỉ dùng nội bộ.
Java là ngôn ngữ biên dịch hay thông dịch?
Xem đáp án
Cả hai — và đó là câu trả lời chính xác, không phải né tránh.
- Biên dịch:
javacdịch mã nguồn sang bytecode. Bước này kiểm tra kiểu, cú pháp, và các lỗi khác trước khi chương trình chạy — đây là lý do Java bắt được rất nhiều lỗi mà Python/JavaScript phải đợi tới lúc chạy mới biết. - Thông dịch: JVM đọc và thực thi từng lệnh bytecode.
- Biên dịch lần hai (JIT): JVM theo dõi những phương thức được gọi nhiều, dịch chúng sang mã máy và tối ưu dựa trên dữ liệu chạy thực tế.
Hệ quả thực tế của mô hình lai này: chương trình Java khởi động chậm (JVM phải nạp class, thông dịch trước khi JIT kịp làm việc) nhưng chạy lâu thì rất nhanh — đôi khi ngang C++ ở những đoạn nóng nhất, vì JIT có thông tin mà trình biên dịch AOT không thể có.
LTS là gì? Vì sao dự án production nên chọn bản LTS?
Xem đáp án
LTS = Long-Term Support — phiên bản được cam kết vá lỗi và vá bảo mật trong nhiều năm (thường 5-8 năm tuỳ nhà cung cấp).
Từ 2018, Java ra bản mới mỗi 6 tháng, nhưng bản không phải LTS chỉ được hỗ trợ đúng 6 tháng — hết hạn là không còn bản vá bảo mật. Với một hệ thống ngân hàng chạy 10 năm, nâng cấp Java hai lần mỗi năm là không khả thi, và chạy phiên bản không còn được vá là rủi ro bảo mật nghiêm trọng.
Các bản LTS: 8 (2014), 11 (2018), 17 (2021), 21 (2023), 25 (2025). Bản không-LTS (18, 19, 20, 22…) dùng để thử nghiệm tính năng mới, không dành cho production.
Vì sao cần Maven/Gradle khi đã có javac?
Xem đáp án
javac chỉ làm đúng một việc: dịch .java thành .class. Dự án
thật cần nhiều hơn thế:
- Quản lý phụ thuộc bắc cầu — bạn khai báo cần thư viện A; A lại cần B và C; B cần D phiên bản 2.0 còn C cần D phiên bản 1.5. Giải quyết xung đột này bằng tay là bất khả thi.
- Thứ tự biên dịch — hàng trăm tệp phụ thuộc lẫn nhau.
- Vòng đời chuẩn hoá —
compile→test→package→install, giống nhau ở mọi dự án nên ai cũng biết cách build. - Build lặp lại được — cùng một lệnh cho kết quả giống nhau trên máy bạn, máy đồng nghiệp, và máy chủ CI.
- Tích hợp — chạy test, đo độ phủ, kiểm tra chất lượng, đóng gói, publish, tất cả trong một quy trình.
Nói ngắn gọn: javac là trình biên dịch, Maven/Gradle là hệ thống xây dựng.
So sánh: gcc ↔ make/CMake, hoặc tsc ↔ npm scripts.
Tổng kết
Sau chương 1, bạn đã nắm:
- Ba tầng: JDK ⊃ JRE ⊃ JVM; lập trình viên cài JDK.
- Chuỗi thực thi: mã nguồn →
javac→ bytecode → JVM (thông dịch + JIT) → mã máy. - Mô hình lai giải thích cả điểm yếu (khởi động chậm) lẫn điểm mạnh (chạy lâu thì nhanh) của Java.
- Cài đặt: SDKMAN + Temurin; tránh Oracle JDK vì vấn đề giấy phép; ghim phiên bản bằng
.sdkmanrc. - Hợp đồng
main:public static void main(String[])— và lý do của từng từ khoá. - Quy tắc tệp: tên tệp trùng tên class
public; cây thư mục khớppackage. javapđể nhìn bytecode — công cụ giải thích nhiều hành vi kỳ lạ của Java.- JShell để kiểm chứng nhanh mọi nghi vấn về ngôn ngữ.
- Maven:
pom.xml, cấu trúcsrc/main/java, vòng đời build. - LTS: 8, 11, 17, 21, 25 — production luôn chọn LTS.
Kết nối
- Chương 2 (Biến & kiểu dữ liệu) — giải thích mọi kết quả bất ngờ bạn vừa thấy trong bài tập JShell: chia số nguyên, IEEE 754, cache
Integer, String pool. - Chương 4 (OOP cơ bản) — vì sao mọi thứ phải nằm trong class;
staticthực sự nghĩa là gì; constructor mặc định màjavapvừa cho bạn thấy. - Chương 12 (Đa luồng) — vùng nhớ của JVM (Heap, Stack, Metaspace) và garbage collector.
- Chương 14 (Kiểm thử & build) — Maven đầy đủ, fat JAR, JUnit, CI.
- Python Chương 1 — đối chiếu: Python cũng "biên dịch" ngầm ra bytecode
.pyc, nhưng không có JIT trong CPython và không kiểm tra kiểu lúc biên dịch. - JavaScript Chương 1 — V8 cũng dùng JIT, nhưng biên dịch từ mã nguồn chứ không qua bước bytecode do lập trình viên tạo ra.
- Trụ cột CLI — biến môi trường
PATH,JAVA_HOME, chuyển hướng đầu ra. - Trụ cột DevOps — đóng gói ứng dụng Java vào Docker, chọn ảnh nền JRE tối giản.