Biến và khai báo — cái tên gắn với một giá trị
Hiểu biến trong Java từ cái đơn giản nhất (ngăn kéo có tên) đến phân biệt primitive vs reference, cơ chế copy, pass-by-value và var.
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 ô đó.
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) |
- 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).
- 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
}
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.
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ì.
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
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
Q1Vì sao int b = a; b = 99; không đổi a, nhưng int[] arr2 = arr1; arr2[0] = 99; lại đổi arr1[0]?▸
int b = a; b = 99; không đổi a, nhưng int[] arr2 = arr1; arr2[0] = 99; lại đổi arr1[0]?int là primitive — giá trị thật nằm trực tiếp trong biến. int b = a copy giá trị, a và b là 2 ô nhớ độc lập. int[] là reference — biến giữ địa chỉ mảng trên heap. arr2 = arr1 copy địa chỉ → arr1 và arr2 cùng trỏ 1 mảng duy nhất. Sửa arr2[0] là sửa mảng chung đó → arr1[0] cũng thấy thay đổi.Q2Đoạn sau in gì? Giải thích.String s = "hi";
changeIt(s);
System.out.println(s);
// void changeIt(String x) { x = "bye"; }
▸
String s = "hi";
changeIt(s);
System.out.println(s);
// void changeIt(String x) { x = "bye"; }hi. Java luôn pass-by-value; giá trị ở đây là bản copy của reference. Trong changeIt, x là biến local giữ cùng địa chỉ với s. Gán x = "bye" chỉ đổi biến local x sang trỏ String mới, không đổi biến s của caller. Muốn caller thấy: return về giá trị mới và gán lại.Q3var x; — hợp lệ không? Vì sao?▸
var x; — hợp lệ không? Vì sao?var (JEP 286, Java 10+) là type inference tại compile time — compiler cần biểu thức khởi tạo để suy ra kiểu. var x; không có initializer → compiler không biết suy ra kiểu gì → từ chối. Phải viết var x = 10; hoặc dùng kiểu tường minh int x;.Q4Vì sao Integer.valueOf(200) == Integer.valueOf(200) cho false, nhưng Integer.valueOf(100) == Integer.valueOf(100) cho true?▸
Integer.valueOf(200) == Integer.valueOf(200) cho false, nhưng Integer.valueOf(100) == Integer.valueOf(100) cho true?Integer cho range -128..127. Integer.valueOf(100) trả cùng instance cho mọi lần gọi → == so 2 reference cùng trỏ 1 object → true. 200 ngoài cache → mỗi lần tạo object mới → 2 reference khác nhau → == = false. Bài học: luôn dùng .equals() cho wrapper, không dùng ==.Q5Bạn muốn lưu password đọc từ user nhập. Nên dùng String hay char[]? Vì sao?▸
String hay char[]? Vì sao?Dùng char[]. Lý do:
Stringimmutable — giá trị password nằm trên heap cho tới khi GC dọn (không kiểm soát được lúc nào). Heap dump / memory scan có thể đọc được.char[]cho phépArrays.fill(password, ' ')để xóa ngay sau khi dùng xong → giảm thời gian password tồn tại trong RAM.- Spec Java chính thức đồng ý:
JPasswordField.getPassword()vàPasswordCallback.getPassword()đều trảchar[], không phảiString.
Bài tiếp theo: Kiểu nguyên thuỷ — int, long, double, boolean, char
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên