Kiểu dữ liệu và Biến¶
1. Biến (Variable)¶
Biến là một vùng nhớ được đặt tên, dùng để lưu giá trị trong suốt quá trình chương trình chạy. Mỗi biến trong Java có ba thành phần:
- Kiểu dữ liệu — xác định loại giá trị có thể lưu và kích thước vùng nhớ
- Tên — định danh để tham chiếu đến vùng nhớ đó
- Giá trị — dữ liệu đang được lưu tại thời điểm đó
Khai báo và khởi tạo¶
// Khai báo — compiler cấp phát vùng nhớ, chưa có giá trị
int age;
// Khởi tạo — gán giá trị lần đầu
age = 28;
// Khai báo + khởi tạo cùng lúc — cách viết phổ biến nhất
int age = 28;
Local variable bắt buộc phải được khởi tạo trước khi dùng — compiler báo lỗi nếu không.
int score;
System.out.println(score); // ❌ compile error: variable score might not have been initialized
Quy tắc đặt tên¶
Java theo chuẩn camelCase cho tên biến và method:
int studentAge = 20; // ✅ camelCase
String firstName = "An"; // ✅
boolean isLoggedIn = false; // ✅ boolean thường bắt đầu bằng is/has/can
int StudentAge = 20; // ❌ PascalCase — dành cho class
int student_age = 20; // ❌ snake_case — không phải Java convention
int 2fast = 0; // ❌ không được bắt đầu bằng số
int class = 1; // ❌ từ khóa Java — không dùng làm tên biến
Tên biến nên là danh từ mô tả rõ giá trị nó lưu. Tránh x, temp, data trừ khi ngữ cảnh đủ rõ ràng.
Phân loại biến¶
| Loại | Khai báo ở đâu | Phạm vi | Giá trị mặc định |
|---|---|---|---|
| Local variable | Trong method / block | Trong block đó | Không có — bắt buộc khởi tạo |
| Instance variable | Trong class, ngoài method | Cả object | Có (0, false, null) |
| Static variable | Trong class, có từ khóa static |
Cả class | Có (0, false, null) |
| Parameter | Danh sách tham số method | Trong method đó | Giá trị được truyền vào |
public class Student {
String name; // instance variable
static int count = 0; // static variable
void greet(String greeting) { // greeting là parameter
String message = greeting + ", " + name; // message là local variable
System.out.println(message);
}
}
Lưu ý
Instance variable và static variable sẽ được giải thích chi tiết ở phần OOP. Ở đây chỉ cần nhận biết sự tồn tại của chúng.
2. Kiểu dữ liệu (Data Type)¶
Mỗi biến phải khai báo kiểu — đây là đặc điểm của ngôn ngữ statically typed như Java. Kiểu dữ liệu xác định loại giá trị có thể lưu, kích thước vùng nhớ, và tập thao tác hợp lệ trên biến đó.
Trong Java, mọi giá trị thuộc về một trong hai nhóm hoàn toàn khác nhau về cách lưu trữ và truy cập:
| Nhóm | Ví dụ | Lưu ở đâu | Lưu gì |
|---|---|---|---|
| Primitive | int, double, boolean |
Stack | Giá trị trực tiếp |
| Reference | String, int[], mọi Object |
Stack + Heap | Địa chỉ của object |
Sự phân biệt này không đơn thuần là chi tiết kỹ thuật. Đây là nền tảng để lý giải:
- Tại sao
==cho kết quả bất ngờ với String và Integer - Tại sao truyền object vào method không "copy" nó
- Tại sao
Integertrong vòng lặp lớn là performance bug kinh điển - Tại sao
nullchỉ tồn tại ở reference type, không phải primitive
Đây cũng là topic phỏng vấn phổ biến nhất ở level Junior.
3. Kiểu nguyên thủy (Primitive)¶
Java có đúng 8 kiểu primitive. Chúng được JVM xử lý trực tiếp mà không cần tạo object hay phụ thuộc vào Garbage Collector.
| Kiểu | Kích thước | Mặc định | Khoảng giá trị |
|---|---|---|---|
byte |
8 bit | 0 | -128 → 127 |
short |
16 bit | 0 | -32,768 → 32,767 |
int |
32 bit | 0 | -2.1 tỷ → 2.1 tỷ |
long |
64 bit | 0L | -9.2 × 10¹⁸ → 9.2 × 10¹⁸ |
float |
32 bit | 0.0f | ~7 chữ số thập phân |
double |
64 bit | 0.0 | ~15 chữ số thập phân |
char |
16 bit | ' ' | 0 → 65,535 (Unicode) |
boolean |
Tùy JVM (thường 1 byte trong array, 4 byte trong field) | false | true / false |
int age = 28;
double salary = 25_000.50; // dấu gạch dưới giúp đọc số lớn dễ hơn
boolean isActive = true;
char grade = 'A';
long population = 8_000_000_000L; // phải có hậu tố L
Cạm bẫy với số thực¶
float và double lưu số theo chuẩn IEEE 754 — tức là xấp xỉ nhị phân, không phải giá trị thập phân chính xác:
Quy tắc bất di bất dịch
Không bao giờ dùng float hay double để tính toán tiền tệ.
Sử dụng java.math.BigDecimal cho mọi bài toán tài chính.
// Sai — kết quả sai ở hàng xu
double total = 0.1 + 0.2; // 0.30000000000000004
// Đúng — chính xác tuyệt đối
BigDecimal total = new BigDecimal("0.1")
.add(new BigDecimal("0.2")); // 0.3
Java 25 — Primitive trong Pattern Matching (JEP 507, Preview)¶
Java 25 mở rộng pattern matching sang cả primitive, giúp code gọn và nhất quán hơn:
// Trước Java 25 — phải boxing hoặc dùng if/else
int score = 85;
String grade;
if (score >= 90) grade = "A";
else if (score >= 70) grade = "B";
else grade = "C";
// Java 25 preview — primitive trực tiếp trong switch
String grade = switch (score) {
case int i when i >= 90 -> "A";
case int i when i >= 70 -> "B";
default -> "C";
};
4. Kiểu tham chiếu (Reference)¶
Mọi thứ không phải primitive đều là reference type: String, array, và mọi class do lập trình viên định nghĩa. Điểm khác biệt cốt lõi: biến không lưu trữ object — nó lưu trữ địa chỉ dẫn đến object.
Có thể hình dung như một tờ giấy ghi địa chỉ nhà. Tờ giấy không phải là ngôi nhà — nó chỉ cho biết ngôi nhà ở đâu.
"Alice"được tạo trên Heap.namelà reference trên Stack giữ địa chỉ của object đó.copykhông tạo object mới. Nó nhận cùng địa chỉ vớiname— cả hai đang trỏ vào cùng một object.
5. So sánh == và .equals()¶
Toán tử == luôn so sánh giá trị đang nằm trực tiếp trên Stack. Với primitive, đó là giá trị thật. Với reference, đó là địa chỉ.
// Primitive — == hoạt động như kỳ vọng
int x = 42;
int y = 42;
System.out.println(x == y); // true
// Reference — == so sánh địa chỉ, không phải nội dung
String s1 = new String("Java");
String s2 = new String("Java");
System.out.println(s1 == s2); // false — hai địa chỉ khác nhau
System.out.println(s1.equals(s2)); // true — nội dung giống nhau
Nguyên tắc thực hành
Luôn dùng .equals() để so sánh nội dung String.
Đặt literal phía trước để tránh NullPointerException:
// Nguy hiểm — ném NullPointerException nếu userInput là null
if (userInput.equals("admin")) { ... }
// An toàn — literal không bao giờ null
if ("admin".equals(userInput)) { ... }
6. var — Type Inference (Java 10+)¶
Java 10 giới thiệu var cho phép compiler tự suy kiểu từ vế phải. Đây là cú pháp tiêu chuẩn trong Java hiện đại — giảm lặp lại mà không làm mất type safety.
var name = "Alice"; // String
var count = 42; // int
var prices = new ArrayList<Double>(); // ArrayList<Double>
var entry = map.entrySet().iterator().next(); // Map.Entry<K,V>
var không phải dynamic typing — compiler biết kiểu tại compile time và lock nó lại:
Giới hạn của var
var chỉ dùng được cho local variable có khởi tạo ngay lập tức.
Không dùng được cho field, method parameter, hay return type.
var list = new ArrayList<String>(); // ✅ local variable
// var name; // ❌ thiếu initializer
// private var field = "x"; // ❌ không dùng làm field
7. String Pool — Cơ chế tối ưu của JVM¶
Khi sử dụng string literal, JVM kiểm tra String Pool trước. Nếu chuỗi đó đã tồn tại, JVM trả về object cũ thay vì tạo mới.
String a = "hello"; // tạo "hello" trong Pool
String b = "hello"; // tìm thấy trong Pool, trả về cùng object
System.out.println(a == b); // true — cùng object
new String("hello") phá vỡ hành vi này — nó ép JVM tạo object mới trên Heap thông thường, bỏ qua Pool. Không có lý do chính đáng nào để viết new String("...") trong code thực tế.
Text Blocks (Java 15+)¶
"""...""" là cú pháp tiêu chuẩn cho String nhiều dòng:
// Cũ — khó đọc, nhiều escape
String json = "{\n \"name\": \"Alice\",\n \"age\": 28\n}";
// Java 15+ — rõ ràng, sạch
String json = """
{
"name": "Alice",
"age": 28
}
""";
Text Blocks vẫn là String bình thường — pool và immutability không thay đổi.
8. Integer Cache — Cạm bẫy ít được chú ý¶
JVM cache sẵn các object Integer trong khoảng -128 đến 127 khi khởi động.
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true — cùng cached object
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false — hai object khác nhau trên Heap
Khoảng -128 đến 127 được chọn vì đây là các giá trị phổ biến nhất trong thực tế: index mảng, bộ đếm, flag.
Có thể mở rộng cache
JVM cho phép tăng upper bound của cache:
-XX:AutoBoxCacheMax=<n>
Ví dụ -XX:AutoBoxCacheMax=256 sẽ cache đến 256.
Lưu ý
Không bao giờ dùng == để so sánh Integer, Long, hay Double.
Luôn dùng .equals().
9. Autoboxing & Unboxing¶
Collection như ArrayList chỉ chấp nhận object, không chấp nhận primitive. Java tự động chuyển đổi qua lại bằng autoboxing:
| Primitive | Wrapper |
|---|---|
int |
Integer |
long |
Long |
double |
Double |
boolean |
Boolean |
List<Integer> numbers = new ArrayList<>();
numbers.add(42); // Java tự gọi Integer.valueOf(42)
int n = numbers.get(0); // Java tự gọi intValue()
Autoboxing tiện lợi nhưng ẩn chi phí nghiêm trọng trong vòng lặp lớn:
// Có vấn đề — tạo 1 triệu object Long không cần thiết
Long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
sum += i; // unbox → cộng → tạo Long mới → gán lại
}
// Đúng — thuần túy số học, không object nào được tạo ra
long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
sum += i;
}
Phiên bản đầu chậm hơn khoảng 6 lần và tạo áp lực GC không cần thiết (Effective Java, Item 61).
10. Code ví dụ¶
Verified
Bản đầy đủ có thể compile: DataTypesDemo.java
11. Lỗi thường gặp¶
Lỗi 1 — Dùng == so sánh String¶
// SAI — so sánh địa chỉ, không phải nội dung
if (userInput == "admin") { ... }
// ĐÚNG — đặt literal trước để tránh NPE nếu userInput là null
if ("admin".equals(userInput)) { ... }
Lỗi 2 — Dùng float/double tính tiền¶
double price = 0.1 + 0.2;
System.out.println(price); // 0.30000000000000004
BigDecimal p = new BigDecimal("0.1").add(new BigDecimal("0.2")); // 0.3
Lỗi 3 — Quên hậu tố L và f¶
long big = 9_999_999_999; // ❌ compile error — int overflow
long big = 9_999_999_999L; // ✅
float f = 3.14; // ❌ compile error — 3.14 là double literal
float f = 3.14f; // ✅
Lỗi 4 — Autoboxing trong vòng lặp lớn¶
Long sum = 0L; // ❌ tạo hàng triệu Long object
for (long i = 0; i < 1_000_000; i++) sum += i;
long sum = 0L; // ✅ thuần primitive
for (long i = 0; i < 1_000_000; i++) sum += i;
Lỗi 5 — NullPointerException khi unboxing¶
Integer count = null;
int total = count + 1; // ❌ NPE khi unbox null
if (count != null) {
int total = count + 1; // ✅
}
Lỗi 6 — Local variable chưa khởi tạo¶
int total;
System.out.println(total); // ❌ compile error: variable total might not have been initialized
int total = 0; // ✅
System.out.println(total);
12. Câu hỏi phỏng vấn¶
Q1: Sự khác nhau cốt lõi giữa primitive và reference type?
Primitive lưu giá trị thật trực tiếp trên Stack — không có indirection, không có GC overhead, không thể null. Reference lưu địa chỉ của object trên Heap — được GC quản lý, có thể null, hỗ trợ polymorphism.
Q2: Tại sao Integer a = 128; Integer b = 128; a == b trả về false?
JVM cache
Integertừ -128 đến 127. Với giá trị 128, autoboxing gọiInteger.valueOf(128)— method này tạo object mới trên Heap thay vì trả về cached instance. Kết quả là==so sánh hai địa chỉ khác nhau. Luôn dùng.equals()để so sánh wrapper type.
Q3: String immutability có ý nghĩa gì trong thực tế?
Một khi được tạo, nội dung String không bao giờ thay đổi. Mọi thao tác như
concat,toUpperCase,replaceđều trả về String mới — object gốc không bị ảnh hưởng. Immutability là điều kiện để String Pool hoạt động an toàn và làm String thread-safe tự nhiên.
Q4: Khi nào dùng StringBuilder thay vì toán tử +?
Mỗi phép nối
+tạo ra một String object mới. Trong vòng lặp n lần, điều này tạo ra O(n²) memory overhead.StringBuilderdùng resizable buffer nội bộ — O(n) overall. Quy tắc: dùng+khi nối ít chuỗi trong một statement, dùngStringBuilderkhi nối trong vòng lặp.
Q5: Khi nào bắt buộc phải dùng Integer thay vì int?
Khi lưu vào Collection, khi dùng làm generic type parameter, khi cần null để biểu diễn "không có giá trị", hoặc khi cần utility method như
Integer.parseInt(),Integer.toBinaryString().
Q6: var có phải dynamic typing không? Khác gì với JavaScript?
Không.
varlà local variable type inference tại compile time. Compiler suy kiểu từ vế phải và lock nó lại —var x = 10; x = "hello"là compile error. Java vẫn là statically typed. JavaScriptvarmới là dynamic — khác hoàn toàn.
Q7: Local variable khác instance variable như thế nào?
Local variable khai báo trong method, chỉ tồn tại trong scope của block đó, và bắt buộc phải khởi tạo trước khi dùng. Instance variable khai báo trong class, tồn tại suốt vòng đời của object, và có giá trị mặc định nếu không khởi tạo (
0cho số,falsecho boolean,nullcho reference type).
13. Tài liệu tham khảo¶
| Tài liệu | Nội dung liên quan |
|---|---|
| Effective Java — Joshua Bloch | Item 6: Avoid unnecessary objects · Item 61: Prefer primitives to boxed primitives |
| JLS Section 4 — Types, Values, Variables | Đặc tả ngôn ngữ chính thức |
| JEP 507 — Primitive Types in Patterns | Java 25 preview: primitive trong pattern matching |
| JEP 286 — Local Variable Type Inference | var — thiết kế và giới hạn |
| Java Performance — Scott Oaks | Chapter 4: Working with the JVM |
| Oracle Java Tutorial — Data Types | Primitive Data Types |