Viết một dòng int x = 10; tưởng tầm thường. Nhưng khi bạn có 2 biến, truyền biến vào method, hay so sánh 2 chuỗi bằng ==, bạn sẽ bắt đầu gặp những hiện tượng "lạ" mà không hiểu mechanism bên dưới thì khó giải thích.
Bài này đi từ cách dùng đơn giản nhất, dần đến cơ chế JVM bên dưới.
1. Biến là gì?
Biến là một cái tên gắn với một ô nhớ giữ một giá trị. Bạn dùng tên để đọc/ghi giá trị trong ô đó.
💡 💡 Cách nhớ
Hình dung dãy ngăn kéo có nhãn:
- Ngăn
tuoichứa số25 - Ngăn
tenchứa chữ"An"
Gọi tên ngăn để lấy món. Mở ngăn, thay món mới — đó là gán lại biến.
2. Cách khai báo cơ bản
Khai báo biến cần 3 thành phần:
- Kiểu (type) — biến chứa loại gì (
int,String,boolean...) - Tên — cách bạn gọi nó (camelCase là convention Java)
- Giá trị ban đầu (optional với field, khuyến khích với local variable)
int tuoi = 25; // so nguyen
String ten = "An"; // chuoi
boolean daDangKy = true; // dung/sai
double luong = 12.5; // so thap phan
Dùng và gán lại:
System.out.println(tuoi); // in: 25
tuoi = 26; // gan lai gia tri moi
System.out.println(tuoi); // in: 26
2.1 Các kiểu hay dùng nhất
| Kiểu | Chứa gì | Ví dụ | Dùng khi |
|---|---|---|---|
int | Số nguyên 32-bit | 25, -7, 0 | Đếm, chỉ số, id nhỏ |
long | Số nguyên 64-bit | 1_000_000_000L | Timestamp, id database |
double | Số thập phân 64-bit | 3.14, -0.5 | Giá tiền, toán tổng quát |
boolean | Đúng / sai | true, false | Flag, điều kiện |
char | 1 ký tự | 'A', '?' | Ký tự đơn lẻ |
String | Chuỗi văn bản | "Hello" | Mọi text, tên, ID text |
2.2 Quy tắc đặt tên
- ✅
tuoi,tenHocSinh,soLuongSanPham— camelCase - ❌
Tuoi(PascalCase — dành cho tên class),ten_hoc_sinh(snake_case — không phải Java convention) - ✅ Bắt đầu bằng chữ hoặc
_hoặc$, không bắt đầu bằng số - ❌ Không dùng keyword (
int,class,return...) làm tên biến - 🧠 Tên mô tả hành động/ý nghĩa, không phải loại —
agetốt hơnintAge
Thử chạy đoạn này (Java 11+):
public class Chao {
public static void main(String[] args) {
String ten = "An";
int tuoi = 25;
boolean daDangKy = true;
System.out.println("Xin chao, " + ten + ", " + tuoi + " tuoi.");
System.out.println("Da dang ky: " + daDangKy);
}
}
java Chao.java
Đến đây bạn đã viết được chương trình nhỏ. Các phần sau giải thích vì sao Java hành xử như vậy.
3. Hai loại biến: Primitive vs Reference
Đây là phân biệt quan trọng nhất của Java — nguồn gốc của hầu hết bẫy mà người mới gặp phải.
3.1 Thử nghiệm nhỏ — đoán kết quả trước khi đọc
// Doan A — 2 bien so
int a = 10;
int b = a;
b = 99;
System.out.println(a); // ?
System.out.println(b); // ?
// Doan B — 2 bien mang
int[] arr1 = {1, 2, 3};
int[] arr2 = arr1;
arr2[0] = 99;
System.out.println(arr1[0]); // ?
System.out.println(arr2[0]); // ?
Kết quả:
- Đoạn A:
a=10,b=99— sửabkhông ảnh hưởnga. Trực giác đúng. - Đoạn B:
arr1[0]=99,arr2[0]=99— cả hai đều bị sửa. Bất ngờ?
3.2 Vì sao khác nhau?
Java có 2 họ kiểu, hành xử khác hẳn ở cấp memory:
| Primitive | Reference | |
|---|---|---|
| Ví dụ | int, double, boolean, char, long, byte, short, float | String, mảng, class tự viết, Integer, List... |
| Biến chứa gì? | Giá trị thật | Địa chỉ tới object nằm nơi khác |
| Copy biến làm gì? | Sao giá trị | Sao địa chỉ |
Có thể null? | ❌ Không | ✅ Có |
| Viết hoa chữ đầu type? | ❌ (int, double) | ✅ (String, Integer) |
💡 💡 Cách nhớ
- Primitive = tờ giấy ghi số. Sao tờ giấy → 2 tờ độc lập, mỗi tờ có số 10. Sửa tờ này không ảnh hưởng tờ kia.
- Reference = tờ giấy ghi địa chỉ nhà. Sao tờ giấy → 2 tờ cùng ghi một địa chỉ. Ai sửa đồ trong nhà qua tờ nào cũng thấy qua tờ kia.
flowchart LR
subgraph Primitive["Primitive -- copy gia tri"]
a["a = 10"]
b["b = 99"]
end
subgraph Reference["Reference -- copy dia chi"]
r1["arr1 -- ref"]
r2["arr2 -- ref"]
obj["Object [99, 2, 3]"]
r1 -.-> obj
r2 -.-> obj
end4. Bên trong — JVM bố trí memory thế nào
Đến đây giới thiệu các từ khoá kỹ thuật bạn sẽ gặp khắp nơi.
4.1 JVM, Stack, Heap — ba từ khoá đầu tiên
- JVM (Java Virtual Machine) — chương trình chạy code Java. Coi như "máy ảo" khiến code bạn chạy giống nhau trên Windows, macOS, Linux.
- Stack — vùng nhớ tạm, mỗi method đang chạy có một stack frame (ngăn chứa biến local của method đó). Method kết thúc → frame biến mất.
- Heap — vùng nhớ dài hạn, chứa tất cả object (
String, mảng, class bạn viết...). Object sống đến khi không còn ai tham chiếu, JVM tự dọn qua Garbage Collection (GC).
💡 💡 Cách nhớ
- Stack = bàn làm việc — đang làm việc gì, giấy tờ đang mở trên bàn. Xong việc, dọn sạch.
- Heap = kho hàng — đồ đạc dùng chung, để đó lâu dài, nhân viên dọn kho (GC) tự xử lý đồ không ai dùng.
4.2 Primitive vs Reference ở cấp memory
- Biến primitive — giá trị nằm thẳng trong stack frame. Đọc/ghi cực nhanh, không GC.
- Biến reference — stack frame chứa địa chỉ (reference). Object thật nằm trên heap.
flowchart LR
subgraph Stack["Stack frame"]
x["int x = 10 (value in-place)"]
s["String s (holds reference)"]
end
subgraph Heap["Heap"]
obj["Object 'hello'"]
end
s -.-> objĐây là lý do:
- ✅
intkhông thểnull— không có "không có giá trị" cho số. - ✅ Ba tỷ
Stringcùng nội dung "hello" không nhất thiết là 3 object — có thể dùng chung 1 object qua String Pool. - ✅ Mảng truyền vào method vẫn sửa được từ bên trong — vì caller và method cùng giữ địa chỉ.
5. "Java luôn pass-by-value" — câu gây tranh cãi
Pass-by-value = khi bạn truyền biến vào method, Java copy biến đó.
- Biến primitive → copy giá trị thật
- Biến reference → copy địa chỉ (không copy object)
void addOne(int n) {
n = n + 1; // chi sua ban copy — caller khong biet
}
void addFirst(int[] arr) {
arr[0] = arr[0] + 1; // sua object qua dia chi — caller bi anh huong
}
void resetList(List<String> list) {
list = new ArrayList<>(); // chi sua ban copy reference — caller van giu list cu
}
💡 💡 Cách nhớ
Method nhận tờ giấy ghi địa chỉ nhà (đã sao từ caller).
- Đi vào nhà sơn tường đỏ (
arr[0]++) → caller thấy nhà đỏ. - Xé tờ giấy cũ, viết địa chỉ nhà mới (
list = new ArrayList<>()) → caller không quan tâm, vẫn giữ tờ giấy cũ địa chỉ nhà cũ.
Đây là lý do câu "Java không có pass-by-reference" đúng về kỹ thuật, dù cảm giác như có.
6. String và == vs .equals()
String là kiểu reference. Khi so sánh 2 String:
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true — bat ngo!
System.out.println(s1.equals(s2)); // true
String s3 = new String("hello");
System.out.println(s1 == s3); // false
System.out.println(s1.equals(s3)); // true
Vì sao s1 == s2 ra true với literal?
JVM có String Pool — vùng đặc biệt trong heap lưu các String literal. Khi bạn viết "hello" nhiều lần, JVM chỉ tạo 1 object duy nhất và cho các biến literal cùng trỏ vào đó.
new String("hello") bỏ qua pool, tạo object mới → địa chỉ khác → == cho false.
Quy tắc thực tế
- ✅ Luôn dùng
.equals()để so sánh nội dung chuỗi. - ❌ Không bao giờ dùng
==để so chuỗi — lúc đúng lúc sai tuỳ bạn có dùng literal hay không.
⚠️ 🔒 Lưu ý về bảo mật
Vì String là immutable và có thể bị intern vào String Pool, password và API key thường được lưu dưới dạng char[] thay vì String — để có thể ghi đè bằng Arrays.fill(buf, ' ') sau khi dùng, tránh hacker dump heap đọc được. Chi tiết sẽ có ở module Security.
7. var — cú pháp rút gọn từ Java 10
var tuoi = 25; // JVM hieu la int
var ten = "An"; // JVM hieu la String
var list = new ArrayList<String>(); // JVM hieu la ArrayList<String>
var có phải "kiểu động"?
❌ Không. Java vẫn là static type — kiểu xác định lúc compile, không đổi lúc chạy.
var chỉ là type inference — compiler nhìn vế phải = để tự điền kiểu thật. Sau khi compile, file .class có kiểu tường minh, y như bạn viết int tuoi.
✅ Khi nào dùng var?
- ✅ Kiểu rõ ràng từ vế phải:
var list = new ArrayList<String>();— đọc 1 giây biết ngay. - ✅ Giảm boilerplate generic dài:
var map = new HashMap<String, List<Integer>>(); - ❌ Vế phải không rõ kiểu:
var x = someMethod();— phải đi đọc method mới biết → khó đọc, tránh. - ❌ Khai báo không gán:
var x;— không hợp lệ, compiler không biết infer gì.
💡 🔬 Bytecode behind (optional)
Compile 2 file:
// A.java
class A { void m() { int x = 10; } }
// B.java
class B { void m() { var x = 10; } }
Chạy javap -c A B — bytecode giống hệt:
bipush 10
istore_1
return
var biến mất trong class file. Compiler đã điền int rồi.
8. Hệ quả bạn phải nhớ
Vì primitive nằm trực tiếp, reference là địa chỉ:
- Copy primitive = 2 biến độc lập. Sửa biến này không ảnh hưởng biến kia.
- Copy reference = 2 biến trỏ cùng object. Sửa object qua biến nào cũng thấy qua biến kia.
- Truyền primitive vào method = caller không bị ảnh hưởng bởi thao tác bên trong method.
- Truyền reference vào method = method không đổi được biến reference của caller (gán lại không có tác dụng), nhưng có thể sửa object qua reference đó.
- Primitive không thể
null; reference có thể → đây là nguồnNullPointerException. - So sánh chuỗi: luôn
.equals(), không bao giờ==.
9. Pitfall thực tế
❌ Nhầm 1: Dùng == để so chuỗi, hoạt động lúc test local nhưng vỡ trên production.
✅ Objects.equals(a, b) hoặc a.equals(b) (nếu chắc a không null).
❌ Nhầm 2: Tưởng void clearList(List<String> list) { list = new ArrayList<>(); } xoá list.
✅ Muốn xoá thật: list.clear() — gọi method qua reference, sửa object.
❌ Nhầm 3: Tin var là dynamic type.
✅ var chỉ compile-time. Sau compile, type cố định y hệt.
❌ Nhầm 4: Integer a = 128; Integer b = 128; a == b ra false nhưng a = 100; b = 100; a == b ra true.
✅ Bẫy Integer Cache — Integer.valueOf(-128..127) trả cache instance, ngoài range tạo object mới. Luôn dùng .equals().
10. 📚 Deep Dive Oracle
ℹ️ 📚 Deep Dive Oracle (optional)
Nguồn gốc spec:
- JLS §4.1 — Kinds of Types and Values
- JLS §4.12 — Variables
- JEP 286 — Local-Variable Type Inference (var)
- JLS §5.1.7 — Boxing Conversion (Integer cache)
Diễn giải đơn giản: Oracle chia type của Java thành 2 họ (primitive, reference). var là cú pháp tiện, compiler tự điền type — không có "untyped variable" trong Java. Integer cache là optimization được spec hoá trong §5.1.7.
11. Tóm tắt
- Biến = kiểu + tên + giá trị. Cú pháp:
<kieu> <ten> = <giaTri>;. - 2 họ biến: primitive (giá trị thật trong stack frame) vs reference (địa chỉ trỏ tới object trên heap).
- Copy primitive = sao giá trị. Copy reference = sao địa chỉ.
- Java luôn pass-by-value; "value" có thể là số hoặc địa chỉ.
- Luôn
.equals()cho chuỗi, không==. var= compile-time inference, không phải dynamic type.
12. Tự kiểm tra
- Vì sao
int b = a; b = 99;không đổia, nhưngint[] arr2 = arr1; arr2[0] = 99;lại đổiarr1[0]? - Đoạn sau in gì? Giải thích.
String s = "hi"; changeIt(s); System.out.println(s); // void changeIt(String x) { x = "bye"; } var x;— hợp lệ không? Vì sao?- Vì sao
Integer.valueOf(200) == Integer.valueOf(200)chofalse, nhưngInteger.valueOf(100) == Integer.valueOf(100)chotrue? - Bạn muốn lưu password đọc từ user nhập. Nên dùng
Stringhaychar[]? Vì sao?
Bài tiếp theo: Kiểu nguyên thuỷ — int, long, double, boolean, char