Bỏ qua

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 Integer trong vòng lặp lớn là performance bug kinh điển
  • Tại sao null chỉ 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

floatdouble 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:

System.out.println(0.1 + 0.2); // 0.30000000000000004

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.

String name = "Alice"; // (1)
String copy = name;    // (2)
  1. "Alice" được tạo trên Heap. name là reference trên Stack giữ địa chỉ của object đó.
  2. copy không tạo object mới. Nó nhận cùng địa chỉ với name — cả hai đang trỏ vào cùng một object.

Stack vs Heap — reference variable


5. So sánh ==.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:

var x = 10;
x = "hello"; // ❌ compile error — x vẫn là int

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().

Autoboxing and Integer Cache


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

import java.math.BigDecimal;
import java.util.ArrayList;

public class DataTypesDemo {

    public static void main(String[] args) {
        // ── Primitive ───────────────────────────────────────────
        int age = 28;
        double salary = 25_000.50;
        boolean isEmployed = true;
        char grade = 'A';

        // ── Reference ───────────────────────────────────────────
        String name = "Nguyen Van A";      // literal → String Pool
        int[] scores = {85, 90, 78};       // array object trên Heap

        // ── var (Java 10+) ──────────────────────────────────────
        var city = "Ho Chi Minh";          // String — compiler tự suy
        var list = new ArrayList<String>(); // ArrayList<String>

        // ── Tiền — luôn dùng BigDecimal ─────────────────────────
        BigDecimal price = new BigDecimal("199.99");
        BigDecimal tax   = new BigDecimal("0.10");
        BigDecimal total = price.multiply(BigDecimal.ONE.add(tax));
        System.out.println("Total: " + total); // 219.989 — chính xác

        // ── == vs equals ────────────────────────────────────────
        String s1 = new String("Java");
        String s2 = new String("Java");
        System.out.println(s1 == s2);      // false — khác địa chỉ
        System.out.println(s1.equals(s2)); // true  — cùng nội dung

        // ── Integer cache boundary ──────────────────────────────
        Integer a = 100; Integer b = 100;
        System.out.println(a == b);  // true  (cached)

        Integer c = 200; Integer d = 200;
        System.out.println(c == d);  // false (ngoài cache)
    }
}

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ố Lf

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 Integer từ -128 đến 127. Với giá trị 128, autoboxing gọi Integer.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. StringBuilder dù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ùng StringBuilder khi 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. varlocal 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. JavaScript var mớ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 (0 cho số, false cho boolean, null cho 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

Bình luận