- 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
BigDecimalkhi tính tiền, và biết vì saodoublekhông được phép. - Hiểu wrapper class, autoboxing, và bẫy
==với bộ nhớ đệmInteger. - 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àfinalcho 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
// 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
| Java | Python | JavaScript | |
|---|---|---|---|
| Khai báo | int x = 5; | x = 5 | let x = 5; |
| Kiểm tra kiểu | Lúc biên dịch | Lúc chạy | Lú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ệnh | Dấu ; bắt buộc | Xuố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ại | Quy ước | Ví dụ |
|---|---|---|
| Biến, tham số, phương thức | camelCase | soLuong, tinhTongTien() |
| Class, interface, enum, record | PascalCase | SinhVien, DonHang |
Hằng số (static final) | UPPER_SNAKE_CASE | MAX_SIZE, THUE_SUAT |
| Package | toàn chữ.thường | com.vidu.quanly |
| Biến boolean | Bắt đầu bằng is/has/can | isActive, hasPermission |
| Biến đếm trong vòng lặp | i, j, k | Ngoạ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ểu | Kích thước | Phạm vi | Mặc định | Wrapper | Dùng khi |
|---|---|---|---|---|---|
byte | 8 bit | −128 … 127 | 0 | Byte | Xử lý dữ liệu nhị phân, mảng byte |
short | 16 bit | −32.768 … 32.767 | 0 | Short | Hiếm dùng |
int ⭐ | 32 bit | ≈ −2,1 tỷ … 2,1 tỷ | 0 | Integer | Mặc định cho số nguyên |
long | 64 bit | ≈ ±9,2 × 10¹⁸ | 0L | Long | Timestamp, ID, tiền (đơn vị nhỏ nhất) |
float | 32 bit | ≈ ±3,4 × 10³⁸ (7 chữ số) | 0.0f | Float | Hiếm dùng — độ chính xác thấp |
double ⭐ | 64 bit | ≈ ±1,8 × 10³⁰⁸ (15-16 chữ số) | 0.0d | Double | Mặc định cho số thực |
char | 16 bit | 0 … 65.535 (một đơn vị mã UTF-16) | ' ' | Character | Một ký tự |
boolean | không xác định* | true / false | false | Boolean | Đ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.
// 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
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
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
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ó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
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 ✅
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
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.
int tuoi → 22double d → 8.75boolean b → truegiá trị nằm THẲNG trong biến
String s → 0x7f3a ─┐int[] a → 0x7f8c ─┼→ objectSinhVien sv → null │ trên heapbiến chỉ giữ ĐỊA CHỈ
null nếu chưa trỏ vào đâu.
| Kiểu nguyên thủy | Kiể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 ở đâu | Stack (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í) |
// --- 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.
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.
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
== với IntegerInteger 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".
// ① 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
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
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
Stringmà 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ếnStringtrở thành khoá lý tưởng choHashMap. - 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
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
==. 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
// ❌ 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();
+ 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.
| Class | Sử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+)
// ❌ 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
// 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
// --- 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();
var khi nào// ✅ 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ên | Ví dụ | Ghi chú |
|---|---|---|---|
+ | Cộng / nối chuỗi | 7 + 2 → 9 | Có một toán hạng là String thì nối chuỗi |
- * | Trừ, nhân | 7 * 2 → 14 | |
/ | Chia | 7 / 2 → 3 | Hai int → chia nguyên |
% | Chia lấy dư | -7 % 2 → -1 | Dấu theo số bị chia (khác Python) |
++ -- | Tăng / giảm 1 | i++, ++i | Tiền tố trả giá trị sau, hậu tố trả giá trị trước |
+= -= *= /= %= | Gán ghép | x += 5 | Tự ép kiểu ngầm — xem bẫy bên dưới |
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ĩa | Lưu ý quan trọng |
|---|---|---|
== != | Bằng / khác | Nguyê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ủ định | Chỉ áp cho boolean |
? : | Ba ngôi | dieuKien ? a : b — biểu thức, có giá trị trả về |
instanceof | Kiểm tra kiểu | Có pattern matching từ Java 16 — xem chương 5 |
// ⭐ Đ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ên | Ví dụ | Kết quả |
|---|---|---|---|
& | AND bit | 12 & 10 | 8 (1100 & 1010 = 1000) |
| | OR bit | 12 | 10 | 14 |
^ | XOR bit | 12 ^ 10 | 6 |
~ | NOT bit | ~12 | -13 (~x == -x-1) |
<< | Dịch trái | 3 << 2 | 12 (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 >>> 1 | 2147483644 (đ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ức | Toá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) | = += -= … |
// 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ống | Cách làm |
|---|---|
| Số nguyên mặc định | int; vượt 2,1 tỷ thì long (nhớ hậu tố L) |
| Số thực mặc định | double |
| Tiền tệ | BigDecimal (constructor nhận String) hoặc long theo đơn vị nhỏ nhất |
| So sánh chuỗi | a.equals(b) — không bao giờ == |
| So sánh wrapper | a.equals(b) — nhớ bẫy cache −128…127 |
So sánh BigDecimal | a.compareTo(b) == 0, không phải equals |
So sánh double | Math.abs(a - b) < 1e-9 |
Kiểm tra NaN | Double.isNaN(x) |
| Nối chuỗi trong vòng lặp | StringBuilder.append() |
| Chuỗi nhiều dòng | Text 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àn | Math.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ỗi | String.valueOf(n) |
| Hằng số | public static final + UPPER_SNAKE_CASE |
| Tránh NPE khi kiểm tra | x != 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ế.
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:
// 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ặpStringBuilder.append(i)StringBuildervớ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:
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:
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ớic = 100: nếu viếtc * 9 / 5có sai không? Cònc * (9 / 5)?chuoiSangInt(null, 0)vàchuoiSangInt("abc", -1)không được ném lỗi.- Viết
mainin 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
Kiểu nguyên thủy và kiểu tham chiếu khác nhau ra sao?
Xem đá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".
Integer a = 127, b = 127; a == b cho true, nhưng với 128 lại cho false. Vì sao?
Xem đá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().
Vì sao không được dùng double cho tiền tệ? Dùng gì thay thế?
Xem đá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ậnString(truyềndoublelà mang cả sai số vào); dùng phương thứcadd/multiplychứ không phải toán tử; so sánh bằngcompareTo() == 0chứ không phảiequals()(vì1.0và1.00khác scale).longtheo đơn vị nhỏ nhất — lưu số xu thay vì số đô, số đồng thay vì nghìn đồng. Nhanh hơnBigDecimalrấ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.
String bất biến nghĩa là gì? Điều đó mang lại lợi ích gì?
Xem đá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
Stringlà khoá lý tưởng choHashMap. - 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 ở đó.
Java truyền tham số theo giá trị hay theo tham chiếu?
Xem đá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ỉ".
7 / 2 cho 3. Làm sao để có 3.5? Còn -7 / 2 thì sao?
Xem đá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:
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).
Khi nào bắt buộc dùng StringBuilder thay +?
Xem đá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.
var có biến Java thành ngôn ngữ định kiểu động không?
Xem đá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 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;
intvàdoublelà mặc định thực dụng. - Tràn số không báo lỗi — ép
longtừ toán hạng đầu, hoặc dùngMath.*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
BigDecimalhoặclong. - Nguyên thủy vs tham chiếu: stack/heap,
==vsequals(), 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,StringBuildercho 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. varlà suy luận kiểu lúc biên dịch, không phải kiểu động;finalkhoá 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;
switchtrênStringtận dụngequals/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 finalcho hằng số. - Chương 5 (OOP nâng cao) —
equals()/hashCode()tự viết;recordsinh sẵn cả hai. - Chương 7-8 (Collections & Map) —
List<Integer>phải dùng wrapper;Stringlà 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
Stringan toàn luồng miễn phí; nguyên tử của phép toán trênlong. - Python Chương 2 — đối chiếu:
intcủ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ùngDecimalthayBigDecimal. - 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ểuBigDecimaldựng sẵn.