Chương 02 · Biến & kiểu dữ liệu

Biến, kiểu dữ liệu và toán tử

Java chia thế giới làm hai nửa dứt khoát: kiểu nguyên thủy (giá trị nằm thẳng trong biến) và kiểu tham chiếu (biến chỉ giữ địa chỉ tới object trên heap). Ranh giới này giải thích gần như mọi hành vi khó hiểu của Java — vì sao == lúc đúng lúc sai, vì sao Integer có bộ nhớ đệm, vì sao String bất biến, vì sao nối chuỗi trong vòng lặp lại chậm. Hiểu chương này là hiểu nền móng của mọi chương sau.

Độ dài: ~1600 dòng Bài tập: 5 Quiz: 8 Prerequisites: Chương 1
🎯 Mục tiêu chương
  • Thuộc lòng 8 kiểu nguyên thủy: kích thước, phạm vi, giá trị mặc định.
  • Phân biệt rạch ròi nguyên thủy và tham chiếu; hiểu stack và heap.
  • Nắm tràn số, chia số nguyên, và sai số IEEE 754 — ba nguồn bug kinh điển.
  • Dùng BigDecimal khi tính tiền, và biết vì sao double không được phép.
  • Hiểu wrapper class, autoboxing, và bẫy == với bộ nhớ đệm Integer.
  • Hiểu String bất biến, String pool, và khi nào phải dùng StringBuilder.
  • Dùng text block (Java 15+) cho chuỗi nhiều dòng.
  • Nắm ép kiểu mở rộng (tự động) và thu hẹp (bắt buộc khai báo).
  • Dùng var (Java 10+) đúng chỗ, và final cho hằng số.
  • Tra được bảng toán tử đầy đủ và thứ tự ưu tiên.

1. Khai báo biến — kiểu đứng trước tên

KhaiBao.java
// Cú pháp: KIỂU tên = giá_trị;
int tuoi = 22;
String ten = "Việt Anh";
double diem = 8.75;
boolean dangHoc = true;

// Khai báo trước, gán sau
int soLuong;
soLuong = 10;

// Nhiều biến cùng kiểu trên một dòng — hợp lệ nhưng nên tránh
int a = 1, b = 2, c = 3;

// ❌ Không thể đổi kiểu — Java là ngôn ngữ định kiểu TĨNH và MẠNH
tuoi = "hai mươi hai";
// error: incompatible types: String cannot be converted to int

// ❌ Không thể dùng biến cục bộ chưa gán
int x;
System.out.println(x);
// error: variable x might not have been initialized
🔁 Đối chiếu với Python & JavaScript
JavaPythonJavaScript
Khai báoint x = 5;x = 5let x = 5;
Kiểm tra kiểuLúc biên dịchLúc chạyLúc chạy
Đổi kiểu biến❌ Không được✅ Được✅ Được
Dùng biến chưa gán❌ Lỗi biên dịch❌ NameError lúc chạy⚠️ undefined — không báo gì!
Kết thúc câu lệnhDấu ; bắt buộcXuống dòng; tuỳ chọn

Sự khắt khe của Java trả giá bằng số ký tự phải gõ, nhưng đổi lại rất nhiều lỗi bị bắt trước khi chương trình chạy. Một dự án Java 100.000 dòng gần như không bao giờ gặp lỗi kiểu TypeError lúc chạy — thứ mà dự án Python cùng cỡ gặp hằng ngày.

1.1. Quy ước đặt tên

LoạiQuy ướcVí dụ
Biến, tham số, phương thứccamelCasesoLuong, tinhTongTien()
Class, interface, enum, recordPascalCaseSinhVien, DonHang
Hằng số (static final)UPPER_SNAKE_CASEMAX_SIZE, THUE_SUAT
Packagetoàn chữ.thườngcom.vidu.quanly
Biến booleanBắt đầu bằng is/has/canisActive, hasPermission
Biến đếm trong vòng lặpi, j, kNgoại lệ được chấp nhận cho tên một chữ

2. Tám kiểu nguyên thủy

Java có đúng 8 kiểu nguyên thủy — con số này cố định, không thêm bớt được. Chúng không phải object: giá trị nằm trực tiếp trong biến, không có phương thức, không thể là null.

KiểuKích thướcPhạm viMặc địnhWrapperDùng khi
byte8 bit−128 … 1270ByteXử lý dữ liệu nhị phân, mảng byte
short16 bit−32.768 … 32.7670ShortHiếm dùng
int ⭐32 bit≈ −2,1 tỷ … 2,1 tỷ0IntegerMặc định cho số nguyên
long64 bit≈ ±9,2 × 10¹⁸0LLongTimestamp, ID, tiền (đơn vị nhỏ nhất)
float32 bit≈ ±3,4 × 10³⁸ (7 chữ số)0.0fFloatHiếm dùng — độ chính xác thấp
double ⭐64 bit≈ ±1,8 × 10³⁰⁸ (15-16 chữ số)0.0dDoubleMặc định cho số thực
char16 bit0 … 65.535 (một đơn vị mã UTF-16)''CharacterMột ký tự
booleankhông xác định*true / falsefalseBooleanĐiều kiện logic

* Đặc tả JVM không quy định kích thước boolean; trên thực tế nó thường chiếm cả một byte (hoặc một int) trong bộ nhớ vì lý do căn chỉnh.

NguyenThuy.java
// Hậu tố BẮT BUỘC cho long và float
long danSo = 8_000_000_000L;   // L bắt buộc — vượt phạm vi int
float pi = 3.14f;               // f bắt buộc — mặc định số thực là double
double e = 2.718;              // d tuỳ chọn

// Dấu gạch dưới làm số dễ đọc (Java 7+) — trình biên dịch bỏ qua
int mot_trieu = 1_000_000;
long theTinDung = 1234_5678_9012_3456L;

// Các hệ cơ số
int thapPhan  = 255;
int nhiPhan   = 0b1111_1111;   // 255 (Java 7+)
int batPhan   = 0377;          // 255 — ⚠️ số 0 đứng đầu = hệ 8!
int thapLucPhan = 0xFF;        // 255

// Tra giới hạn — không cần học thuộc con số
System.out.println(Integer.MAX_VALUE);   // 2147483647
System.out.println(Integer.MIN_VALUE);   // -2147483648
System.out.println(Long.MAX_VALUE);      // 9223372036854775807
System.out.println(Double.MAX_VALUE);    // 1.7976931348623157E308
🕳️ Số 0 đứng đầu nghĩa là hệ bát phân
BatPhan.java
int gio = 08;    // ❌ error: integer number too large
                  //    vì 0 đầu = hệ 8, mà hệ 8 không có chữ số 8!
int phut = 030;  // ⚠️ biên dịch OK nhưng giá trị là 24, không phải 30

