Chương 01 · Hello Java

JDK, JVM và bộ công cụ Java

Trước khi viết dòng code Java đầu tiên, có một câu hỏi phải trả lời dứt điểm: JDK, JRE, JVM khác nhau chỗ nào? Đây là thứ gây rối cho gần như mọi người mới, và cũng là chìa khoá để hiểu vì sao Java hoạt động theo cách của nó — vì sao có bước biên dịch, vì sao "write once, run anywhere" là thật, và vì sao chương trình Java khởi động chậm nhưng chạy lâu lại nhanh. Chương này dựng xong môi trường, chạy được chương trình đầu tiên, và cho bạn thấy tận mắt cái bytecode mà JVM thực sự đọc.

Độ dài: ~1400 dòng Bài tập: 5 Quiz: 8 Prerequisites: Không có
🎯 Mục tiêu chương
  • 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ỗ.

1. Mã nguồn
Hello.java
bạn viết
2. Biên dịch
javac
một lần duy nhất
3. Bytecode
Hello.class
độc lập nền tảng
4. JVM
java
riêng mỗi OS
5. Mã máy
x86 / ARM
CPU thực thi
Bước 1-3 làm một lần trên máy lập trình viên. Bước 4-5 làm mỗi lần chạy, trên bất kỳ máy nào có JVM. Tệp .class mang từ macOS sang Linux chạy được ngay, không cần biên dịch lại.
🔁 Ba mô hình thực thi
Mô hìnhNgô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ựcVí dụ cụ thểVì sao chọn Java
Ngân hàng & tài chínhHệ 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ệpSpring Boot, Jakarta EEHệ 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ớnHadoop, Spark, Kafka, Elasticsearch, FlinkChạy trên JVM, đa luồng mạnh, quản lý bộ nhớ tự động
AndroidToàn bộ nền tảng AndroidJava 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ìnhIntelliJ 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ứ.

┌─ JDK (Java Development Kit) ────────────────────────────┐ │ Bộ công cụ PHÁT TRIỂN — cái bạn cần cài │ │ │ │ javac ← trình biên dịch (.java → .class) │ │ javadoc ← sinh tài liệu HTML từ comment │ │ jar ← đóng gói nhiều .class thành 1 tệp │ │ jdb ← trình gỡ lỗi │ │ javap ← dịch ngược bytecode để xem │ │ jshell ← REPL tương tác (Java 9+) │ │ │ │ ┌─ JRE (Java Runtime Environment) ────────────────┐ │ │ │ Môi trường CHẠY — đủ để chạy app, không build │ │ │ │ │ │ │ │ Thư viện chuẩn: java.lang, java.util, │ │ │ │ java.io, java.nio, java.time, java.sql... │ │ │ │ │ │ │ │ ┌─ JVM (Java Virtual Machine) ───────────┐ │ │ │ │ │ Máy ảo — thực thi bytecode │ │ │ │ │ │ │ │ │ │ │ │ • Class Loader — nạp .class │ │ │ │ │ │ • Bộ thông dịch bytecode │ │ │ │ │ │ • Trình biên dịch JIT │ │ │ │ │ │ • Garbage Collector │ │ │ │ │ │ • Vùng nhớ: Heap, Stack, Metaspace │ │ │ │ │ └────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────┘
JVMJREJDK
Viết tắt củaJava Virtual MachineJava Runtime EnvironmentJava Development Kit
Là gìMáy ảo thực thi bytecodeJVM + thư viện chuẩnJRE + 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ôngKhôngCó
Ai cần—Người dùng cuối chạy app JavaLập trình viên — chính là bạn
📌 Từ Java 11, JRE không còn phát hành riêng

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ốiDo aiGiấy phépNên chọn khi
Eclipse Temurin ⭐Eclipse AdoptiumMiễn phí hoàn toàn, kể cả thương mạiMặc định — trung lập, cộng đồng lớn nhất
Amazon CorrettoAmazonMiễn phíTriển khai trên AWS
Azul ZuluAzul SystemsMiễn phí (có bản trả phí)Cần hỗ trợ thương mại
Microsoft Build of OpenJDKMicrosoftMiễn phíTriển khai trên Azure
Oracle JDKOracle⚠️ Giấy phép có điều kiện, đã đổi nhiều lầnChỉ khi công ty có hợp đồng với Oracle
GraalVMOracleBản Community miễn phíCần biên dịch ra binary gốc (native image)
⚠️ Cẩn thận với Oracle JDK

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.

