Java Foundations
10/39
Bài 10 / 39~13 phútCú pháp Java & Kiểu dữ liệuLộ trình · chặng 02/21Miễn phí… lượt xem

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 ô đó.

💡 Cách nhớ

Hình dung dãy ngăn kéo có nhãn:

  • Ngăn tuoi chứa số 25
  • Ngăn ten chứ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:

  1. Kiểu (type) — biến chứa loại gì (int, String, boolean...)
  2. Tên — cách bạn gọi nó (camelCase là convention Java)
  3. Giá trị ban đầu (optional với field, khuyến khích với local variable)
Java
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:

Java
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ểuChứa gìVí dụDùng khi
intSố nguyên 32-bit25, -7, 0Đếm, chỉ số, id nhỏ
longSố nguyên 64-bit1_000_000_000LTimestamp, id database
doubleSố thập phân 64-bit3.14, -0.5Giá tiền, toán tổng quát
booleanĐúng / saitrue, falseFlag, điều kiện
char1 ký tự'A', '?'Ký tự đơn lẻ
StringChuỗ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 — age tốt hơn intAge

Thử chạy đoạn này (Java 11+):

Java
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);
  }
}
Bash
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ả

Java
// 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ửa b không ảnh hưởng a vì 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íPrimitiveReference
Ví dụint, double, boolean, char, longString, 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ếnSao chép giá trị bitSao chép địa chỉ
Có thể null?KhôngCó

Hai khung so sánh: bên trái copy primitive sinh ra hai ô nhớ riêng giữ 10 và 99, bên phải copy reference cho arr1 và arr2 cùng giữ một địa chỉ và cùng trỏ tới một mảng duy nhất trên heap

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.

Khung stack frame chứa int x giữ thẳng số 10 và String s chỉ giữ địa chỉ, một mũi tên nét đứt từ s sang khung heap nơi đặt object chuỗi hello

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).
Java
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ử ==?

Java
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?

Java
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ì.
🔬 Bytecode behind (optional)

Compile 2 file:

Java
// 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ỉ:

  1. Copy primitive = 2 biến độc lập. Sửa biến này không ảnh hưởng biến kia.
  2. 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.
  3. Truyền primitive vào method = caller không bị ảnh hưởng bởi thao tác bên trong method.
  4. 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 đó.
  5. Primitive không thể null; reference có thể → đây là nguồn NullPointerException.
  6. 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?

📚 Deep Dive Oracle (optional)

Nguồn gốc spec:

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?

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao int b = a; b = 99; không đổi a, nhưng int[] arr2 = arr1; arr2[0] = 99; lại đổi arr1[0]?
  2. 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"; }
  3. Q3
    var x; — hợp lệ không? Vì sao?
  4. Q4
    Vì sao Integer.valueOf(200) == Integer.valueOf(200) cho false, nhưng Integer.valueOf(100) == Integer.valueOf(100) cho true?
  5. Q5
    Bạn muốn lưu password đọc từ user nhập. Nên dùng String hay char[]? 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.

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

Bài tiếp theo

Kiểu nguyên thuỷ trong Java: 8 loại, size và overflow