Bẫy này hay xuất hiện khi định dạng thời gian hoặc mã số có số 0 ở đầu. Nếu cần giữ số 0 đầu để hiển thị, hãy dùng String và String.format("%02d", n).

2.1. Tràn số — Java không báo lỗi

TranSo.java
int max = Integer.MAX_VALUE;      // 2147483647
System.out.println(max + 1);        // -2147483648  ⚠️ quay vòng, KHÔNG báo lỗi

// Bug kinh điển: nhân hai int lớn rồi mới gán vào long
long mili = 1000 * 60 * 60 * 24 * 365;
System.out.println(mili);            // 1471228928  ❌ SAI!
// Phép nhân thực hiện ở kiểu int rồi TRÀN, sau đó mới ép sang long

// ✅ Sửa: ép một toán hạng sang long NGAY TỪ ĐẦU
long dung = 1000L * 60 * 60 * 24 * 365;
System.out.println(dung);            // 31536000000  ✅

// ✅ Muốn được BÁO LỖI khi tràn: dùng Math.*Exact (Java 8+)
try {
    int r = Math.addExact(max, 1);
} catch (ArithmeticException e) {
    System.out.println(e.getMessage());  // integer overflow
}
// Còn có: subtractExact, multiplyExact, negateExact, toIntExact

Vì sao Java không báo lỗi khi tràn số

Vì kiểm tra tràn ở mọi phép tính sẽ làm chậm chương trình đáng kể, và Java được thiết kế để phép toán số nguyên ánh xạ trực tiếp xuống lệnh CPU. Cách quay vòng (wrap-around) tuân theo chuẩn bù hai và có hành vi xác định — không phải "hành vi không xác định" như C.

Bài học thực tế: khi làm việc với thời gian, tiền, kích thước tệp, hoặc bất cứ thứ gì có thể vượt 2,1 tỷ, hãy dùng long ngay từ toán hạng đầu tiên, không phải chỉ ở biến đích. Nơi cần an toàn tuyệt đối thì dùng Math.multiplyExact để lỗi lộ ra ngay thay vì âm thầm cho kết quả sai.

2.2. Chia số nguyên cắt phần thập phân

Chia.java
System.out.println(7 / 2);        // 3    ⚠️ không phải 3.5
System.out.println(7 % 2);        // 1
System.out.println(7.0 / 2);      // 3.5  — một toán hạng là double là đủ
System.out.println((double) 7 / 2); // 3.5  — ép kiểu trước khi chia
System.out.println((double) (7 / 2)); // 3.0  ❌ ép SAU khi đã cắt — quá muộn

// Chia số âm: CẮT VỀ 0, không phải làm tròn xuống
System.out.println(-7 / 2);       // -3   (Python cho -4!)
System.out.println(-7 % 2);       // -1   (Python cho 1!)

// Muốn hành vi làm tròn xuống như Python:
System.out.println(Math.floorDiv(-7, 2));  // -4
System.out.println(Math.floorMod(-7, 2));  //  1

// Chia cho 0: hai hành vi HOÀN TOÀN khác nhau
System.out.println(1 / 0);        // ArithmeticException: / by zero
System.out.println(1.0 / 0);      // Infinity  — không ném lỗi!
System.out.println(0.0 / 0);      // NaN
🕳️ NaN không bằng chính nó
NaN.java
double nan = 0.0 / 0.0;
System.out.println(nan == nan);           // false  ⚠️
System.out.println(Double.isNaN(nan));   // true   ✅ cách kiểm tra đúng

Đây là quy định của chuẩn IEEE 754, áp dụng cho mọi ngôn ngữ. Không bao giờ dùng == để kiểm tra NaN. Tương tự, kiểm tra vô cực bằng Double.isInfinite(x).

2.3. double không chính xác — và tiền thì không được sai

SaiSo.java
System.out.println(0.1 + 0.2);            // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3);      // false  ⚠️
System.out.println(1.03 - 0.42);           // 0.6100000000000001

// Vì sao? 0.1 trong hệ nhị phân là số vô hạn tuần hoàn,
// giống như 1/3 = 0.333... trong hệ thập phân. 64 bit không đủ chứa.

// So sánh double đúng cách: dùng ngưỡng sai số
double a = 0.1 + 0.2, b = 0.3;
System.out.println(Math.abs(a - b) < 1e-9);  // true ✅
Tien.java — hai cách đúng để xử lý tiền
import java.math.BigDecimal;
import java.math.RoundingMode;

// ❌ TUYỆT ĐỐI KHÔNG dùng double cho tiền
double gia1 = 0.1, gia2 = 0.2;
System.out.println(gia1 + gia2);         // 0.30000000000000004 đ ???

// ✅ CÁCH 1: BigDecimal — chính xác tuyệt đối, dùng cho tài chính
BigDecimal x = new BigDecimal("0.1");   // ⚠️ PHẢI truyền String!
BigDecimal y = new BigDecimal("0.2");
System.out.println(x.add(y));            // 0.3  ✅ chính xác

// ⚠️ Truyền double vào constructor là sai — nó nhận cả sai số
System.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625

// BigDecimal dùng METHOD, không dùng toán tử
BigDecimal gia = new BigDecimal("1500000");
BigDecimal sl  = new BigDecimal("3");
BigDecimal vat = new BigDecimal("0.08");

BigDecimal tong = gia.multiply(sl);
BigDecimal thue = tong.multiply(vat).setScale(0, RoundingMode.HALF_UP);
System.out.println(tong.add(thue));    // 4860000

// ⚠️ So sánh BigDecimal: dùng compareTo, KHÔNG dùng equals
BigDecimal m = new BigDecimal("1.0");
BigDecimal n = new BigDecimal("1.00");
System.out.println(m.equals(n));         // false — khác số chữ số thập phân!
System.out.println(m.compareTo(n) == 0); // true  ✅

// ✅ CÁCH 2: long tính bằng đơn vị nhỏ nhất (đồng, xu)
long giaDong = 1_500_000L;      // nhanh hơn BigDecimal nhiều lần
long tongDong = giaDong * 3;      // hợp với VND vì không có đơn vị nhỏ hơn đồng
Quy tắc không có ngoại lệ: tiền tệ không bao giờ lưu bằng float hay double. Dùng BigDecimal (khi cần chia, tính lãi, nhiều loại tiền) hoặc long theo đơn vị nhỏ nhất (khi chỉ cộng trừ nhân). Với VND, long thường là lựa chọn tốt vì không có đơn vị nhỏ hơn đồng.

3. Nguyên thủy vs tham chiếu — stack và heap

Đây là khái niệm quan trọng nhất chương. Mọi biến trong Java thuộc đúng một trong hai loại, và cách chúng lưu trữ hoàn toàn khác nhau.