Terminal — cài SDKMAN và Java
$ 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)
💡 Ghim phiên bản cho từng dự án với .sdkmanrc
Terminal
$ 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

PowerShell
# 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.

Terminal
# 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

Hello.java
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ầnNghĩa là gìBỏ đi thì saoHọc kỹ ở
public class HelloKhai báo một class tên Hello, ai cũng truy cập đượcJava bắt buộc mọi code phải nằm trong classChương 4
public (ở method)JVM ở ngoài class phải gọi được mainmain không tìm thấy → không chạyChương 4
staticThuộc về class, không cần tạo object mới gọi đượcJVM sẽ phải tạo object trước — mà nó không biết tạo thế nàoChương 4
voidPhương thức không trả về giá trịChữ ký sai → JVM không nhậnChương 4
String[] argsMảng tham số dòng lệnhChữ ký sai → JVM không nhậnChương 3
System.out.printlnGhi 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:

Terminal
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.

⚠️ Quy tắc vàng: tên tệp phải trùng tên class public
Terminal
# 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

Terminal — cách truyền thống, hai bước
$ 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
📌 Vì sao 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.

Terminal — chạy một tệp trực tiếp (Java 11+)
# 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.

Terminal — các cờ hay dùng của javac
# Đư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
🕳️ Tiếng Việt bị lỗi phông khi biên dịch

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.

Terminal — javap -c
$ 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.
  • javac tự thêm constructor. Bạn không viết constructor nào, nhưng public 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 #13 nạ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ề true còn new String("abc") == "abc" lại trả false. Chương 2 sẽ mổ xẻ.
Terminal — các chế độ khác của javap
$ 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ữ.

Terminal
$ 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
💡 Dùng JShell như một phần của việc học

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.

src/com/vidu/quanly/Main.java
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:

Cấu trúc 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
Terminal — biên dịch và chạy code có package
$ 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 ướcVí dụLý do
Toàn chữ thườngcom.vidu.quanlyTrá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ượcCô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.newnew, int, class… là từ khoá của ngôn ngữ
Nhóm theo tính năng...donhang, ...thanhtoanDễ tìm hơn nhóm theo tầng kỹ thuật (...dao, ...service)
⚠️ Tránh 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.

