volatile hay HashMap.Tổng quan & phiên bản
Nắm bốn khái niệm nền trước khi đi vào chi tiết: JDK/JRE/JVM, bytecode, cách JVM thực thi, và mốc tính năng của từng phiên bản.
JDK, JRE, JVM khác nhau thế nào
| Thành phần | Bên trong | Dùng để |
|---|---|---|
| JVM | Máy ảo: class loader, interpreter, JIT, GC | Chạy file .class |
| JRE | JVM + thư viện chuẩn (core classes) | Chạy ứng dụng đã build |
| JDK | JRE + javac, jar, javadoc, jcmd… | Phát triển và build |
Từ Java 11 không còn JRE tách rời chính thức; muốn “chỉ chạy” thì dùng jlink tạo runtime image tối giản.
Từ source tới lúc chạy
Bytecode là ngôn ngữ trung gian cho JVM, không phải mã máy của CPU. Nhờ vậy cùng một file .class chạy được trên Windows, Linux, macOS — miễn có JVM tương thích.
Vì sao Java “write once, run anywhere”
javacchỉ dịch tới bytecode, không gắn với CPU- JVM chịu trách nhiệm hiểu bytecode trên từng nền tảng
- JIT biên dịch phần “nóng” thành mã máy tại thời điểm chạy
- Hệ quả: khởi động chậm hơn native, nhưng tối ưu dần theo profile thực tế
“Đa nền tảng” là ở mức bytecode. Gọi native library, hard-code đường dẫn hay dùng sun.misc.Unsafe vẫn có thể làm hỏng tính di động.
Mốc tính năng cần nhớ
| Phiên bản | Tính năng ảnh hưởng nhiều nhất |
|---|---|
| Java 8 — LTS | Lambda, Stream API, Optional, default method, java.time, CompletableFuture, Metaspace thay PermGen |
| Java 9 | Module (JPMS), List.of/Map.of, JShell, G1 thành GC mặc định |
| Java 11 — LTS | var, HttpClient, String.isBlank/strip/lines/repeat, chạy file nguồn trực tiếp, bỏ Java EE khỏi JDK |
| Java 14–16 | switch expression, instanceof pattern, text block, record, Stream.toList() |
| Java 17 — LTS | Sealed class, pattern matching cho switch (preview), đóng gói mạnh module JDK, RandomGenerator |
| Java 21 — LTS | Virtual thread, pattern matching switch chính thức, record pattern, sequenced collection, ZGC phân thế hệ |
| Java 25 — LTS | Scoped value, module import, compact source file + void main(), flexible constructor body, compact object header |
Nên bám Java 17/21 vì đa số dự án production đang ở đó; biết thêm 25 để trả lời câu “em biết gì mới?”. Mọi ví dụ trong file dùng cú pháp từ Java 17 trở lên, trừ khi ghi rõ.
Ba câu mở đầu hay gặp
- JVM, JRE, JDK khác gì nhau?
- Vì sao Java chạy được đa nền tảng?
- Java biên dịch hay thông dịch?
Gợi ý: Java là cả hai — javac biên dịch trước sang bytecode, JVM thông dịch lúc chạy và JIT biên dịch nóng sang mã máy.
Thuật ngữ nên dùng đúng
| bytecode | Mã trung gian trong .class |
| JIT | Just-In-Time compiler (C1/C2) |
| GC | Garbage Collector, thu hồi vùng không còn reachable |
| JMM | Java Memory Model — quy tắc visibility/ordering |
| LTS | Long-Term Support: 8, 11, 17, 21, 25… |
JVM & bộ nhớ
Câu hỏi “heap và stack khác gì” xuất hiện ở gần như mọi buổi kiểm tra Java. Nắm sơ đồ bộ nhớ, class loading và GC là đủ để trả lời tự tin.
Các vùng nhớ của JVM
Heap và Metaspace dùng chung, bị GC quản lý. Stack, PC, native stack là per-thread. Từ Java 8, PermGen bị thay bằng Metaspace (nằm ngoài heap, mặc định không giới hạn).
Stack vs Heap — lỗi đi kèm
- Stack: nhỏ, tự động thu hồi khi method return, chứa primitive và reference
- Heap: lớn, cấp phát object, GC mới thu hồi
- StackOverflow là
Error; hết heap làOutOfMemoryError
Class loading
| Giai đoạn | Việc xảy ra |
|---|---|
| Loading | Đọc .class, tạo đối tượng Class |
| Verify | Kiểm tra bytecode hợp lệ, an toàn |
| Prepare | Cấp phát static field, gán giá trị mặc định |
| Resolve | Tham chiếu tượng trưng → tham chiếu trực tiếp |
| Initialize | Chạy static {} và static initializer |
Parent delegation: Bootstrap → Platform → Application. Nhờ đó không thể ghi đè java.lang.String bằng class tự viết, và tránh nạp trùng.
Thứ tự khởi tạo — câu hỏi kinh điển
Static block chạy một lần khi class được khởi tạo (cha trước con). Instance initializer + field chạy trước constructor, cũng cha trước con.
GC: cách nghĩ đúng
- GC tìm object không còn reachable từ GC root (biến stack, static, thread, JNI)
- Không phải “đếm reference” — vòng tham chiếu vẫn bị thu hồi
- Heap chia thế hệ: object mới ở Young, sống lâu được promote lên Old
- Minor GC (Young) nhanh, thường stop-the-world rất ngắn; Major/Full GC đắt
| Collector | Đặc điểm |
|---|---|
| Serial | 1 thread, heap nhỏ, client |
| Parallel | Nhiều thread, ưu tiên throughput, batch |
| G1 | Mặc định từ Java 9, chia region, đặt mục tiêu pause |
| ZGC | Pause cực thấp, heap rất lớn; phân thế hệ mặc định từ 23 |
| Shenandoah | Pause thấp, có trong OpenJDK |
| Epsilon | Không thu hồi — dùng để test/benchmark |
JIT và tối ưu lúc chạy
- Ban đầu JVM thông dịch bytecode
- Method “nóng” được C1 (nhanh, ít tối ưu) rồi C2 (tối ưu sâu) biên dịch
- Tiered compilation bật mặc định; có thể deoptimize nếu giả định sai
- Escape analysis cho phép cấp phát object trên stack hoặc bỏ hẳn nếu không thoát ra ngoài
- Vì vậy micro-benchmark phải dùng JMH, không đo bằng
System.nanoTime()thủ công
JIT giải thích vì sao Java có thể nhanh gần native dù khởi động chậm: nó tối ưu dựa trên dữ liệu chạy thật, thứ mà AOT truyền thống không có.
Công cụ chẩn đoán nên biết
| jps | Liệt kê tiến trình Java + PID |
| jstack | Thread dump — tìm deadlock, thread bị block |
| jmap | Heap dump, -histo:live xem object chiếm nhiều |
| jstat | Theo dõi GC theo thời gian thực |
| jcmd | “Dao đa năng”: GC.heap_info, Thread.print, JFR |
| JFR | Flight Recorder — profile production gần như miễn phí |
Khi bị OutOfMemoryError, bật heap dump tự động rồi phân tích bằng Eclipse MAT hoặc VisualVM.
Các loại OOM thường gặp
Java heap space— rò rỉ bộ nhớ hoặc heap quá nhỏMetaspace— sinh class liên tục (proxy, script engine, hot reload)GC overhead limit exceeded— GC chạy quá nhiều mà thu hồi quá ítDirect buffer memory— NIOByteBuffer.allocateDirectkhông được GC kịpunable to create native thread— quá nhiều thread hoặc hết bộ nhớ native
“Tăng -Xmx” chỉ là giải pháp tạm. Phải tìm chỗ giữ reference: static collection, cache không giới hạn, ThreadLocal trong pool, listener không remove.
Kiểu dữ liệu, boxing & equals
Phần “dễ mà hay sai”: primitive vs wrapper, bẫy ==, Integer cache, pass-by-value và immutability.
8 kiểu nguyên thuỷ
| Kiểu | Bit | Khoảng / ghi chú |
|---|---|---|
| byte | 8 | −128 … 127 |
| short | 16 | −32 768 … 32 767 |
| int | 32 | −2³¹ … 2³¹−1, mặc định của số nguyên |
| long | 64 | Hậu tố L |
| float | 32 | Hậu tố f, ít chính xác |
| double | 64 | Mặc định cho số thực |
| char | 16 | Không dấu, 0 … 65 535 (UTF-16 code unit) |
| boolean | — | JVM quyết định; trong mảng thường 1 byte |
Không có kiểu unsigned (trừ char); Java 8 thêm Integer.toUnsignedString, compareUnsigned…
Wrapper, autoboxing và Integer cache
Ba bẫy: (1) so sánh wrapper bằng ==; (2) equals giữa hai kiểu wrapper khác nhau luôn false; (3) unboxing null → NPE, kể cả trong biểu thức ba ngôi.
== và equals
| Toán hạng | Ý nghĩa |
|---|---|
| primitive | So sánh giá trị |
| reference | So sánh địa chỉ (cùng object hay không) |
equals | Mặc định cũng là ==; lớp con override để so nội dung |
String, wrapper, BigDecimal, enum, record đều đã override equals. Class tự viết thì phải tự làm.
Java luôn pass-by-value
Với object, Java copy giá trị của tham chiếu. Vì vậy sửa nội dung object thì thấy được, nhưng gán lại biến tham chiếu thì không ảnh hưởng bên ngoài.
Class immutable — công thức
final class, fieldprivate final, không setter- Copy phòng vệ khi nhận và khi trả collection/array/mutable object
- Immutable tự động thread-safe, an toàn làm key của map
final · finally · finalize
| final | Biến không gán lại; method không override; class không kế thừa |
| finally | Khối luôn chạy sau try/catch (trừ System.exit hoặc JVM crash) |
| finalize | Deprecated, không đảm bảo được gọi — đừng dùng để đóng tài nguyên |
return trong finally nuốt exception và giá trị trả về trước đó. Tuyệt đối tránh.
static — bốn cách dùng
- Field: thuộc class, dùng chung mọi instance
- Method: không có
this, không override được (chỉ hiding) - Block: chạy một lần khi class được khởi tạo
- Nested class: không giữ tham chiếu ngầm tới instance ngoài
- Import static: gọi trực tiếp hằng số/method
Static field giữ reference sống suốt vòng đời classloader — đây là một trong những nguyên nhân memory leak phổ biến nhất.
BigDecimal — tiền phải dùng kiểu này
- Luôn dùng constructor nhận
StringhoặcvalueOf - Chia phải chỉ định scale +
RoundingMode, nếu không có thể némArithmeticException - So sánh bằng
compareTo == 0, không dùngequals
var — chỉ là suy luận kiểu
var chỉ dùng cho biến local, tham số lambda, biến vòng lặp. Không dùng cho field, tham số method hay kiểu trả về. Đây là static typing, không phải dynamic typing.
Tràn số, chia và ép kiểu
So sánh số thực bằng == rất dễ sai: dùng Math.abs(a - b) < 1e-9 hoặc BigDecimal. Với NaN, mọi so sánh đều false, kể cả NaN == NaN.
OOP & thiết kế
Không chỉ là “4 tính chất”. Kiểm tra hay hỏi overloading/overriding, abstract vs interface, thứ tự khởi tạo và equals/hashCode.
Bốn trụ cột — và Java hiện thực bằng gì
| Trụ cột | Ý nghĩa | Công cụ Java |
|---|---|---|
| Encapsulation | Giấu trạng thái, chỉ lộ hành vi; giữ invariant luôn đúng | private field + method, package-private |
| Inheritance | Tái sử dụng theo quan hệ is-a | extends, super |
| Polymorphism | Cùng lời gọi, hành vi khác nhau tùy đối tượng thực | Override (runtime), overload (compile-time) |
| Abstraction | Chỉ mô tả “làm gì”, ẩn “làm thế nào” | abstract class, interface |
Encapsulation không phải “viết getter/setter cho mọi field”. Nếu setter cho phép trạng thái vô lý (tuổi âm, số dư âm) thì đã phá vỡ đóng gói.
Overloading vs Overriding
| Overloading | Overriding | |
|---|---|---|
| Chọn khi | Compile-time | Runtime (dispatch) |
| Điều kiện | Khác danh sách tham số | Cùng signature |
| Kiểu trả về | Tùy ý | Covariant (kiểu con) |
| Access | Tùy ý | Không được thu hẹp |
| throws | Tùy ý | Không mở rộng checked |
| static/final/private | Không liên quan | Không override được |
Chọn overload nào?
Thứ tự ưu tiên: khớp chính xác → widening primitive → boxing → varargs. Tránh overload chỉ khác wrapper/primitive vì rất dễ gọi nhầm.
Abstract class vs Interface
| Abstract class | Interface | |
|---|---|---|
| Trạng thái | Field instance bình thường | Chỉ hằng public static final |
| Constructor | Có | Không |
| Method | Abstract + concrete | Abstract, default, static, private |
| Đa kế thừa | Không | Có (nhiều interface) |
| Khi dùng | Chia sẻ state + hành vi trong cùng họ | Hợp đồng / khả năng, cho phép ghép |
Nguyên tắc thực dụng: interface để định nghĩa kiểu, abstract class để tái sử dụng phần chung. Ưu tiên interface vì không khóa cây kế thừa.
Default method và xung đột hình thoi
- Class thắng interface: method trong class cha luôn được ưu tiên
- Hai interface: interface “cụ thể hơn” (extends interface kia) thắng
- Không phân định được → bắt buộc override, có thể gọi
X.super.method()
Constructor: this(), super() và bẫy gọi virtual
Constructor chạy trước khi field của lớp con được khởi tạo. Gọi method overridable trong constructor là lỗi thiết kế kinh điển — field còn giá trị mặc định.
equals và hashCode
a.equals(b)→a.hashCode() == b.hashCode()(bắt buộc)- Hash bằng nhau không suy ra equals
- Field dùng trong equals phải giữ nguyên — đổi sau khi put vào HashSet sẽ “mất” phần tử
- Override equals mà quên hashCode:
HashMap/HashSethoạt động sai
Class lồng nhau
| static nested | Không cần instance ngoài — ưu tiên dùng |
| inner | Giữ ngầm Outer.this, dễ gây leak |
| local | Trong method, chỉ thấy biến effectively final |
| anonymous | Triển khai tại chỗ; lambda thay thế cho functional interface |
enum — không chỉ là hằng số
- Enum ngầm kế thừa
java.lang.Enum, không extends class khác, nhưng implement interface được - Có
values(),valueOf(),name(),ordinal(); so sánh bằng== - Enum singleton an toàn trước serialization và reflection — cách làm singleton được khuyên dùng
Composition thay vì kế thừa
Kế thừa làm lớp con phụ thuộc vào cài đặt của lớp cha (fragile base class). Chỉ kế thừa khi thật sự is-a và lớp cha được thiết kế để kế thừa.
SOLID trong một dòng
| S | Một class chỉ một lý do để thay đổi |
| O | Mở để mở rộng, đóng để sửa (thêm class mới, không sửa if/else cũ) |
| L | Lớp con thay được lớp cha mà không phá hành vi |
| I | Nhiều interface nhỏ tốt hơn một interface béo |
| D | Phụ thuộc abstraction, không phụ thuộc cài đặt cụ thể |
Đừng học thuộc; hãy gắn mỗi nguyên tắc với một ví dụ bạn đã từng sửa code.
Pattern hay gặp khi kiểm tra
| Pattern | Vấn đề giải quyết | Dấu hiệu nhận biết |
|---|---|---|
| Singleton | Duy nhất một instance toàn cục | enum, holder idiom, DCL + volatile |
| Factory Method | Tạo object mà không lộ class cụ thể | Trả về interface, nhánh if/switch theo loại |
| Builder | Object nhiều tham số tùy chọn, immutable | Chuỗi gọi .name().age().build() |
| Strategy | Hoán đổi thuật toán lúc chạy | Inject interface hành vi vào constructor |
| Observer | Thông báo khi trạng thái đổi | Danh sách listener, publish/subscribe |
| Decorator | Thêm hành vi mà không sửa class gốc | Bọc cùng interface: new Buffered(new File()) |
| Template Method | Khung thuật toán, lớp con điền bước | Method final gọi các abstract step |
| Proxy | Kiểm soát truy cập, thêm cross-cutting | Spring AOP, lazy loading |
String & text
String là kiểu được dùng nhiều nhất và cũng là nguồn câu hỏi bẫy nhiều nhất: pool, immutability, cộng chuỗi trong vòng lặp, Unicode.
Vì sao String immutable?
- An toàn chia sẻ giữa các thread, không cần đồng bộ
- Cho phép string pool: nhiều biến cùng trỏ một object, tiết kiệm bộ nhớ
- An toàn khi dùng làm key
HashMaphoặc tham số bảo mật (đường dẫn, SQL) - HashCode được cache — tính một lần
Mọi method “thay đổi” (toUpperCase, substring, replace) đều trả object mới. Gán lại mới thấy kết quả.
Pool và intern()
Chuỗi nối từ hằng số biên dịch cũng vào pool: "a" + "b" tương đương literal "ab". Nhưng nối với biến thì tính lúc chạy, ra object mới.
Cộng chuỗi trong vòng lặp
+trong một biểu thức được javac dịch thànhStringBuilder— ổn+=trong vòng lặp tạoStringBuildermới mỗi lần — rất tốnStringBuilder: nhanh, không đồng bộ;StringBuffer: cósynchronized, chậm hơn- Đặt capacity trước nếu biết độ dài gần đúng
API String nên nhớ
| length / charAt | Độ dài, ký tự tại vị trí |
| substring(a, b) | Cắt chuỗi, b không bao gồm |
| indexOf / lastIndexOf | Tìm vị trí, trả −1 nếu không thấy |
| contains / startsWith / endsWith | Kiểm tra chứa/tiền tố/hậu tố |
| equals / equalsIgnoreCase | So nội dung (không dùng ==) |
| compareTo | Thứ tự từ điển, dùng để sort |
| replace / replaceAll | Literal vs regex |
| split / join | Tách theo regex, ghép có delimiter |
| strip / trim | Bỏ khoảng trắng Unicode / ASCII |
| isBlank / isEmpty | Chỉ khoảng trắng / độ dài 0 |
| repeat / indent / lines | Java 11+ |
| formatted / format | Chèn tham số kiểu printf |
| toCharArray / getBytes | Chuyển đổi, chỉ định charset |
Text block
"""mở phải xuống dòng ngay- Thụt lề chung nhỏ nhất bị loại bỏ (incidental whitespace)
\cuối dòng để nối,\sđể giữ khoảng trắng cuối- Dùng
"""...""".formatted(x)thay vì nối chuỗi
Những bẫy nhỏ nhưng hay hỏi
String.format dùng locale mặc định: máy cấu hình tiếng Việt có thể ra 1.234,56. Với dữ liệu máy đọc, truyền Locale.US.
Unicode: length không phải số ký tự
Xử lý văn bản người dùng: dùng codePointCount, Normalizer, và BreakIterator khi cắt theo từ.
Ghép chuỗi có delimiter
Đừng nối bằng vòng lặp và tự xử lý dấu phẩy — dễ thừa/thiếu delimiter ở đầu/cuối.
Collections
Chọn đúng cấu trúc dữ liệu là 80% câu trả lời. Phần còn lại là hiểu bên trong ArrayList, HashMap và hợp đồng equals/hashCode.
Bản đồ tổng quát
| Nhóm | Implementation chính | Đặc điểm quyết định |
|---|---|---|
| List | ArrayList, LinkedList, CopyOnWriteArrayList | Có thứ tự, truy cập theo index, cho phép trùng |
| Set | HashSet, LinkedHashSet, TreeSet | Không trùng; khác nhau ở thứ tự và chi phí |
| Queue / Deque | ArrayDeque, PriorityQueue, BlockingQueue | FIFO/LIFO, ưu tiên, hàng đợi đồng bộ |
| Map | HashMap, LinkedHashMap, TreeMap, ConcurrentHashMap | Key-value, không nằm trong Collection |
Map không kế thừa Collection. Đây là câu hỏi kiểm tra bạn có đọc hierarchy thật hay không. Ba nhóm con của Collection là List, Set, Queue.
ArrayList vs LinkedList
| Thao tác | ArrayList | LinkedList |
|---|---|---|
| get(i) | O(1) | O(n) |
| add cuối | O(1) khấu hao | O(1) |
| add/remove giữa | O(n) dịch mảng | O(n) tìm vị trí |
| Bộ nhớ | Mảng liền, gọn | Node + 2 con trỏ, tốn hơn |
| Cache CPU | Tốt | Kém |
Thực tế: mặc định dùng ArrayList. LinkedList chỉ hơn khi chèn/xóa ở hai đầu — mà việc đó ArrayDeque làm tốt hơn.
ArrayList bên trong
- Mảng
Object[] elementData+sizeriêng - Capacity mặc định 10, nhưng chỉ cấp phát ở lần
addđầu tiên - Khi đầy: tăng khoảng 1.5 lần (
old + (old >> 1)) rồiArrays.copyOf trimToSize()thu gọn;ensureCapacity()giảm số lần cấp phátsubList()là view, không phải bản copy — sửa view ảnh hưởng list gốc
HashMap hoạt động thế nào
| Bước | Chi tiết |
|---|---|
| 1. Hash | h = key.hashCode(), sau đó “spread”: h ^ (h >>> 16) |
| 2. Bucket | Index = (n - 1) & hash — vì n là luỹ thừa 2 nên tương đương modulo |
| 3. Va chạm | Bucket là linked list; Java 8 chuyển thành cây đỏ-đen khi đủ điều kiện |
| 4. Treeify | Bucket ≥ 8 node và table ≥ 64; nếu table nhỏ hơn thì resize trước |
| 5. Untreeify | Giảm còn 6 node thì quay lại linked list |
| 6. Resize | Khi size > capacity × 0.75: nhân đôi capacity, chia lại bucket |
| 7. Độ phức tạp | Trung bình O(1); xấu nhất O(log n) với tree, O(n) với list |
Vì index dùng (n-1) & hash, capacity luôn là luỹ thừa 2. HashMap không thread-safe; Java 8 không còn vòng lặp vô hạn như Java 7 nhưng vẫn có thể mất dữ liệu khi ghi đồng thời.
LinkedHashMap làm LRU cache
LinkedHashMap giữ thứ tự chèn; với accessOrder = true nó đổi thứ tự mỗi lần get — đúng hành vi LRU.
HashSet, TreeSet, LinkedHashSet
| Loại | Thứ tự | Chi phí |
|---|---|---|
| HashSet | Không đảm bảo | O(1) |
| LinkedHashSet | Theo lúc chèn | O(1) |
| TreeSet | Đã sắp xếp | O(log n) |
HashSet thực chất bọc một HashMap với value giả. TreeSet dùng compareTo/Comparator để xác định trùng, không dùng equals.
Queue và Deque
| ArrayDeque | Stack/queue nhanh nhất, cấm null |
| LinkedList | Cũng là Deque nhưng chậm hơn |
| PriorityQueue | Heap nhị phân, poll theo ưu tiên; duyệt không có thứ tự |
| ArrayBlockingQueue | Có giới hạn, dùng cho producer-consumer |
| LinkedBlockingQueue | Có thể giới hạn; mặc định gần như vô hạn |
| SynchronousQueue | Bàn giao trực tiếp, không lưu trữ |
| DelayQueue | Phần tử chỉ “nở” sau thời gian delay |
Key hỏng làm hỏng HashMap
Dùng object mutable làm key cũng nguy hiểm: sửa field tham gia hashCode sau khi put khiến entry không thể tìm lại. Key nên immutable.
Comparable vs Comparator
Comparable: “thứ tự tự nhiên” của class, một cách duy nhấtComparator: nhiều cách sắp xếp, truyền từ ngoài vào, không sửa classcompareTonên nhất quán vớiequalsnếu dùng trongTreeSet/TreeMap
Fail-fast và xóa phần tử
Iterator so sánh modCount với giá trị lúc tạo và ném CME — cơ chế best-effort, không đảm bảo phát hiện mọi trường hợp.
“Không sửa được” có ba loại
Arrays.asList trả list fixed-size và ghi trực tiếp xuống mảng gốc. List.of mới thật sự immutable và không chấp nhận null.
Chọn nhanh
| Cần index, duyệt nhiều | ArrayList |
| Chèn/xóa hai đầu | ArrayDeque |
| Key-value, tra cứu | HashMap |
| Giữ thứ tự chèn | LinkedHashMap |
| Cần sắp xếp theo key | TreeMap |
| Đa thread đọc/ghi | ConcurrentHashMap |
| Đọc cực nhiều, ghi ít | CopyOnWriteArrayList |
| Producer-consumer | BlockingQueue |
| Ưu tiên động | PriorityQueue |
Với HashMap biết trước số phần tử, khởi tạo new HashMap<>(n * 4 / 3 + 1) để tránh resize.
Generics
Generics của Java là compile-time và bị xoá kiểu (type erasure) lúc chạy. Hiểu điều này là hiểu vì sao có wildcard, bridge method và heap pollution.
Vì sao cần generics
Ba lợi ích: kiểm tra kiểu sớm, bỏ cast, API tự mô tả. Generics không tồn tại lúc runtime — đó là cái giá của tương thích ngược.
Type erasure — hệ quả
- Không biết kiểu generic lúc runtime → không thể
new T() - Không overload hai method chỉ khác type argument
- Không dùng primitive làm tham số kiểu — phải dùng wrapper
- Static member không dùng được type parameter của class
- Bù bằng
Class<T>,TypeToken, hoặc factory method
Bounded type parameter
Chỉ một class bound và phải đứng đầu; sau đó là các interface. Nhờ bound, bên trong method gọi được method của kiểu giới hạn.
Wildcards và PECS
| Dạng | Đọc (get) | Ghi (add) | Dùng khi |
|---|---|---|---|
List<T> | T | T | Cần cả đọc và ghi |
List<? extends T> | T | Không (trừ null) | Nguồn dữ liệu — producer |
List<? super T> | Object | T | Đích nhận dữ liệu — consumer |
List<?> | Object | Không (trừ null) | Chỉ đọc, không biết kiểu |
PECS — Producer Extends, Consumer Super. Nhớ bằng câu: “đọc thì extends, ghi thì super”. List<?> khác List<Object>: cái sau vẫn add(new Object()) được.
Generic method và suy luận kiểu
Compiler suy luận từ đối số và ngữ cảnh. Khi không suy ra được, chỉ định tường minh: Collections.<String>emptyList().
Bridge method — chuyện ít ai để ý
Do erasure, lớp cha thật sự có set(Object). Compiler tạo bridge để đảm bảo dispatch đúng — đó là lý do getDeclaredMethods() đôi khi thấy method “lạ”.
Heap pollution và @SafeVarargs
Chỉ dùng @SafeVarargs khi thật sự không ghi gì vào mảng varargs. Nếu không, lỗi ClassCastException sẽ nổ ở nơi khác, rất khó truy.
Wildcard capture
“Capture” là khi compiler tạm đặt tên cho kiểu chưa biết. Helper generic giúp chuyển List<?> thành List<T> để thao tác.
Exception
Câu hỏi không chỉ là “checked khác unchecked thế nào” mà còn: khi nào tự tạo exception, try-with-resources đóng theo thứ tự nào, và vì sao nuốt exception là lỗi.
Cây phân cấp
Error là lỗi JVM/môi trường — không nên bắt. RuntimeException là lỗi lập trình. Checked exception là điều kiện mà caller có thể xử lý.
Checked vs Unchecked
| Checked | Unchecked | |
|---|---|---|
| Gốc | Exception (trừ RuntimeException) | RuntimeException, Error |
| Compiler | Bắt catch hoặc khai báo throws | Không bắt buộc |
| Ví dụ | IOException, SQLException | NPE, IllegalArgument, ClassCast |
| Ý nghĩa | Tình huống ngoài tầm kiểm soát, có thể phục hồi | Bug lập trình, vi phạm hợp đồng |
Lạm dụng checked exception làm chữ ký method rối và sinh try/catch rỗng. Đa số lỗi nghiệp vụ nên là unchecked + xử lý tập trung ở @ControllerAdvice.
try-with-resources
- Đóng theo thứ tự ngược lúc mở
- Exception khi đóng không mất — thành suppressed
- Chỉ nhận class implement
AutoCloseable - Viết ngắn hơn
finallyvà không quên đóng khi có exception
Tự tạo exception
- Kế thừa
RuntimeExceptioncho lỗi nghiệp vụ; checked khi buộc caller xử lý - Truyền
causeđể không mất stack trace gốc - Đặt tên theo tình huống, không theo tầng (
UserNotFoundExceptiontốt hơnServiceException)
Bắt và ném lại đúng cách
Đừng catch (Exception e) { log.error(e); } rồi đi tiếp — lỗi bị nuốt, transaction có thể đã rollback mà caller tưởng thành công. Cũng đừng log rồi ném lại ở nhiều tầng: log một lần ở biên.
Exception hay gặp và nguyên nhân
| NullPointerException | Gọi method/field trên null, unboxing null |
| ClassCastException | Ép kiểu sai; generics bị erasure có thể gây |
| IllegalArgumentException | Tham số sai hợp đồng |
| IllegalStateException | Đúng kiểu nhưng sai trạng thái (gọi next() khi hết) |
| IndexOutOfBounds | Index âm hoặc ≥ size |
| ConcurrentModificationException | Sửa collection khi đang duyệt |
| UnsupportedOperationException | Sửa collection immutable/fixed-size |
| NumberFormatException | Parse chuỗi không phải số |
Từ Java 14 (bật mặc định từ 15), NPE có “helpful message” chỉ rõ biểu thức nào null — hữu ích khi debug.
Quy tắc vàng khi xử lý lỗi
- Bắt cụ thể nhất có thể; tránh
catch (Throwable) - Không dùng exception cho luồng điều khiển bình thường
- Không để
catchrỗng — nếu bỏ qua phải ghi lý do - Wrap phải kèm
cause - Đóng tài nguyên bằng try-with-resources
- Message đủ ngữ cảnh nhưng không lộ mật khẩu/token
- Phân biệt lỗi “có thể thử lại” và “không thể” để retry đúng
Lambda & Optional
Lambda là “cú pháp ngắn cho một functional interface”. Optional là “kiểu trả về nói rằng có thể không có gì”. Cả hai đều bị dùng quá tay.
Functional interface
Có thể có nhiều default/static method, nhưng chỉ một abstract method. @FunctionalInterface giúp compiler báo lỗi nếu bạn vô tình thêm method thứ hai.
Các functional interface có sẵn
| Interface | Method | Dùng cho |
|---|---|---|
Function<T,R> | R apply(T) | Biến đổi 1-1 |
BiFunction<T,U,R> | R apply(T,U) | Biến đổi từ hai nguồn |
UnaryOperator<T> | T apply(T) | Biến đổi cùng kiểu |
Predicate<T> | boolean test(T) | Điều kiện lọc |
Supplier<T> | T get() | Tạo lazy, factory |
Consumer<T> | void accept(T) | Side effect, in log |
Runnable | void run() | Task không trả về |
Callable<V> | V call() throws Exception | Task có trả về, cho phép checked |
Comparator<T> | int compare(T,T) | Sắp xếp |
Có biến thể primitive để tránh boxing: IntFunction, ToIntFunction, IntPredicate, IntSupplier…
Cú pháp và biến capture
Lambda không tạo scope mới cho this — this vẫn là instance bao ngoài (khác anonymous class). Biến local capture phải effectively final.
Method reference — 4 kiểu
Chỉ dùng khi method reference dễ đọc hơn lambda. Chuỗi :: lồng nhau quá sâu thì lambda rõ nghĩa hơn.
Ghép hàm
andThen xuôi chiều đọc, compose ngược chiều toán học. Với predicate có and, or, negate.
Optional — dùng đúng chỗ
- Dùng làm kiểu trả về khi “không có kết quả” là bình thường
- Không dùng cho field, tham số, collection phần tử
- Không gọi
get()mà chưa kiểm tra maptự bọc,flatMapkhi hàm đã trảOptional
orElse vs orElseGet
orElse nhận giá trị đã tính. Nếu chi phí tạo cao hoặc có side effect (query DB, gọi API), luôn dùng orElseGet.
Optional trong stream
Optional.stream() (Java 9) là cách gọn nhất để lọc bỏ phần tử rỗng khi đang ở trong pipeline.
Stream API
Stream không phải collection: nó là pipeline mô tả phép biến đổi, chạy một lần, lười và có thể song song. Nắm ba phần source → intermediate → terminal là nắm 90%.
Pipeline và tính lười
| Thành phần | Đặc điểm | Ví dụ |
|---|---|---|
| Source | Collection, array, I/O, generator | list.stream(), Stream.of() |
| Intermediate | Trả Stream, chưa thực thi, nối được nhiều | filter, map, sorted |
| Terminal | Kích hoạt pipeline, trả kết quả hoặc void | toList, collect, forEach |
Vì lười, limit(5) + findFirst() chỉ xử lý đủ 5 phần tử rồi dừng — không quét cả collection. Đây là lợi ích hiệu năng thật sự của stream.
Tạo stream
Java 9 thêm Stream.iterate(seed, hasNext, next) — dừng được mà không cần limit.
Intermediate ops
| filter | Giữ phần tử thoả predicate |
| map | Biến đổi 1-1 |
| flatMap | Biến đổi 1-n rồi làm phẳng |
| distinct | Loại trùng theo equals/hashCode |
| sorted | Sắp xếp tự nhiên hoặc comparator |
| peek | Xem giữa pipeline (debug) |
| limit / skip | Cắt đầu / bỏ đầu |
| takeWhile | Lấy khi còn thoả, dừng ở phần tử đầu tiên sai |
| dropWhile | Bỏ khi còn thoả |
| mapMulti | Như flatMap nhưng không tạo stream con |
| boxed / asLongStream | Chuyển primitive ↔ object stream |
Terminal ops
| toList / collect | Gom kết quả |
| forEach / forEachOrdered | Side effect; bản ordered giữ thứ tự |
| count | Đếm |
| reduce | Gộp về một giá trị |
| min / max | Trả Optional |
| anyMatch / allMatch / noneMatch | Kiểm tra điều kiện, short-circuit |
| findFirst / findAny | Trả Optional; findAny nhanh hơn ở parallel |
| sum / average / summaryStatistics | Chỉ trên primitive stream |
| toArray | Ra mảng |
Collectors — phần được hỏi nhiều nhất
toMap ném IllegalStateException khi trùng key và NPE khi value null (vì dùng Map.merge). Với dữ liệu có thể null, lọc trước hoặc gom thủ công.
map vs flatMap
Chọn flatMap khi đã có sẵn collection/stream con. Chọn mapMulti khi cần sinh 0, 1 hoặc nhiều phần tử bằng code.
reduce — ba dạng
Identity phải là phần tử trung tính và immutable. Dùng new ArrayList<>() làm identity trong reduce/parallel là lỗi kinh điển vì mọi nhánh chia sẻ cùng một list.
Primitive stream
Dùng mapToInt/mapToLong/mapToDouble để tránh boxing và có sẵn sum, average. Đừng lạm dụng IntStream.range khi for dễ đọc hơn.
takeWhile, dropWhile, sorted
takeWhile/dropWhile chỉ có ý nghĩa với stream có thứ tự; với stream không ordered, kết quả không xác định.
Ví dụ thực tế: top 3 lương theo phòng
Đọc từ trái sang phải như câu tiếng Việt: “lọc → sắp → lấy 3 → lấy tên”. Nếu pipeline dài quá 6 bước, tách thành method có tên.
Song song — chỉ khi đáng
- Mặc định dùng
ForkJoinPool.commonPool(), size = số core − 1 - Chỉ lợi khi dữ liệu lớn, tác vụ CPU-bound, không chia sẻ trạng thái
- Không dùng cho I/O, blocking, hay list nhỏ (chi phí chia việc lớn hơn lợi ích)
- Không đảm bảo thứ tự với
forEach; dùngforEachOrderednếu cần - Tránh collector có side effect;
toMapsong song phải có merge
Stream Gatherers (Java 24)
Gatherer là “intermediate op tự định nghĩa”, cho phép cửa sổ trượt, gộp dần, giới hạn concurrency — những thứ trước đây phải viết collector hoặc vòng lặp.
Bẫy stream hay gặp
- Dùng lại stream đã có terminal op →
IllegalStateException - Sửa collection nguồn trong lúc stream → CME hoặc kết quả sai
toMaptrùng key hoặc value null- Stream vô hạn quên
limit - Dùng
forEachđể cộng dồn — hãy dùngreduce/collector - Parallel + biến mutable chia sẻ
- Checked exception trong lambda → phải wrap thành unchecked
peekđể tạo side effect nghiệp vụ (chỉ nên để debug)
Stream không thay thế vòng lặp trong mọi trường hợp. Vòng lặp có break/continue phức tạp, cần index, hoặc thao tác I/O tuần tự thường dễ đọc hơn bằng for.
Concurrency
Trọng tâm kiểm tra mid/senior. Bốn câu hay hỏi nhất: volatile khác synchronized thế nào, thread pool cấu hình ra sao, deadlock phòng thế nào, virtual thread dùng khi nào.
Trạng thái thread
| NEW | Đã tạo, chưa start() |
| RUNNABLE | Đang chạy hoặc chờ CPU |
| BLOCKED | Chờ monitor của synchronized |
| WAITING | wait(), join() không timeout |
| TIMED_WAITING | sleep(t), wait(t), join(t) |
| TERMINATED | run() đã kết thúc |
Race condition và ba cách sửa
Hai thread cùng đọc 5, cùng ghi 6 → mất một lần tăng. synchronized đảm bảo cả visibility và atomicity; AtomicInteger dùng CAS, nhanh hơn khi tranh chấp thấp.
volatile vs synchronized vs Atomic
| Visibility | Atomicity | Blocking | |
|---|---|---|---|
| volatile | Có | Không | Không |
| synchronized | Có | Có | Có |
| AtomicX | Có | Có (1 biến) | Không (CAS) |
| ReentrantLock | Có | Có | Có, linh hoạt hơn |
volatile không làm count++ nguyên tử, không bảo vệ bất biến nhiều bước. Và volatile không thay thế synchronized khi cần loại trừ tương hỗ.
wait / notify / sleep / join
wait()nhả monitor và chờ;sleep()không nhảwait/notifyphải gọi trong khốisynchronizedtrên cùng objectjoin()chờ thread kia kết thúc- Ưu tiên
BlockingQueuehơn tự viết wait/notify
ExecutorService và ThreadPoolExecutor
| Tham số | Ý nghĩa |
|---|---|
| corePoolSize | Số thread giữ lại kể cả khi rảnh |
| maximumPoolSize | Trần thread khi hàng đợi đầy |
| workQueue | Queue chờ; quyết định hành vi pool |
| handler | Abort (mặc định), CallerRuns, Discard, DiscardOldest |
Thứ tự dùng thread: core → queue → max. Vì vậy newFixedThreadPool dùng queue không giới hạn nên maximumPoolSize không bao giờ phát huy tác dụng, còn queue phình đến OOM. newCachedThreadPool thì có thể tạo vô số thread.
Chọn kích thước pool
| CPU-bound | số core + 1 |
| I/O-bound | core × (1 + thời gian chờ / thời gian tính) |
| Hỗn hợp | Tách pool riêng cho từng loại tác vụ |
| Virtual thread | Không pool — mỗi task một virtual thread |
Đừng đoán — đo tải thật rồi chỉnh. Pool quá nhỏ làm task xếp hàng; quá lớn gây context switch và tranh chấp.
CompletableFuture
thenApply: hàm đồng bộ;thenApplyAsync: chạy trên poolthenComposekhi hàm trảCompletableFuture, tránh lồngCF<CF<T>>handlenhận cả kết quả và lỗi;exceptionallychỉ xử lý lỗijoin()ném unchecked;get()ném checked- Truyền executor cho tác vụ I/O, đừng dựa vào common pool
Deadlock
- Bốn điều kiện: loại trừ tương hỗ, giữ và chờ, không tước đoạt, chờ vòng tròn
- Phòng: sắp thứ tự lock cố định, dùng
tryLock(timeout), giảm phạm vi lock - Phát hiện:
jstack <pid>tìm “Found one Java-level deadlock” - Không gọi code bên ngoài (callback, listener) khi đang giữ lock
ThreadLocal và rò rỉ trong pool
Mỗi thread giữ một bản riêng — không phải cách chia sẻ dữ liệu. Trong pool, thread sống rất lâu nên giá trị cũ bị giữ lại và có thể rò rỉ qua ThreadLocalMap.
Virtual thread (Java 21)
| Platform thread | Virtual thread | |
|---|---|---|
| Bộ nhớ | ~1 MB stack | Vài KB, lớn dần |
| Số lượng | Hàng nghìn | Hàng triệu |
| Lập lịch | OS | JVM trên carrier thread |
| Hợp với | CPU-bound | I/O blocking |
- Không nhanh hơn cho tính toán — chỉ tăng throughput khi chờ I/O
- Không pool virtual thread; tạo mới cho mỗi task
- Trước Java 24,
synchronizedlàm pin carrier thread; JEP 491 đã xử lý - Giới hạn tài nguyên (DB connection) bằng
Semaphore, không bằng pool ThreadLocalđắt hơn trên virtual thread — cân nhắcScopedValue
Structured concurrency (preview)
Ý tưởng: task con thuộc về một scope, cha chờ tất cả, lỗi một task hủy cả nhóm, không có thread “mồ côi”. Vẫn là preview ở Java 25.
JMM và double-checked locking
Thiếu volatile, phép gán có thể bị reorder: thread khác thấy instance != null nhưng object chưa khởi tạo xong. Đây là ví dụ kinh điển của safe publication.
Collection cho đa thread
| ConcurrentHashMap | Đọc/ghi song song, không cho null |
| CopyOnWriteArrayList | Ghi bằng cách copy mảng; đọc rất nhiều, ghi ít |
| BlockingQueue | Producer-consumer, có put/take chặn |
| ConcurrentLinkedQueue | Queue không khoá, không giới hạn |
| ConcurrentSkipListMap | Map có thứ tự, thay TreeMap khi đa thread |
Collections.synchronizedMap khoá cả map; ConcurrentHashMap chỉ khoá theo bucket nên throughput cao hơn nhiều.
Happens-before — bốn quan hệ cần nhớ
- Trong một thread: câu lệnh trước happens-before câu sau
unlockcủa monitor happens-beforelockkế tiếp trên cùng monitor- Ghi
volatilehappens-before lần đọcvolatileđó Thread.start()happens-before mọi thứ trongrun(); mọi thứ trong thread happens-beforejoin()trả về
Không có happens-before thì trình biên dịch/CPU được phép reorder và cache giá trị — đó là gốc của lỗi “chạy máy tôi thì đúng”.
Java hiện đại 8→25
Câu “em biết gì về Java mới?” thường chỉ cần 4 thứ: record, sealed + pattern matching, virtual thread, và vài API tiện dụng. Đừng kể tên JEP — hãy nói giải quyết vấn đề gì.
Record — data class thật sự
| Được | Không được |
|---|---|
| Constructor phụ, static field, method, implement interface | Thêm field instance, setter, kế thừa class khác |
Tự viết lại equals/hashCode/toString | Khai báo extends (đã extends Record) |
Record phù hợp DTO, value object, key của map, kết quả trả về. Không phù hợp entity JPA (cần constructor rỗng và mutable proxy).
Sealed — đóng cây kế thừa
Lớp con phải là final, sealed hoặc non-sealed. Lợi ích lớn nhất: compiler biết đầy đủ các subtype nên switch không cần default.
Pattern matching cho switch
whenlà guard — thứ tự case có ý nghĩacase nullphải khai báo nếu muốn bắt null, nếu không sẽ NPE- Với
sealed, bỏdefaultđược và compiler kiểm tra đủ nhánh - Record pattern deconstruct được cả record lồng nhau
instanceof pattern
Biến pattern chỉ tồn tại trong nhánh mà điều kiện đúng — compiler phân tích luồng chứ không chỉ scope.
Sequenced Collections (21)
Trước Java 21 phải dùng list.get(0), list.get(size-1), Deque… không thống nhất. Giờ SequencedCollection/Set/Map có hợp đồng chung.
Compact source file & instance main (25)
Dành cho người mới và script nhỏ. File vẫn biên dịch thành class ẩn; khi cần đóng gói nghiêm túc thì quay lại class thường. Đây là tính năng chính thức từ Java 25.
Module import (25)
Một dòng import thay cho hàng chục import java.util.*. Chỉ import package được module export, không phá encapsulation.
Flexible constructor body (25)
Trước đây super() bắt buộc là lệnh đầu tiên, muốn validate phải tách static helper. Giờ được phép đặt logic không dùng this trước super().
API đáng nhớ theo phiên bản
| Phiên bản | API / cú pháp |
|---|---|
| 9 | List.of, Map.of, Optional.or, Stream.takeWhile, JShell |
| 10–11 | var, HttpClient, String.isBlank/strip/repeat/lines, Files.readString |
| 12–13 | Collectors.teeing, String.indent/transform, text block (15) |
| 14–16 | switch expression, instanceof pattern, record, Stream.toList, mapMulti |
| 17–18 | sealed class, RandomGenerator, UTF-8 mặc định (18) |
| 19–21 | virtual thread, pattern switch, record pattern, sequenced collection, Math.clamp |
| 22–24 | FFM API chính thức, stream gatherers (24), synchronized không pin virtual thread (24) |
| 25 | scoped value, module import, compact source, flexible constructor, compact object header |
Đừng học thuộc bảng này. Khi được hỏi, hãy chọn 3 tính năng bạn đã dùng thật và nói lợi ích cụ thể: record giảm boilerplate DTO, sealed giúp switch exhaustive, virtual thread tăng throughput I/O.
Đã bị loại bỏ / ngừng dùng
- Security Manager bị vô hiệu vĩnh viễn từ Java 24
- Port 32-bit x86 bị bỏ từ Java 25
finalize()deprecated for removal — dùngCleanerhoặc try-with-resourcesThread.stop/suspend/resumekhông an toàn — đã bị vô hiệu, gọi sẽ némUnsupportedOperationExceptionjava.util.Date/Calendarnên thay bằngjava.time- Java serialization không dành cho dữ liệu không tin cậy
I/O & serialization
Phần này ít được hỏi sâu nhưng vài câu chắc chắn xuất hiện: byte stream vs char stream, try-with-resources, transient, và vì sao không nên dùng Java serialization.
Bốn nhóm stream
| Byte (8-bit) | Char (16-bit) | |
|---|---|---|
| Input | InputStream | Reader |
| Output | OutputStream | Writer |
| Dùng cho | Ảnh, file nhị phân, socket | Văn bản, ký tự |
Cầu nối: InputStreamReader, OutputStreamWriter. Luôn bọc Buffered* khi đọc/ghi nhiều lần để giảm syscall.
java.nio.file — dùng hằng ngày
Nhớ đóng Files.lines/Files.walk (chúng giữ file handle). Từ Java 18, charset mặc định của JVM là UTF-8.
Java serialization
Serializablelà marker interface, không có methodserialVersionUIDlệch →InvalidClassExceptiontransient= bỏ qua khi serialize;staticcũng không được serialize- Deserialization có thể bị lợi dụng để chạy gadget chain → không nhận dữ liệu không tin cậy
- Thực tế: dùng JSON (Jackson) hoặc Protobuf cho API/lưu trữ
IO vs NIO
| IO (cổ điển) | NIO | |
|---|---|---|
| Mô hình | Blocking, 1 thread/connection | Non-blocking, selector |
| Đơn vị | Stream | Channel + Buffer |
| Dùng khi | File, ít kết nối | Nhiều kết nối, server hiệu năng cao |
Với ứng dụng thông thường, Files + try-with-resources là đủ. Virtual thread giúp mô hình blocking truyền thống scale tốt hơn nhiều, nên NIO thuần ít cần hơn trước.
Bẫy & checklist
Tổng hợp lỗi mà người kiểm tra hay cài vào câu hỏi để xem bạn có thực sự hiểu không.
Mười hai bẫy kinh điển
| Bẫy | Sai | Đúng |
|---|---|---|
| So sánh chuỗi | s1 == s2 | s1.equals(s2) |
| Integer ngoài cache | a == b với 1000 | a.equals(b) hoặc intValue() |
| Sửa list immutable | List.of("a").add("b") | Tạo new ArrayList<>(...) |
Arrays.asList(int[]) | Tưởng là List<Integer> | Dùng Arrays.stream(arr).boxed() |
| Xóa khi for-each | list.remove(x) | removeIf / iterator.remove |
toMap trùng key | Không có merge function | toMap(k, v, (a,b) -> b) |
| Dùng lại stream | Gọi terminal op hai lần | Tạo stream mới từ source |
Optional.get() | Không kiểm tra | orElseThrow / map |
BigDecimal | equals so sánh | compareTo(...) == 0 |
finally có return | Nuốt exception/giá trị | Không return trong finally |
| DCL thiếu volatile | Object chưa publish an toàn | Thêm volatile hoặc holder idiom |
SimpleDateFormat chia sẻ | Không thread-safe | DateTimeFormatter (immutable) |
Nên
Dùng equals cho mọi object, == chỉ cho primitive/enum.
Ưu tiên immutability và final.
Lập trình với interface (List, Map).
Khởi tạo collection với capacity hợp lý.
Dùng try-with-resources cho mọi tài nguyên.
Bắt exception cụ thể, giữ cause.
Dùng java.time thay Date/Calendar.
Dùng record cho DTO/value object.
Đo hiệu năng trước khi tối ưu.
Viết test cho logic biên (null, rỗng, tràn số).
Tránh
So sánh chuỗi/wrapper bằng ==.
Trả về null cho collection — trả collection rỗng.
Dùng exception cho luồng điều khiển.
Nuốt exception hoặc catch (Exception) chung chung.
HashMap với key mutable.
Parallel stream cho I/O hoặc list nhỏ.
Thread pool không giới hạn queue.
Log rồi ném lại ở nhiều tầng.
Dựa vào finalize() để đóng tài nguyên.
Dùng double cho tiền.
Checklist tự đánh giá trước buổi kiểm tra
| Mức | Bạn cần trả lời được |
|---|---|
| Junior | OOP 4 tính chất, String pool, == vs equals, ArrayList vs LinkedList, checked/unchecked, try-with-resources, lambda cơ bản |
| Middle | HashMap internals, equals/hashCode, generics + PECS, stream pipeline + collector, volatile vs synchronized, thread pool, immutable class, JVM memory |
| Senior | JMM/happens-before, deadlock, virtual thread & pinning, GC tuning, memory leak, CompletableFuture, thiết kế API, trade-off khi chọn giải pháp |
Nếu chưa trả lời trôi chảy mục Middle, hãy quay lại các mục 3, 6, 7, 10, 11 và tự viết lại ví dụ — không đọc lại lần hai.
Hỏi nhanh đáp gọn
Tự trả lời trước khi mở. Nếu câu nào phải mở mới nhớ, đánh dấu lại mục tương ứng ở trên.
JDK, JRE và JVM khác nhau thế nào?
JVM là máy ảo chạy bytecode. JRE = JVM + thư viện chuẩn, đủ để chạy ứng dụng. JDK = JRE + compiler và công cụ phát triển (javac, jar, jcmd). Từ Java 11 không còn JRE đóng gói riêng; muốn runtime gọn thì dùng jlink.
Vì sao Java chạy được đa nền tảng?
javac dịch sang bytecode trung lập, không phải mã máy. Mỗi nền tảng có JVM riêng hiểu bytecode đó. JIT biên dịch phần nóng sang mã máy tại thời điểm chạy nên vẫn đạt hiệu năng cao.
Heap và stack khác gì, cái gì nằm ở đâu?
Stack là per-thread, chứa frame method, biến primitive và reference; tự giải phóng khi method return; nhỏ và nhanh. Heap dùng chung, chứa object và array; do GC quản lý. Object luôn ở heap (trừ khi escape analysis tối ưu thành stack allocation).
== và equals khác nhau thế nào?
== so sánh giá trị với primitive, so sánh địa chỉ với reference. equals mặc định cũng là ==; các lớp như String, wrapper, BigDecimal override để so nội dung. Luôn dùng equals cho object, trừ enum (dùng == an toàn).
Hợp đồng equals/hashCode gồm gì?
Reflexive, symmetric, transitive, consistent; equals(null) luôn false. Nếu a.equals(b) thì a.hashCode() == b.hashCode(). Ngược lại không đúng. Override một cái phải override cái kia, nếu không HashMap/HashSet sẽ hoạt động sai.
Vì sao String là immutable?
An toàn chia sẻ đa thread, cho phép string pool tiết kiệm bộ nhớ, an toàn khi làm key HashMap, và tránh bị sửa khi dùng cho bảo mật (đường dẫn, tham số SQL). Mọi thao tác “sửa” đều trả object mới.
StringBuilder, StringBuffer và phép +?
StringBuilder nhanh, không đồng bộ; StringBuffer có synchronized nên chậm hơn và hiếm khi cần. Phép + trong một biểu thức được javac dịch thành StringBuilder, nhưng += trong vòng lặp tạo object mới mỗi vòng — O(n²).
Overloading và overriding khác nhau ở đâu?
Overloading chọn lúc biên dịch, khác danh sách tham số, có thể khác kiểu trả về. Overriding chọn lúc chạy (dynamic dispatch), cùng signature, kiểu trả về covariant, không được thu hẹp access hay mở rộng checked exception. static, final, private không override được.
Abstract class hay interface?
Interface để định nghĩa hợp đồng, cho phép đa kế thừa kiểu và ghép năng lực. Abstract class để chia sẻ state + hành vi trong cùng một họ. Mặc định ưu tiên interface vì không khóa cây kế thừa; chỉ dùng abstract class khi thật sự cần field instance hoặc constructor.
ArrayList hay LinkedList?
ArrayList truy cập index O(1), thêm cuối khấu hao O(1), cache-friendly — mặc định nên dùng. LinkedList chỉ hơn khi chèn/xóa ở hai đầu, nhưng ArrayDeque làm việc đó tốt hơn. Trong thực tế gần như luôn chọn ArrayList.
HashMap hoạt động thế nào?
Băm key, “spread” bằng h ^ (h >>> 16), lấy bucket (n-1) & hash. Va chạm thì thêm vào linked list; khi bucket ≥ 8 và table ≥ 64 thì chuyển cây đỏ-đen (O(log n)). Khi size vượt 75% capacity thì resize gấp đôi và chia lại bucket.
HashMap, Hashtable và ConcurrentHashMap khác gì?
HashMap không thread-safe, cho phép null key/value. Hashtable khoá toàn map, không cho null, đã lỗi thời. ConcurrentHashMap khoá theo bucket + CAS, không cho null, đọc ghi song song tốt, iterator weakly consistent (không ném CME).
Type erasure là gì, PECS là gì?
Erasure: thông tin generic bị xoá sau biên dịch, nên không thể new T(), instanceof T, hay tạo mảng T[]. PECS: ? extends T cho nguồn chỉ đọc (producer), ? super T cho đích chỉ ghi (consumer).
Checked và unchecked exception khác gì?
Checked (kế thừa Exception, không qua RuntimeException) buộc compiler kiểm tra catch/throws, dùng cho tình huống có thể phục hồi. Unchecked (RuntimeException/Error) là lỗi lập trình, không bắt buộc khai báo. Đa số lỗi nghiệp vụ nên là unchecked và xử lý tập trung.
try-with-resources đóng tài nguyên theo thứ tự nào?
Ngược với thứ tự mở. Nếu close() ném exception, nó được gắn vào exception chính dưới dạng suppressed thay vì che mất lỗi gốc. Chỉ áp dụng cho class implement AutoCloseable.
Vì sao biến capture trong lambda phải effectively final?
Lambda không tạo bản sao state mà đóng gói giá trị biến tại thời điểm tạo. Nếu cho phép thay đổi, ngữ nghĩa “capture theo giá trị” sẽ mâu thuẫn và không thể triển khai an toàn. Vì vậy biến local phải final hoặc không bị gán lại.
Stream lười là gì, vì sao không dùng lại được?
Intermediate op chỉ ghi lại phép biến đổi; terminal op mới kéo dữ liệu và chạy pipeline, cho phép short-circuit (limit, findFirst). Stream là một lần dùng: sau terminal op nó ở trạng thái đã tiêu thụ, gọi tiếp sẽ IllegalStateException.
Optional dùng sao cho đúng?
Dùng làm kiểu trả về khi “không có kết quả” là bình thường. Không dùng cho field, tham số hay phần tử collection. Ưu tiên map/flatMap/filter/orElseGet/orElseThrow, tránh get() không kiểm tra. orElse luôn tính giá trị, orElseGet mới lười.
volatile khác synchronized thế nào?
volatile đảm bảo visibility và cấm reorder quanh truy cập, nhưng không nguyên tử hoá count++. synchronized đảm bảo cả visibility lẫn mutual exclusion nhưng có thể block. Với một cờ trạng thái dùng volatile; với bất biến nhiều bước dùng lock hoặc atomic.
ThreadPoolExecutor nhận task theo thứ tự nào?
Nếu số thread < core thì tạo thread mới; nếu đủ core thì đưa vào queue; chỉ khi queue đầy mới tạo tới max; vượt max thì gọi rejection handler. Vì vậy pool dùng queue không giới hạn sẽ không bao giờ tăng tới max và có thể OOM.
Virtual thread khác platform thread ở đâu?
Virtual thread do JVM lập lịch trên một pool carrier thread nhỏ, tốn vài KB thay vì ~1 MB, nên tạo được hàng triệu. Chúng chỉ giúp tăng throughput khi tác vụ chờ I/O; không nhanh hơn cho CPU-bound. Không pool virtual thread; giới hạn tài nguyên bằng semaphore.
Deadlock hình thành thế nào và phòng ra sao?
Hai thread giữ hai lock và chờ nhau theo vòng tròn. Phòng bằng cách sắp thứ tự lock cố định, dùng tryLock có timeout, giữ lock ngắn, không gọi callback khi đang giữ lock. Phát hiện bằng jstack.
G1 và ZGC khác nhau thế nào?
G1 chia heap thành region, cân bằng throughput và pause, là mặc định từ Java 9, phù hợp đa số ứng dụng. ZGC dùng barrier + colored pointer để pause cực thấp (dưới ms) trên heap rất lớn, đổi lại tốn CPU/bộ nhớ hơn. ZGC phân thế hệ mặc định từ Java 23.
Record khác class thường ở đâu?
Record là class final, field private final, tự sinh constructor chuẩn, accessor, equals/hashCode/toString. Không thêm field instance, không setter, không kế thừa class khác (đã extends Record). Hợp cho DTO/value object, không hợp entity JPA.
Nguyên nhân memory leak trong Java?
Object còn reachable nhưng không còn dùng: static collection tăng mãi, cache không giới hạn, listener chưa remove, ThreadLocal trong pool không remove(), key HashMap bị giữ, classloader rò rỉ khi hot reload. Khắc phục bằng giới hạn cache, weak reference và profiler (heap dump + MAT).
Luyện trắc nghiệm
12 câu đúng kiểu bẫy kiểm tra. Chọn đáp án để xem giải thích ngay — điểm và kỷ lục được lưu trên máy bạn.