Stack — nhanh, tự dọn
int tuoi → 22
double d → 8.75
boolean b → true
giá trị nằm THẲNG trong biến
Heap — lớn, do GC dọn
String s → 0x7f3a ─┐
int[] a → 0x7f8c ─┼→ object
SinhVien sv → null │   trên heap
biến chỉ giữ ĐỊA CHỈ
Biến nguyên thủy chứa giá trị. Biến tham chiếu chứa địa chỉ của object nằm trên heap — hoặc null nếu chưa trỏ vào đâu.
Kiểu nguyên thủyKiểu tham chiếu
Ví dụint, double, boolean, char…String, mảng, mọi class, Integer, enum
Biến chứa gìChính giá trịĐịa chỉ tới object
Nằm ở đâuStack (nếu là biến cục bộ)Object nằm trên heap
Nhận null❌ Không✅ Có
Có phương thức❌ Không✅ Có
Giá trị mặc định (trường)0, false, ''null
So sánh bằng ==So giá trị ✅So địa chỉ ⚠️
So sánh đúng cách==.equals()
Bộ nhớCố định, nhỏObject + con trỏ (thêm chi phí)
ThamChieu.java
// --- Nguyên thủy: sao chép GIÁ TRỊ ---
int a = 10;
int b = a;      // b nhận bản sao của 10
b = 99;
System.out.println(a);   // 10  — a không đổi

// --- Tham chiếu: sao chép ĐỊA CHỈ ---
int[] x = {1, 2, 3};
int[] y = x;   // y trỏ vào CÙNG mảng, không tạo mảng mới
y[0] = 99;
System.out.println(x[0]);  // 99  ⚠️ x cũng thấy thay đổi!

// Muốn bản sao thật:
int[] z = x.clone();
z[0] = 1;
System.out.println(x[0]);  // 99 — x không bị ảnh hưởng ✅

Java luôn truyền tham số theo giá trị — kể cả object

Câu này gây tranh cãi nhiều nhất trong giới Java, nhưng nó chính xác. Thứ được sao chép khi gọi phương thức luôn là nội dung của biến. Với kiểu nguyên thủy, nội dung đó là giá trị. Với kiểu tham chiếu, nội dung đó là địa chỉ — nên phương thức nhận được một bản sao của địa chỉ, trỏ vào cùng một object.

TruyenThamSo.java
static void suaNoiDung(int[] mang) {
    mang[0] = 99;          // ✅ sửa object mà địa chỉ trỏ tới → NGOÀI THẤY
}

static void ganLai(int[] mang) {
    mang = new int[]{9, 9, 9};  // ❌ chỉ đổi BẢN SAO địa chỉ → ngoài KHÔNG thấy
}

int[] m = {1, 2, 3};
suaNoiDung(m);
System.out.println(m[0]);   // 99
ganLai(m);
System.out.println(m[0]);   // 99 — vẫn thế, KHÔNG thành 9

Nếu Java truyền theo tham chiếu thật (như & của C++), phương thức ganLai sẽ đổi được biến m ở ngoài. Nó không đổi được — bằng chứng cho thấy cái được truyền là bản sao của địa chỉ. Câu nói chính xác nhất là: "truyền theo giá trị, trong đó giá trị của biến tham chiếu chính là địa chỉ".

4. Wrapper class và autoboxing

Kiểu nguyên thủy không phải object, nên chúng không dùng được ở những nơi Java yêu cầu object — ví dụ List<int> là bất hợp lệ. Mỗi kiểu nguyên thủy vì thế có một wrapper class tương ứng.

Wrapper.java
import java.util.List;
import java.util.ArrayList;

// ❌ Không hợp lệ — generic chỉ nhận kiểu tham chiếu
// List<int> ds = new ArrayList<>();

// ✅ Dùng wrapper
List<Integer> ds = new ArrayList<>();

// AUTOBOXING — int tự chuyển thành Integer
ds.add(42);                 // thực chất: ds.add(Integer.valueOf(42))

// UNBOXING — Integer tự chuyển về int
int x = ds.get(0);          // thực chất: ds.get(0).intValue()

// Wrapper còn mang theo nhiều tiện ích tĩnh
System.out.println(Integer.parseInt("123"));      // 123 (int)
System.out.println(Integer.valueOf("123"));      // 123 (Integer)
System.out.println(Integer.toBinaryString(10));  // 1010
System.out.println(Integer.max(3, 7));         // 7
System.out.println(Double.parseDouble("3.14"));    // 3.14
System.out.println(Character.isDigit('7'));      // true
System.out.println(Boolean.parseBoolean("true"));  // true
⚠️ Bẫy lớn nhất: == với Integer
IntegerCache.java
Integer a = 127, b = 127;
System.out.println(a == b);        // true   😀

Integer c = 128, d = 128;
System.out.println(c == d);        // false  😱 CÙNG GIÁ TRỊ mà khác nhau?!

System.out.println(c.equals(d));   // true   ✅ luôn dùng equals

Nguyên nhân: Integer.valueOf() giữ sẵn một bộ nhớ đệm các object cho giá trị từ −128 đến 127 (theo yêu cầu của đặc tả ngôn ngữ, vì khoảng này được dùng cực kỳ nhiều). Trong khoảng đó, hai lần autoboxing cùng giá trị trả về cùng một object → == đúng. Ngoài khoảng đó, mỗi lần tạo một object mới → == so hai địa chỉ khác nhau → sai.

Bài học: với mọi kiểu tham chiếu — kể cả wrapper — luôn dùng .equals(). Dùng == chỉ khi bạn thực sự muốn hỏi "có phải cùng một object không".

🕳️ Hai bẫy khác của autoboxing
AutoboxBay.java
// ① NullPointerException khi unboxing null
Map<String, Integer> diem = new HashMap<>();
int d = diem.get("khong-ton-tai");
// NullPointerException — get() trả null, unboxing null thì nổ
// ✅ Sửa: dùng Integer d = ... rồi kiểm tra, hoặc getOrDefault("x", 0)

// ② Hiệu năng: autoboxing trong vòng lặp nóng
Long tong = 0L;                       // ❌ Long (wrapper) thay vì long
for (long i = 0; i < 10_000_000L; i++) {
    tong += i;    // mỗi vòng: unbox → cộng → box → tạo 1 object MỚI trên heap
}
// Chậm hơn phiên bản dùng `long tong` khoảng 5-10 lần và tạo 10 triệu rác
Quy tắc chọn: dùng nguyên thủy làm mặc định (nhanh hơn, không có null, không tạo rác). Chỉ dùng wrapper khi bắt buộc: trong generic (List<Integer>), khi cần biểu diễn "chưa có giá trị" bằng null, hoặc khi cần gọi phương thức tiện ích tĩnh.

5. String — bất biến và có pool riêng

String không phải kiểu nguyên thủy — nó là một class. Nhưng Java ưu ái nó đặc biệt: có cú pháp chữ viết sẵn ("..."), có toán tử + riêng, và có một vùng bộ nhớ riêng gọi là String pool.

5.1. Bất biến — mọi thao tác đều tạo chuỗi mới

BatBien.java
String s = "Xin chào";
s.toUpperCase();                    // ⚠️ trả về chuỗi MỚI, s không đổi
System.out.println(s);              // "Xin chào"

s = s.toUpperCase();                // ✅ phải gán lại
System.out.println(s);              // "XIN CHÀO"

// Mọi method của String đều trả về chuỗi mới:
String t = "  Java  ";
System.out.println(t.trim());              // "Java"
System.out.println(t.strip());             // "Java" (Java 11+, hiểu Unicode)
System.out.println(t.replace("a", "@"));   // "  J@v@  "
System.out.println(t.isBlank());           // false (Java 11+)
System.out.println("ab".repeat(3));        // "ababab" (Java 11+)
System.out.println(String.join(", ", "a", "b", "c"));  // "a, b, c"
System.out.println("a,b,c".split(",").length);         // 3

Vì sao String được thiết kế bất biến

  • An toàn luồng miễn phí — nhiều luồng đọc chung một String mà không cần khoá.
  • Cho phép pool — nếu chuỗi sửa được, việc dùng chung object sẽ cực kỳ nguy hiểm.
  • Mã băm được cache — hashCode() tính một lần rồi lưu lại, khiến String trở thành khoá lý tưởng cho HashMap.
  • Bảo mật — đường dẫn tệp, URL, tham số kết nối truyền vào thư viện không thể bị sửa ngầm sau khi đã kiểm tra.

5.2. String pool — vì sao == lúc đúng lúc sai

StringPool.java
String a = "Java";              // vào String pool
String b = "Java";              // tái dùng object trong pool
System.out.println(a == b);       // true   — cùng object

String c = new String("Java");  // ép tạo object MỚI ngoài pool
System.out.println(a == c);       // false  — khác object
System.out.println(a.equals(c));  // true   ✅ so nội dung

System.out.println(a == c.intern()); // true — intern() đưa vào/lấy từ pool

// Ghép hằng số: trình biên dịch tính sẵn → vào pool
String d = "Ja" + "va";
System.out.println(a == d);       // true — javac gộp thành "Java" lúc biên dịch

// Ghép với BIẾN: tính lúc chạy → object mới, không vào pool
String phan = "Ja";
String e = phan + "va";
System.out.println(a == e);       // false  ⚠️

// Trừ khi biến là final — lúc đó javac coi nó là hằng
final String phanF = "Ja";
String f = phanF + "va";
System.out.println(a == f);       // true
Không bao giờ so sánh chuỗi bằng ==. Luôn dùng .equals(), hoặc .equalsIgnoreCase() khi không phân biệt hoa thường. Muốn an toàn với null, dùng Objects.equals(a, b) hoặc đặt hằng số ở vế trái: "Java".equals(x).

5.3. StringBuilder — khi nào bắt buộc dùng

NoiChuoi.java
// ❌ CHẬM — mỗi vòng lặp tạo một String mới, copy toàn bộ nội dung cũ
String s = "";
for (int i = 0; i < 100_000; i++) {
    s += i;       // độ phức tạp O(n²) — với n=100k mất vài giây
}

// ✅ NHANH — một bộ đệm ký tự, chỉ nới rộng khi đầy
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100_000; i++) {
    sb.append(i);   // O(n) tổng cộng — mất vài mili giây
}
String ketQua = sb.toString();

// StringBuilder cho phép nối chuỗi phương thức
String bao = new StringBuilder()
        .append("Tên: ").append("An")
        .append(" | Tuổi: ").append(22)
        .reverse()                 // còn có insert, delete, replace, setCharAt
        .toString();
📌 Khi nào + vẫn ổn

Trình biên dịch tự chuyển a + b + c (trên một biểu thức, ngoài vòng lặp) thành lời gọi hiệu quả — từ Java 9 là invokedynamic với StringConcatFactory. Nên String s = "Tên: " + ten + ", tuổi: " + tuoi; hoàn toàn ổn và dễ đọc hơn.

Vấn đề chỉ xuất hiện khi nối trong vòng lặp, vì lúc đó mỗi vòng là một biểu thức riêng → mỗi vòng một bộ đệm mới → chi phí O(n²). Bạn đã tự nhìn thấy điều này ở bài tập bytecode chương 1.

ClassSửa được?An toàn luồng?Dùng khi
String❌✅Mặc định
StringBuilder✅❌Nối nhiều lần — nhanh nhất
StringBuffer✅✅ (đồng bộ)Gần như không bao giờ — di sản Java 1.0

5.4. Text block — chuỗi nhiều dòng (Java 15+)

TextBlock.java
// ❌ Cách cũ — khó đọc, dễ sai
String jsonCu = "{\n" +
        "  \"ten\": \"Việt Anh\",\n" +
        "  \"tuoi\": 22\n" +
        "}";

// ✅ Text block — ba dấu nháy kép, Java 15+
String json = """
        {
          "ten": "Việt Anh",
          "tuoi": 22
        }""";

// Không cần escape dấu nháy kép. Thụt lề chung tự bị cắt bỏ:
// Java lấy dòng thụt ÍT NHẤT làm mốc và cắt phần thừa của mọi dòng.

String sql = """
        SELECT id, ho_ten, diem
        FROM   sinh_vien
        WHERE  lop = ? AND diem >= ?
        ORDER  BY diem DESC
        """;

// Dấu \ ở cuối dòng: nối dòng, không xuống dòng
String motDong = """
        Đây là một câu \
        rất dài viết trên nhiều dòng.""";
// → "Đây là một câu rất dài viết trên nhiều dòng."

// Kết hợp formatted() để chèn giá trị
String thu = """
        Kính gửi %s,
        Đơn hàng #%d của bạn đã được xác nhận.
        """.formatted("anh Việt", 1042);

6. Ép kiểu — mở rộng và thu hẹp

EpKieu.java
// MỞ RỘNG (widening) — tự động, không mất dữ liệu
// byte → short → int → long → float → double
int i = 100;
long l = i;        // tự động
double d = l;      // tự động

// THU HẸP (narrowing) — PHẢI khai báo, CÓ THỂ mất dữ liệu
double pi = 3.99;
int nguyen = (int) pi;         // 3  — CẮT phần thập phân, không làm tròn
System.out.println(Math.round(pi)); // 4  — muốn làm tròn thì dùng Math.round

int lon = 300;
byte nho = (byte) lon;         // 44 ⚠️ mất dữ liệu âm thầm (300 % 256 = 44)

// ⚠️ Bẫy: mất chính xác khi long → float/double dù là "mở rộng"
long lonNhat = Long.MAX_VALUE;    // 9223372036854775807
float f = lonNhat;
System.out.println((long) f);      // 9223372036854775807 → nhưng float chỉ có 7 chữ số!

// Nâng kiểu tự động trong biểu thức
byte b1 = 10, b2 = 20;
// byte tong = b1 + b2;    // ❌ error: b1 + b2 cho ra int!
byte tong = (byte) (b1 + b2);   // ✅ phải ép lại
// Quy tắc: mọi phép toán số học nâng byte/short/char lên int trước khi tính

// char ↔ int
char ch = 'A';
int ma = ch;                      // 65
char ke = (char) (ch + 1);       // 'B'
System.out.println('A' + 1);        // 66  ⚠️ in ra SỐ, không phải 'B'
System.out.println("A" + 1);        // "A1" — String + số thì nối chuỗi

// Chuyển đổi qua String
String s = String.valueOf(42);      // "42" — an toàn cả với null
int n = Integer.parseInt("42");     // 42  — ném NumberFormatException nếu sai

7. var và final

VarFinal.java
// --- var (Java 10+): trình biên dịch tự suy ra kiểu ---
var ten = "Việt Anh";                 // String
var tuoi = 22;                          // int
var ds = new ArrayList<String>();      // ArrayList<String>
var map = new HashMap<String, List<Integer>>();  // ⭐ tiết kiệm thật sự

// ⚠️ var KHÔNG phải kiểu động — kiểu vẫn cố định lúc biên dịch
var x = 10;
// x = "chuỗi";   // ❌ error: incompatible types

// var KHÔNG dùng được ở:
// var a;                        ❌ không có giá trị khởi tạo → không suy ra được
// var b = null;                 ❌ null không có kiểu
// class C { var f = 1; }        ❌ chỉ dùng cho biến CỤC BỘ
// void m(var p) { }             ❌ không dùng cho tham số method

// --- final: gán đúng MỘT lần ---
final int MAX = 100;
// MAX = 200;   // ❌ error: cannot assign a value to final variable

// ⚠️ final chỉ khoá THAM CHIẾU, không khoá NỘI DUNG object
final List<String> danhSach = new ArrayList<>();
danhSach.add("a");           // ✅ được — sửa nội dung
// danhSach = new ArrayList<>();  // ❌ không được — gán tham chiếu khác

// Hằng số thật sự ở cấp class
public static final double THUE_SUAT = 0.08;
public static final List<String> MAU = List.of("đỏ", "xanh");
// List.of() tạo list BẤT BIẾN — add() sẽ ném UnsupportedOperationException

// Kết hợp: final var
final var ketQua = tinhToan();
💡 Dùng var khi nào
VarKhiNao.java
// ✅ NÊN — kiểu đã hiển hiện ở vế phải, viết lại là thừa
var ds = new ArrayList<SinhVien>();
var nhom = new HashMap<String, List<DonHang>>();
for (var sv : danhSachSinhVien) { ... }

// ❌ KHÔNG NÊN — người đọc không biết kiểu gì
var kq = xuLy();          // xuLy() trả về cái gì?
var n = laySo();          // int? long? double? BigDecimal?
var t = 1;                // gõ "int" chẳng dài hơn "var"

Nguyên tắc: var để giảm nhiễu, không phải để giấu thông tin. Nếu người đọc không suy ra được kiểu chỉ bằng cách nhìn dòng đó, hãy viết kiểu ra.

8. Bảng toán tử đầy đủ

8.1. Số học và gán

Toán tửTênVí dụGhi chú
+Cộng / nối chuỗi7 + 2 → 9Có một toán hạng là String thì nối chuỗi
- *Trừ, nhân7 * 2 → 14
/Chia7 / 2 → 3Hai int → chia nguyên
%Chia lấy dư-7 % 2 → -1Dấu theo số bị chia (khác Python)
++ --Tăng / giảm 1i++, ++iTiền tố trả giá trị sau, hậu tố trả giá trị trước
+= -= *= /= %=Gán ghépx += 5Tự ép kiểu ngầm — xem bẫy bên dưới
TangGiam.java
int i = 5;
System.out.println(i++);   // 5  — in giá trị CŨ rồi mới tăng
System.out.println(i);     // 6

int j = 5;
System.out.println(++j);   // 6  — tăng TRƯỚC rồi in

// ⚠️ Bẫy: gán ghép tự ép kiểu ngầm — không báo lỗi mất dữ liệu
byte b = 10;
// b = b + 5;      // ❌ error: int không gán vào byte được
b += 5;             // ✅ chạy được — vì thực chất là b = (byte)(b + 5)

int n = 10;
n += 0.5;           // chạy được! thực chất n = (int)(n + 0.5) = 10
System.out.println(n);     // 10 — mất dữ liệu âm thầm ⚠️

8.2. So sánh và logic

Toán tửÝ nghĩaLưu ý quan trọng
== !=Bằng / khácNguyên thủy: so giá trị. Tham chiếu: so địa chỉ → dùng equals()
< > <= >=So sánh thứ tựChỉ dùng cho kiểu số và char
&&VÀ (đoản mạch)Vế phải không chạy nếu vế trái false
||HOẶC (đoản mạch)Vế phải không chạy nếu vế trái true
& |VÀ / HOẶC (không đoản mạch)Luôn chạy cả hai vế — hiếm khi muốn thế
!Phủ địnhChỉ áp cho boolean
? :Ba ngôidieuKien ? a : b — biểu thức, có giá trị trả về
instanceofKiểm tra kiểuCó pattern matching từ Java 16 — xem chương 5
Logic.java
// ⭐ Đoản mạch — kỹ thuật phòng NullPointerException quan trọng nhất
String s = null;
if (s != null && s.length() > 0) {   // ✅ an toàn: vế phải không chạy
    ...
}
if (s != null & s.length() > 0) {    // ❌ NullPointerException — & chạy cả hai vế
    ...
}

// ⚠️ Khác Python/JS: điều kiện PHẢI là boolean
int n = 0;
// if (n) { }        // ❌ error: int cannot be converted to boolean
if (n != 0) { }         // ✅ phải so sánh tường minh
// Java KHÔNG có khái niệm truthy/falsy — đây là điểm cộng về rõ ràng

// Toán tử ba ngôi
int tuoi = 20;
String loai = tuoi >= 18 ? "người lớn" : "trẻ em";

// Lồng nhau được nhưng khó đọc — cân nhắc switch (chương 3)
String xepLoai = diem >= 9 ? "Giỏi"
                 : diem >= 7 ? "Khá"
                 : diem >= 5 ? "Trung bình" : "Yếu";

