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.
TL;DR: Biến là một cái tên đại diện cho một ô nhớ trong máy tính. Trong Java, mọi biến thuộc 1 trong 2 họ: primitive (chứa giá trị thực tế ngay trong stack frame) và reference (chứa địa chỉ trỏ tới đối tượng trên heap). Java luôn là pass-by-value — tức luôn sao chép giá trị (với primitive là sao chép số liệu, với reference là sao chép địa chỉ). Luôn so sánh chuỗi bằng .equals() thay vì ==, và dùng từ khóa var một cách an toàn nhờ cơ chế type inference tại compile time.
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.
Biến là gì và đóng vai trò như thế nào trong bộ nhớ?
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.
Cú pháp khai báo biến cơ bản và các quy tắc đặt tên gồm những gì?
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; // integer
String ten = "An"; // string
boolean daDangKy = true; // boolean flag
double luong = 12.5; // floating-point
Dùng và gán lại:
System.out.println(tuoi); // prints: 25
tuoi = 26; // reassign new value
System.out.println(tuoi); // prints: 26
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.
Kiểu nguyên thủy và kiểu tham chiếu khác biệt nhau ra sao?
Đâ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.
Thử nghiệm nhỏ đoán kết quả
// Code snippet A -- 2 primitive variables
int a = 10;
int b = a;
b = 99;
System.out.println(a); // 10
System.out.println(b); // 99
// Code snippet B -- 2 array variables
int[] arr1 = {1, 2, 3};
int[] arr2 = arr1;
arr2[0] = 99;
System.out.println(arr1[0]); // 99
System.out.println(arr2[0]); // 99
- Đoạn A:
a=10,b=99— sửabkhông ảnh hưởngavì giá trị được sao chép độc lập. - Đoạn B:
arr1[0]=99,arr2[0]=99— cả hai biến cùng trỏ chung một mảng duy nhất trên Heap.
| Tiêu chí | Primitive | Reference |
|---|---|---|
| Ví dụ | int, double, boolean, char, long | String, mảng, Object, List |
| Biến chứa gì? | Giá trị dữ liệu trực tiếp | Địa chỉ tới object trên Heap |
| Copy biến | Sao chép giá trị bit | Sao chép địa chỉ |
| Có thể null? | Không | Có |

JVM phân bổ bộ nhớ Stack và Heap cho các biến như thế nào?
- Stack: Vùng nhớ tạm, mỗi method đang chạy sở hữu một stack frame chứa biến local. Method kết thúc → frame biến mất ngay lập tức.
- Heap: Vùng nhớ dài hạn chứa tất cả object. Object tồn tại đến khi không còn ai tham chiếu và được Garbage Collector (GC) dọn dẹp.
Biến primitive lưu trữ giá trị trực tiếp trong stack frame. Biến reference chỉ giữ địa chỉ trong stack frame, còn object thật nằm trên Heap.

Cơ chế pass-by-value trong Java hoạt động như thế nào khi truyền tham số?
Java luôn luôn pass-by-value: Khi truyền biến vào method, Java luôn copy giá trị biến đó:
- Biến primitive → copy giá trị số học thật.
- Biến reference → copy địa chỉ (không copy toàn bộ object).
void addOne(int n) {
n = n + 1; // modifies local copy only -- caller unaffected
}
void addFirst(int[] arr) {
arr[0] = arr[0] + 1; // modifies object via shared reference -- caller sees change
}
void resetList(List<String> list) {
list = new ArrayList<>(); // modifies local reference copy only -- caller keeps original list
}
Tại sao so sánh chuỗi String phải dùng equals thay vì toán tử ==?
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true -- string pool optimization
System.out.println(s1.equals(s2)); // true
String s3 = new String("hello");
System.out.println(s1 == s3); // false -- different object references
System.out.println(s1.equals(s3)); // true -- same character sequence
JVM có String Pool để tái sử dụng các String literal giống nhau. Khi dùng new String(), JVM tạo object độc lập ngoài pool khiến == trả về false. Vì vậy, luôn dùng .equals() để so sánh nội dung chuỗi.
Từ khóa var trong Java 10 hoạt động ra sao và dùng khi nào?
var tuoi = 25; // inferred as int
var ten = "An"; // inferred as String
var list = new ArrayList<String>(); // inferred as ArrayList<String>
var là type inference tại compile-time, không phải kiểu động. Sau khi biên dịch sang .class, kiểu dữ liệu hoàn toàn tĩnh và cố định.
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.
Những hệ quả quan trọng nào cần lưu ý về quản lý bộ nhớ biến?
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ờ==.
Những sai lầm kinh điển nào thường gặp với biến và tham chiếu?
❌ 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().
Tài liệu đặc tả Java nào giúp hiểu sâu về biến và kiểu dữ liệu?
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.
Đọc thêm: Biến và phép gán — nếu khái niệm "biến" còn mơ hồ trước khi rẽ vào cú pháp Java, khoá Nhập môn lập trình dạy đúng khái niệm này chậm hơn, không dính primitive/reference hay JVM.
Những điểm cốt lõi cần nhớ về biến và kiểu dữ liệu trong Java là gì?
- Phân định hai họ kiểu: Primitive lưu trực tiếp giá trị trong stack frame; Reference lưu địa chỉ trỏ tới đối tượng trên heap.
- Bản chất Pass-by-value: Java luôn sao chép giá trị khi truyền tham số (sao chép bit dữ liệu hoặc sao chép bit địa chỉ).
- So sánh chuỗi an toàn: Luôn sử dụng
.equals()để so sánh nội dung chuỗi; tuyệt đối không dùng toán tử==. - Từ khóa var: Cung cấp cú pháp khai báo tiện lợi dựa trên suy luận kiểu tĩnh ở giai đoạn biên dịch.
Câu hỏi tự kiểm tra giúp củng cố kiến thức như thế nào?
- Q1Vì sao
int b = a; b = 99;không đổia, nhưngint[] arr2 = arr1; arr2[0] = 99;lại đổiarr1[0]? - 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"; } - Q3
var x;— hợp lệ không? Vì sao? - Q4Vì sao
Integer.valueOf(200) == Integer.valueOf(200)chofalse, nhưngInteger.valueOf(100) == Integer.valueOf(100)chotrue? - Q5Bạ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
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Module 1 — Tổng kết & cheat sheet
Bài tiếpKiểu nguyên thuỷ trong Java: 8 loại, size và overflow
Lộ trình Junior Java Backend Developer
Chặng 2/21 · Lập trình từ số 0 với Java — bài 10/32 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này