MavenGradle
Tệp cấu hìnhpom.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ọnKịch bản linh hoạt — muốn gì cũng viết được
Cú phápDài dòng nhưng đoán đượcNgắn gọn nhưng cần biết ngôn ngữ kịch bản
Tốc độChậm hơnNhanh hơn (build tăng dần, daemon, cache)
Phổ biến ởDoanh nghiệp, Spring Boot, thư việnAndroid (mặc định), dự án đa module lớn
Nên học trướcMaven — đơn giản hơn, tài liệu nhiều hơnHọc sau, khi cần
pom.xml — dự án Maven tối thiểu
<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>
Cấu trúc thư mục chuẩn Maven — nhớ cấu trúc này
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
Terminal — vòng đời Maven
$ 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.
💡 Kho ~/.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ảnPhát hànhLoạiTính năng đáng nhớ
Java 82014LTS (rất cũ)Lambda, Stream API, Optional, java.time — bước ngoặt lớn nhất lịch sử Java
Java 112018LTSvar cho tham số lambda, chạy tệp .java trực tiếp, HTTP Client mới
Java 172021LTSsealed class, pattern matching cho instanceof, record chính thức, text block
Java 212023LTS ⭐Virtual thread, pattern matching cho switch, record pattern, sequenced collection
Java 252025LTSLTS mới nhất — nâng cấp dần các tính năng của 21
📌 Vì sao sub-pillar này chọn Java 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ạnhPhù hợp
IntelliJ IDEA Community ⭐Miễn phíGợi ý code và refactor tốt nhất trong ngành JavaKhuyên dùng cho mọi mức
IntelliJ IDEA UltimateTrả phí (miễn phí cho sinh viên)Thêm Spring, JPA, công cụ database, HTTP clientLàm web/doanh nghiệp
VS Code + Extension Pack for JavaMiễn phíNhẹ, quen thuộc nếu bạn đến từ JS/PythonDự án nhỏ, đa ngôn ngữ
EclipseMiễn phíLâu đời, nhiều plugin doanh nghiệpDự án kế thừa
💡 Ba phím tắt IntelliJ đáng học ngay ngày đầu
macOSWindows/LinuxLàm gì
⌘ BCtrl BNhảy tới định nghĩa — kể cả vào mã nguồn thư viện chuẩn
⌥ ↵Alt EnterSửa nhanh: tự import, tạo method còn thiếu, chuyển sang cách viết hiện đại
⇧⇧Shift ShiftTì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àmLệnh
Kiểm tra phiên bảnjava -version · javac -version
Biên dịch một tệpjavac Hello.java
Biên dịch vào thư mục riêngjavac -d out src/*.java
Chạyjava -cp out com.vidu.Main (không có .class)
Chạy thẳng một tệp (11+)java Hello.java
Xem bytecodejavap -c Hello
Xem API một classjavap java.util.ArrayList
REPLjshell
Cài / đổi phiên bản Javasdk install java 21.0.5-tem · sdk use java …
Đóng gói JARmvn package → target/*.jar
Chạy JARjava -jar ten.jar
Bảng mã UTF-8javac -encoding UTF-8 … (mặc định từ Java 18)
Ba điều nhớ nhất chương này: (1) JDK ⊃ JRE ⊃ JVM — cài JDK là đủ. (2) 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ả:

  1. Cài ba bản Java: 8, 17, 21 (Temurin).
  2. Chạy java -version với từng bản, ghi lại chuỗi phiên bản đầy đủ.
  3. In $JAVA_HOME sau mỗi lần sdk use — xác nhận nó đổi theo.
  4. Tạo tệp .sdkmanrc ghim Java 21 cho một thư mục dự án.
  5. 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:

ba_chuong_trinh.java
// 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ép 2 + 3 có xuất hiện trong bytecode không? Vì sao?
  • Trong B, phép s + 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 B và 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):

Cấu trúc
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ệnh javac.
  • Chạy bằng java -cp out com.vidu.cuahang.Main.
  • Đóng gói thành JAR chạy được: java -jar cuahang.jar phả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.xml với maven.compiler.release = 21 và encoding UTF-8.
  • Thêm maven-jar-plugin để cấu hình mainClass.
  • Chạy mvn clean package rồi java -jar target/*.jar.
  • Thêm dependency org.apache.commons:commons-lang3:3.17.0 và 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:

Kiểm chứng
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

Q1

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
✓ Đá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.

Q2

"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
✓ Đá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.

Q3

Vì sao main phải là public static void main(String[] args)?

Xem đáp án
✓ Đá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 qua System.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.

Q4

Vì sao java Hello.class báo lỗi trong khi tệp Hello.class rõ ràng đang nằm đó?

Xem đáp án
✓ Đá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.

Q5

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
✓ Đá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).

Hello.java — hợp lệ
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ộ.

Q6

Java là ngôn ngữ biên dịch hay thông dịch?

Xem đáp án
✓ Đá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: javac dị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ó.

Q7

LTS là gì? Vì sao dự án production nên chọn bản LTS?

Xem đáp án
✓ Đá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.

Q8

Vì sao cần Maven/Gradle khi đã có javac?

Xem đáp án
✓ Đá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ớp package.
  • 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úc src/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; static thực sự nghĩa là gì; constructor mặc định mà javap vừ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.