8.3. Toán tử bit

Toán tửTênVí dụKết quả
&AND bit12 & 108 (1100 & 1010 = 1000)
|OR bit12 | 1014
^XOR bit12 ^ 106
~NOT bit~12-13 (~x == -x-1)
<<Dịch trái3 << 212 (nhân 2ⁿ)
>>Dịch phải có dấu-8 >> 1-4 (giữ bit dấu)
>>>Dịch phải không dấu-8 >>> 12147483644 (điền 0)

>>> — toán tử chỉ Java có

Java không có kiểu số nguyên không dấu (khác C/C++/Rust), nên nó bù lại bằng toán tử dịch phải không dấu >>> — luôn điền số 0 vào bên trái thay vì nhân bản bit dấu. Nó xuất hiện ở những chỗ bạn không ngờ: HashMap dùng h ^ (h >>> 16) để trộn mã băm, và thuật toán tìm kiếm nhị phân trong thư viện chuẩn dùng (low + high) >>> 1 để tránh tràn số khi cộng hai chỉ số lớn. Chương 8 sẽ quay lại điểm đầu tiên.

8.4. Thứ tự ưu tiên

MứcToán tử
1 (cao nhất)() [] . ++ -- (hậu tố)
2++ -- (tiền tố), +x -x, !, ~, (kiểu), new
3* / %
4+ -
5<< >> >>>
6< > <= >= instanceof
7== !=
8-10& → ^ → |
11&&
12||
13? :
14 (thấp nhất)= += -= …
💡 Đừng học thuộc — dùng ngoặc
Ngoac.java
// Bẫy kinh điển: & và | ưu tiên THẤP hơn ==
if (x & 1 == 0) { }      // ❌ hiểu thành x & (1 == 0) → lỗi kiểu
if ((x & 1) == 0) { }    // ✅ ý bạn muốn: kiểm tra số chẵn

// Đúng nhưng khó đọc
if (a && b || c && !d) { }
// ✅ Cùng ý nghĩa, đọc một lần là hiểu
if ((a && b) || (c && !d)) { }

9. Cheat sheet chương 2

Tình huốngCách làm
Số nguyên mặc địnhint; vượt 2,1 tỷ thì long (nhớ hậu tố L)
Số thực mặc địnhdouble
Tiền tệBigDecimal (constructor nhận String) hoặc long theo đơn vị nhỏ nhất
So sánh chuỗia.equals(b) — không bao giờ ==
So sánh wrappera.equals(b) — nhớ bẫy cache −128…127
So sánh BigDecimala.compareTo(b) == 0, không phải equals
So sánh doubleMath.abs(a - b) < 1e-9
Kiểm tra NaNDouble.isNaN(x)
Nối chuỗi trong vòng lặpStringBuilder.append()
Chuỗi nhiều dòngText block """…""" (Java 15+)
Tránh tràn khi nhânÉp long từ toán hạng đầu: 1000L * 60 * 60
Muốn lỗi khi trànMath.multiplyExact(), Math.addExact()
Chia lấy số thực(double) a / b — ép trước khi chia
Chuỗi → sốInteger.parseInt(s), Double.parseDouble(s)
Số → chuỗiString.valueOf(n)
Hằng sốpublic static final + UPPER_SNAKE_CASE
Tránh NPE khi kiểm trax != null && x.method() (đoản mạch)

Bài tập

Giải thích năm kết quả bất ngờ

Chạy đoạn sau trong JShell. Với mỗi dòng: dự đoán kết quả trước, chạy, rồi viết một câu giải thích cơ chế.

BatNgo.java
System.out.println(0.1 + 0.2 == 0.3);
System.out.println(Integer.MAX_VALUE + 1);
System.out.println(1000 * 60 * 60 * 24 * 365);
Integer a = 1000, b = 1000; System.out.println(a == b);
System.out.println('a' + 'b');
System.out.println("a" + 'b');
System.out.println(1 + 2 + "3" + 4 + 5);
byte x = 10; x += 300; System.out.println(x);
Gợi ý

Dòng 7 kiểm tra hiểu biết về tính kết hợp trái sang phải: 1 + 2 tính trước (ra 3, kiểu int), gặp "3" thì chuyển sang nối chuỗi và mọi thứ sau đó đều là chuỗi.
Dòng 8 kiểm tra ép kiểu ngầm của +=: 10 + 300 = 310, ép về byte thì 310 - 256 = 54.

Máy tính tiền không sai một xu

Viết class HoaDon tính hoá đơn với yêu cầu tuyệt đối chính xác:

Yêu cầu
// Ba mặt hàng, mỗi mặt hàng có: tên, đơn giá, số lượng
// - Tính tổng trước thuế
// - Giảm giá 12.5% nếu tổng > 5.000.000đ
// - Thuế VAT 8% trên số sau giảm giá
// - Làm tròn tới hàng đồng, luật HALF_UP
// - In bảng thẳng hàng, tiền có dấu phân cách hàng nghìn

Làm hai phiên bản: một bằng double, một bằng BigDecimal. So sánh kết quả tới từng đồng và ghi lại sai lệch. Với dữ liệu nào thì double cho kết quả sai?

Gợi ý

BigDecimal dùng phương thức chứ không phải toán tử: add, subtract, multiply, divide. Riêng divide bắt buộc truyền RoundingMode, nếu không sẽ ném ArithmeticException: Non-terminating decimal expansion khi kết quả là số vô hạn tuần hoàn (ví dụ 1/3).
Định dạng: String.format("%,.0fđ", …) hoặc NumberFormat.getCurrencyInstance(new Locale("vi","VN")).

Đo hiệu năng nối chuỗi

Viết chương trình đo thời gian nối n chuỗi bằng ba cách, với n = 1.000, 10.000, 100.000:

  • String s += i; trong vòng lặp
  • StringBuilder.append(i)
  • StringBuilder với dung lượng đặt trước: new StringBuilder(n * 6)

Lập bảng kết quả. Thời gian tăng theo tỷ lệ nào khi n tăng 10 lần với từng cách? Điều đó khớp với độ phức tạp lý thuyết nào?

Gợi ý

Đo bằng System.nanoTime(). Chạy mỗi phép đo vài lần và bỏ lần đầu — JIT cần thời gian "làm nóng" (warm-up), nên lần chạy đầu luôn chậm bất thường.
Bạn sẽ thấy cách 1 tăng ~100 lần khi n tăng 10 lần (O(n²)), còn cách 2-3 chỉ tăng ~10 lần (O(n)). Cách 3 nhanh hơn cách 2 một chút vì tránh được việc cấp phát lại mảng khi bộ đệm đầy.

Điều tra bộ nhớ đệm Integer

Viết chương trình tìm chính xác khoảng giá trị mà Integer được cache:

Khung
for (int i = -200; i <= 200; i++) {
    Integer a = i, b = i;
    if (a != b) {
        // in ra i đầu tiên KHÔNG được cache
    }
}

Sau đó trả lời:

  • Khoảng cache chính xác là bao nhiêu? Có đối xứng không?
  • Thử với Long, Short, Byte, Character, Boolean — khoảng cache có giống nhau không?
  • Chạy lại với tuỳ chọn JVM -XX:AutoBoxCacheMax=1000. Kết quả đổi thế nào? Giới hạn trên có đổi không? Giới hạn dưới thì sao?
Gợi ý

Đặc tả ngôn ngữ Java bắt buộc cache từ −128 đến 127 cho Integer, Short, Byte, Long; Character từ 0 đến 127; Boolean thì luôn dùng hai object TRUE/FALSE cố định. Giới hạn trên chỉnh được bằng -XX:AutoBoxCacheMax, giới hạn dưới thì không.
Đọc mã nguồn: ⌘B vào Integer.valueOf trong IntelliJ để thấy class IntegerCache thật.

Bộ chuyển đổi đơn vị an toàn

Viết class ChuyenDoi với các phương thức static:

API
static long ngaySangMili(int soNgay);           // không được tràn!
static String byteSangDocDuoc(long byteS);     // 1536 → "1,5 KB"
static double doCSangF(double c);             // F = C × 9/5 + 32
static String giaySangHhMmSs(long giay);      // 3725 → "01:02:05"
static int chuoiSangInt(String s, int macDinh);  // sai thì trả mặc định

Yêu cầu bắt buộc:

  • ngaySangMili(30000) phải đúng, không tràn.
  • doCSangF — thử với c = 100: nếu viết c * 9 / 5 có sai không? Còn c * (9 / 5)?
  • chuoiSangInt(null, 0) và chuoiSangInt("abc", -1) không được ném lỗi.
  • Viết main in bảng kiểm chứng cho mọi trường hợp biên.
Gợi ý

c * (9 / 5) luôn sai vì 9 / 5 là chia số nguyên → 1. Viết c * 9 / 5 hoặc c * 9.0 / 5.
ngaySangMili: soNgay * 24L * 60 * 60 * 1000 — chữ L phải xuất hiện sớm, trước khi giá trị vượt phạm vi int.
giaySangHhMmSs: String.format("%02d:%02d:%02d", h, m, s).
chuoiSangInt: bọc Integer.parseInt trong try/catch bắt NumberFormatException (và nhớ null cũng ném đúng ngoại lệ đó). Chương 6 sẽ nói kỹ về xử lý ngoại lệ.

Quiz

Q1

Kiểu nguyên thủy và kiểu tham chiếu khác nhau ra sao?

Xem đáp án
✓ Đáp án

Nguyên thủy (8 kiểu: byte, short, int, long, float, double, char, boolean): biến chứa chính giá trị, nằm trên stack nếu là biến cục bộ. Không phải object → không có phương thức, không nhận null. Mặc định là 0 / false. == so giá trị.

Tham chiếu (mọi thứ còn lại: String, mảng, class, wrapper, enum): biến chứa địa chỉ tới object nằm trên heap. Có phương thức, nhận null, mặc định là null. == so địa chỉ → phải dùng .equals() để so nội dung.

Ranh giới này là gốc rễ của phần lớn hành vi khó hiểu trong Java: bẫy Integer cache, String pool, và câu hỏi "Java truyền tham số theo giá trị hay tham chiếu".

Q2

Integer a = 127, b = 127; a == b cho true, nhưng với 128 lại cho false. Vì sao?

Xem đáp án
✓ Đáp án

Vì bộ nhớ đệm Integer. Khi autoboxing, Java gọi Integer.valueOf(int). Phương thức này giữ sẵn một mảng object cho các giá trị từ −128 đến 127 và trả về object đã có sẵn nếu giá trị nằm trong khoảng đó — nên a và b trỏ vào cùng một object, == đúng.

Ngoài khoảng đó, mỗi lần gọi tạo một object mới trên heap → hai địa chỉ khác nhau → == sai, dù giá trị bằng nhau.

Đây không phải chi tiết cài đặt ngẫu nhiên — nó được quy định trong đặc tả ngôn ngữ Java (JLS §5.1.7), vì khoảng số nhỏ này được dùng cực kỳ nhiều nên việc tái sử dụng object tiết kiệm đáng kể bộ nhớ.
Bài học: với mọi kiểu tham chiếu, kể cả wrapper, luôn dùng .equals().

Q3

Vì sao không được dùng double cho tiền tệ? Dùng gì thay thế?

Xem đáp án
✓ Đáp án

Vì double theo chuẩn IEEE 754 lưu số ở hệ nhị phân, mà nhiều phân số thập phân đơn giản lại là số vô hạn tuần hoàn trong hệ nhị phân — giống như 1/3 không biểu diễn hữu hạn được trong hệ thập phân. Kết quả: 0.1 + 0.2 cho 0.30000000000000004. Với hàng triệu giao dịch, sai số tích luỹ thành tiền thật, và nghiệp vụ kế toán không chấp nhận điều đó.

Hai giải pháp:

  • BigDecimal — chính xác tuyệt đối, kiểm soát được cách làm tròn. Lưu ý ba điều: constructor phải nhận String (truyền double là mang cả sai số vào); dùng phương thức add/multiply chứ không phải toán tử; so sánh bằng compareTo() == 0 chứ không phải equals() (vì 1.0 và 1.00 khác scale).
  • long theo đơn vị nhỏ nhất — lưu số xu thay vì số đô, số đồng thay vì nghìn đồng. Nhanh hơn BigDecimal rất nhiều, phù hợp khi chỉ cộng trừ nhân. Với VND đây thường là lựa chọn tốt vì không có đơn vị nhỏ hơn đồng.
Q4

String bất biến nghĩa là gì? Điều đó mang lại lợi ích gì?

Xem đáp án
✓ Đáp án

Bất biến (immutable) nghĩa là nội dung của một object String không bao giờ thay đổi sau khi tạo. Mọi phương thức như toUpperCase(), trim(), replace() đều trả về chuỗi mới, không sửa chuỗi gốc — nên bạn phải gán lại kết quả, nếu không nó bị bỏ đi.

Bốn lợi ích:

  • An toàn luồng miễn phí — nhiều luồng đọc chung không cần đồng bộ hoá.
  • Cho phép String pool — dùng chung object chỉ an toàn khi không ai sửa được nó.
  • Mã băm cache được — tính một lần rồi lưu, khiến String là khoá lý tưởng cho HashMap.
  • Bảo mật — đường dẫn tệp, URL, chuỗi kết nối đã kiểm tra không thể bị sửa ngầm bởi mã khác.

Cái giá phải trả: nối chuỗi trong vòng lặp tạo rất nhiều object trung gian → dùng StringBuilder ở đó.

Q5

Java truyền tham số theo giá trị hay theo tham chiếu?

Xem đáp án
✓ Đáp án

Luôn luôn theo giá trị — không có ngoại lệ. Thứ gây nhầm lẫn là: với kiểu tham chiếu, giá trị của biến chính là địa chỉ. Nên phương thức nhận được một bản sao của địa chỉ, trỏ vào cùng một object.

Hệ quả có thể kiểm chứng được:

  • Sửa nội dung object thì bên ngoài thấy — vì cả hai địa chỉ trỏ chung một object.
  • Gán biến tham số sang object khác thì bên ngoài KHÔNG thấy — vì chỉ bản sao địa chỉ bị đổi.

Chính điểm thứ hai là bằng chứng: nếu Java truyền theo tham chiếu thật (như int& của C++), gán lại tham số sẽ đổi được biến ở ngoài. Nó không đổi được.
Cách nói chính xác nhất: "truyền theo giá trị, trong đó giá trị của biến tham chiếu là địa chỉ".

Q6

7 / 2 cho 3. Làm sao để có 3.5? Còn -7 / 2 thì sao?

Xem đáp án
✓ Đáp án

Khi cả hai toán hạng là số nguyên, Java thực hiện chia nguyên và cắt bỏ phần thập phân. Chỉ cần một toán hạng là số thực thì kết quả là số thực:

Chia.java
7 / 2                 // 3
7.0 / 2               // 3.5
(double) 7 / 2      // 3.5  ✅ ép TRƯỚC khi chia
(double) (7 / 2)    // 3.0  ❌ ép SAU khi đã cắt — quá muộn

Với số âm, Java cắt về 0: -7 / 2 = -3 (không phải -4), và -7 % 2 = -1 — dấu của phần dư theo số bị chia. Điều này khác Python, nơi -7 // 2 = -4 và -7 % 2 = 1 (làm tròn xuống, dấu theo số chia).
Muốn hành vi kiểu Python: Math.floorDiv(-7, 2) và Math.floorMod(-7, 2).

Q7

Khi nào bắt buộc dùng StringBuilder thay +?

Xem đáp án
✓ Đáp án

Khi nối chuỗi trong vòng lặp. Vì String bất biến, mỗi lần s += x phải tạo một String mới và chép lại toàn bộ nội dung cũ. Với n lần lặp, tổng chi phí là O(n²) — nối 100.000 chuỗi mất vài giây thay vì vài mili giây.

Ngoài vòng lặp thì + hoàn toàn ổn. Trình biên dịch tự tối ưu một biểu thức như "Tên: " + ten + ", tuổi: " + tuoi thành một lời gọi hiệu quả (từ Java 9 dùng invokedynamic với StringConcatFactory). Viết StringBuilder ở đây chỉ làm code khó đọc mà không nhanh hơn.

Tối ưu thêm: nếu biết trước kích thước gần đúng, truyền dung lượng ban đầu — new StringBuilder(1024) — để tránh việc cấp phát lại mảng khi bộ đệm đầy.
StringBuffer là bản đồng bộ hoá của StringBuilder, chậm hơn và gần như không bao giờ cần trong code hiện đại.

Q8

var có biến Java thành ngôn ngữ định kiểu động không?

Xem đáp án
✓ Đáp án

Không. var chỉ là suy luận kiểu (type inference): trình biên dịch nhìn vế phải, xác định kiểu, rồi ghi kiểu đó vào bytecode. Sau bước biên dịch, var x = 10 và int x = 10 tạo ra bytecode hoàn toàn giống nhau — bạn kiểm chứng được bằng javap -c.

Kiểu vẫn cố định và vẫn được kiểm tra lúc biên dịch:

Var.java
var x = 10;      // x là int, mãi mãi
x = "chuỗi";      // ❌ error: incompatible types

Giới hạn: chỉ dùng cho biến cục bộ có khởi tạo — không dùng cho trường của class, tham số phương thức, kiểu trả về, hay var x; / var x = null;.

Khuyến nghị dùng: khi kiểu đã hiển hiện ở vế phải (var ds = new HashMap<String, List<DonHang>>()). Tránh khi nó che mất thông tin (var kq = xuLy() — người đọc không biết kiểu gì).

Tổng kết

Sau chương 2, bạn đã nắm:

  • 8 kiểu nguyên thủy với kích thước, phạm vi, giá trị mặc định; int và double là mặc định thực dụng.
  • Tràn số không báo lỗi — ép long từ toán hạng đầu, hoặc dùng Math.*Exact.
  • Chia số nguyên cắt phần thập phân; số âm cắt về 0 (khác Python).
  • IEEE 754 gây sai số — tiền tệ dùng BigDecimal hoặc long.
  • Nguyên thủy vs tham chiếu: stack/heap, == vs equals(), truyền tham số theo giá trị.
  • Wrapper & autoboxing: bẫy cache −128…127, NPE khi unboxing null, chi phí trong vòng lặp nóng.
  • String: bất biến, String pool, StringBuilder cho vòng lặp, text block cho nhiều dòng.
  • Ép kiểu: mở rộng tự động, thu hẹp phải khai báo; += ép ngầm âm thầm.
  • var là suy luận kiểu lúc biên dịch, không phải kiểu động; final khoá tham chiếu chứ không khoá nội dung.
  • Bảng toán tử đầy đủ, đoản mạch &&/||, và >>> đặc trưng Java.

Kết nối

  • Chương 3 (Điều khiển luồng & mảng) — mảng là kiểu tham chiếu; switch trên String tận dụng equals/hashCode.
  • Chương 4 (OOP cơ bản) — trường của class lấy giá trị mặc định (0/null), khác biến cục bộ; static final cho hằng số.
  • Chương 5 (OOP nâng cao) — equals()/hashCode() tự viết; record sinh sẵn cả hai.
  • Chương 7-8 (Collections & Map) — List<Integer> phải dùng wrapper; String là khoá lý tưởng nhờ hash được cache.
  • Chương 9 (Generics) — vì sao List<int> không tồn tại: type erasure chỉ làm việc với kiểu tham chiếu.
  • Chương 12 (Đa luồng) — bất biến khiến String an toàn luồng miễn phí; nguyên tử của phép toán trên long.
  • Python Chương 2 — đối chiếu: int của Python không tràn (độ chính xác vô hạn), không phân biệt nguyên thủy/tham chiếu, dùng Decimal thay BigDecimal.
  • JavaScript Chương 2 — JS chỉ có một kiểu số (double), nên gặp đúng vấn đề IEEE 754 nhưng không có lối thoát kiểu BigDecimal dựng sẵn.