☕ Java Core

Cheat sheet

Java Core
trong một trang

Từ JVM, OOP, String, Collections, Generics, Exception đến Lambda, Stream, Concurrency và Java 8 → 25. Mỗi mục là một ý cốt lõi kèm snippet chạy được.

8 2014Lambda · Stream · Optional
11 2018 LTSvar · HttpClient · String API
17 2021 LTSrecord · sealed · text block
21 2023 LTSvirtual thread · pattern switch
25 2025 LTSscoped value · compact source
/

Bấm / để tìm nhanh · Bấm câu hỏi để mở đáp án · Nút Đã ôn lưu tiến độ trên máy bạn.

Bộ cheatsheet: 🏠 Trang chủ ☕ Java Core 🌱 Spring Core 🟢 Spring Data JPA & Hibernate

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ầnBên trongDùng để
JVMMáy ảo: class loader, interpreter, JIT, GCChạy file .class
JREJVM + thư viện chuẩn (core classes)Chạy ứng dụng đã build
JDKJRE + 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”

  • javac chỉ 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ảnTính năng ảnh hưởng nhiều nhất
Java 8 — LTSLambda, Stream API, Optional, default method, java.time, CompletableFuture, Metaspace thay PermGen
Java 9Module (JPMS), List.of/Map.of, JShell, G1 thành GC mặc định
Java 11 — LTSvar, HttpClient, String.isBlank/strip/lines/repeat, chạy file nguồn trực tiếp, bỏ Java EE khỏi JDK
Java 14–16switch expression, instanceof pattern, text block, record, Stream.toList()
Java 17 — LTSSealed class, pattern matching cho switch (preview), đóng gói mạnh module JDK, RandomGenerator
Java 21 — LTSVirtual thread, pattern matching switch chính thức, record pattern, sequenced collection, ZGC phân thế hệ
Java 25 — LTSScoped 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

  1. JVM, JRE, JDK khác gì nhau?
  2. Vì sao Java chạy được đa nền tảng?
  3. 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

bytecodeMã trung gian trong .class
JITJust-In-Time compiler (C1/C2)
GCGarbage Collector, thu hồi vùng không còn reachable
JMMJava Memory Model — quy tắc visibility/ordering
LTSLong-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

Dùng chung cho cả JVM Heap — object & array Young generation Eden S0 S1 Old generation (tenured) Metaspace — metadata class Code cache — mã JIT Direct buffer (NIO) Mỗi thread có một bản riêng Thread stack Frame: local var, operand stack, return address -Xss để chỉnh kích thước PC con trỏ lệnh Native stack JNI / native call Object luôn ở heap, biến local nằm ở stack (trừ khi escape analysis)

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ạnViệc xảy ra
LoadingĐọc .class, tạo đối tượng Class
VerifyKiểm tra bytecode hợp lệ, an toàn
PrepareCấp phát static field, gán giá trị mặc định
ResolveTham chiếu tượng trưng → tham chiếu trực tiếp
InitializeChạ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
Serial1 thread, heap nhỏ, client
ParallelNhiều thread, ưu tiên throughput, batch
G1Mặc định từ Java 9, chia region, đặt mục tiêu pause
ZGCPause cực thấp, heap rất lớn; phân thế hệ mặc định từ 23
ShenandoahPause thấp, có trong OpenJDK
EpsilonKhô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

jpsLiệt kê tiến trình Java + PID
jstackThread dump — tìm deadlock, thread bị block
jmapHeap dump, -histo:live xem object chiếm nhiều
jstatTheo dõi GC theo thời gian thực
jcmd“Dao đa năng”: GC.heap_info, Thread.print, JFR
JFRFlight 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á ít
  • Direct buffer memory — NIO ByteBuffer.allocateDirect không được GC kịp
  • unable 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ểuBitKhoảng / ghi chú
byte8−128 … 127
short16−32 768 … 32 767
int32−2³¹ … 2³¹−1, mặc định của số nguyên
long64Hậu tố L
float32Hậu tố f, ít chính xác
double64Mặc định cho số thực
char16Khô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
primitiveSo sánh giá trị
referenceSo sánh địa chỉ (cùng object hay không)
equalsMặ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, field private 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

finalBiến không gán lại; method không override; class không kế thừa
finallyKhối luôn chạy sau try/catch (trừ System.exit hoặc JVM crash)
finalizeDeprecated, 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 String hoặc valueOf
  • Chia phải chỉ định scale + RoundingMode, nếu không có thể ném ArithmeticException
  • So sánh bằng compareTo == 0, không dùng equals

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ĩaCông cụ Java
EncapsulationGiấu trạng thái, chỉ lộ hành vi; giữ invariant luôn đúngprivate field + method, package-private
InheritanceTái sử dụng theo quan hệ is-aextends, super
PolymorphismCùng lời gọi, hành vi khác nhau tùy đối tượng thựcOverride (runtime), overload (compile-time)
AbstractionChỉ 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

OverloadingOverriding
Chọn khiCompile-timeRuntime (dispatch)
Điều kiệnKhác danh sách tham sốCùng signature
Kiểu trả vềTùy ýCovariant (kiểu con)
AccessTùy ýKhông được thu hẹp
throwsTùy ýKhông mở rộng checked
static/final/privateKhông liên quanKhô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 classInterface
Trạng tháiField instance bình thườngChỉ hằng public static final
ConstructorCóKhông
MethodAbstract + concreteAbstract, default, static, private
Đa kế thừaKhôngCó (nhiều interface)
Khi dùngChia 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/HashSet hoạt động sai

Class lồng nhau

static nestedKhông cần instance ngoài — ưu tiên dùng
innerGiữ ngầm Outer.this, dễ gây leak
localTrong method, chỉ thấy biến effectively final
anonymousTriể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

SMột class chỉ một lý do để thay đổi
OMở để mở rộng, đóng để sửa (thêm class mới, không sửa if/else cũ)
LLớp con thay được lớp cha mà không phá hành vi
INhiều interface nhỏ tốt hơn một interface béo
DPhụ 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

PatternVấn đề giải quyếtDấu hiệu nhận biết
SingletonDuy nhất một instance toàn cụcenum, holder idiom, DCL + volatile
Factory MethodTạo object mà không lộ class cụ thểTrả về interface, nhánh if/switch theo loại
BuilderObject nhiều tham số tùy chọn, immutableChuỗi gọi .name().age().build()
StrategyHoán đổi thuật toán lúc chạyInject interface hành vi vào constructor
ObserverThông báo khi trạng thái đổiDanh sách listener, publish/subscribe
DecoratorThêm hành vi mà không sửa class gốcBọc cùng interface: new Buffered(new File())
Template MethodKhung thuật toán, lớp con điền bướcMethod final gọi các abstract step
ProxyKiểm soát truy cập, thêm cross-cuttingSpring 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 HashMap hoặ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ành StringBuilder — ổn
  • += trong vòng lặp tạo StringBuilder mới mỗi lần — rất tốn
  • StringBuilder: 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 / lastIndexOfTìm vị trí, trả −1 nếu không thấy
contains / startsWith / endsWithKiểm tra chứa/tiền tố/hậu tố
equals / equalsIgnoreCaseSo nội dung (không dùng ==)
compareToThứ tự từ điển, dùng để sort
replace / replaceAllLiteral vs regex
split / joinTách theo regex, ghép có delimiter
strip / trimBỏ khoảng trắng Unicode / ASCII
isBlank / isEmptyChỉ khoảng trắng / độ dài 0
repeat / indent / linesJava 11+
formatted / formatChèn tham số kiểu printf
toCharArray / getBytesChuyể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ómImplementation chínhĐặc điểm quyết định
ListArrayList, LinkedList, CopyOnWriteArrayListCó thứ tự, truy cập theo index, cho phép trùng
SetHashSet, LinkedHashSet, TreeSetKhông trùng; khác nhau ở thứ tự và chi phí
Queue / DequeArrayDeque, PriorityQueue, BlockingQueueFIFO/LIFO, ưu tiên, hàng đợi đồng bộ
MapHashMap, LinkedHashMap, TreeMap, ConcurrentHashMapKey-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ácArrayListLinkedList
get(i)O(1)O(n)
add cuốiO(1) khấu haoO(1)
add/remove giữaO(n) dịch mảngO(n) tìm vị trí
Bộ nhớMảng liền, gọnNode + 2 con trỏ, tốn hơn
Cache CPUTốtKé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 + size riê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ồi Arrays.copyOf
  • trimToSize() thu gọn; ensureCapacity() giảm số lần cấp phát
  • subList() 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ướcChi tiết
1. Hashh = key.hashCode(), sau đó “spread”: h ^ (h >>> 16)
2. BucketIndex = (n - 1) & hash — vì n là luỹ thừa 2 nên tương đương modulo
3. Va chạmBucket là linked list; Java 8 chuyển thành cây đỏ-đen khi đủ điều kiện
4. TreeifyBucket ≥ 8 node và table ≥ 64; nếu table nhỏ hơn thì resize trước
5. UntreeifyGiảm còn 6 node thì quay lại linked list
6. ResizeKhi size > capacity × 0.75: nhân đôi capacity, chia lại bucket
7. Độ phức tạpTrung 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ạiThứ tựChi phí
HashSetKhông đảm bảoO(1)
LinkedHashSetTheo lúc chènO(1)
TreeSetĐã sắp xếpO(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

ArrayDequeStack/queue nhanh nhất, cấm null
LinkedListCũng là Deque nhưng chậm hơn
PriorityQueueHeap nhị phân, poll theo ưu tiên; duyệt không có thứ tự
ArrayBlockingQueueCó giới hạn, dùng cho producer-consumer
LinkedBlockingQueueCó thể giới hạn; mặc định gần như vô hạn
SynchronousQueueBàn giao trực tiếp, không lưu trữ
DelayQueuePhầ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ất
  • Comparator: nhiều cách sắp xếp, truyền từ ngoài vào, không sửa class
  • compareTo nên nhất quán với equals nếu dùng trong TreeSet/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ềuArrayList
Chèn/xóa hai đầuArrayDeque
Key-value, tra cứuHashMap
Giữ thứ tự chènLinkedHashMap
Cần sắp xếp theo keyTreeMap
Đa thread đọc/ghiConcurrentHashMap
Đọc cực nhiều, ghi ítCopyOnWriteArrayList
Producer-consumerBlockingQueue
Ưu tiên độngPriorityQueue

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>TTCần cả đọc và ghi
List<? extends T>TKhông (trừ null)Nguồn dữ liệu — producer
List<? super T>ObjectTĐích nhận dữ liệu — consumer
List<?>ObjectKhô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

Throwable Error Exception RuntimeException Checked exception OutOfMemoryError StackOverflowError AssertionError NullPointerException IllegalArgumentException IllegalStateException ClassCastException IOException SQLException InterruptedException FileNotFoundException unchecked checked + unchecked

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

CheckedUnchecked
GốcException (trừ RuntimeException)RuntimeException, Error
CompilerBắt catch hoặc khai báo throwsKhông bắt buộc
Ví dụIOException, SQLExceptionNPE, IllegalArgument, ClassCast
Ý nghĩaTình huống ngoài tầm kiểm soát, có thể phục hồiBug 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 finally và không quên đóng khi có exception

Tự tạo exception

  • Kế thừa RuntimeException cho 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 (UserNotFoundException tốt hơn ServiceException)

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

NullPointerExceptionGọi method/field trên null, unboxing null
ClassCastExceptionÉp kiểu sai; generics bị erasure có thể gây
IllegalArgumentExceptionTham số sai hợp đồng
IllegalStateExceptionĐúng kiểu nhưng sai trạng thái (gọi next() khi hết)
IndexOutOfBoundsIndex âm hoặc ≥ size
ConcurrentModificationExceptionSửa collection khi đang duyệt
UnsupportedOperationExceptionSửa collection immutable/fixed-size
NumberFormatExceptionParse 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 để catch rỗ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

InterfaceMethodDù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
Runnablevoid run()Task không trả về
Callable<V>V call() throws ExceptionTask 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
  • map tự bọc, flatMap khi 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ểmVí dụ
SourceCollection, array, I/O, generatorlist.stream(), Stream.of()
IntermediateTrả Stream, chưa thực thi, nối được nhiềufilter, map, sorted
TerminalKích hoạt pipeline, trả kết quả hoặc voidtoList, 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

filterGiữ phần tử thoả predicate
mapBiến đổi 1-1
flatMapBiến đổi 1-n rồi làm phẳng
distinctLoại trùng theo equals/hashCode
sortedSắp xếp tự nhiên hoặc comparator
peekXem giữa pipeline (debug)
limit / skipCắt đầu / bỏ đầu
takeWhileLấy khi còn thoả, dừng ở phần tử đầu tiên sai
dropWhileBỏ khi còn thoả
mapMultiNhư flatMap nhưng không tạo stream con
boxed / asLongStreamChuyển primitive ↔ object stream

Terminal ops

toList / collectGom kết quả
forEach / forEachOrderedSide effect; bản ordered giữ thứ tự
countĐếm
reduceGộp về một giá trị
min / maxTrả Optional
anyMatch / allMatch / noneMatchKiểm tra điều kiện, short-circuit
findFirst / findAnyTrả Optional; findAny nhanh hơn ở parallel
sum / average / summaryStatisticsChỉ trên primitive stream
toArrayRa 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ùng forEachOrdered nếu cần
  • Tránh collector có side effect; toMap song 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
  • toMap trùng key hoặc value null
  • Stream vô hạn quên limit
  • Dùng forEach để cộng dồn — hãy dùng reduce/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
BLOCKEDChờ monitor của synchronized
WAITINGwait(), join() không timeout
TIMED_WAITINGsleep(t), wait(t), join(t)
TERMINATEDrun() đã 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

VisibilityAtomicityBlocking
volatileCóKhôngKhông
synchronizedCóCóCó
AtomicXCóCó (1 biến)Không (CAS)
ReentrantLockCó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/notify phải gọi trong khối synchronized trên cùng object
  • join() chờ thread kia kết thúc
  • Ưu tiên BlockingQueue hơn tự viết wait/notify

ExecutorService và ThreadPoolExecutor

Tham sốÝ nghĩa
corePoolSizeSố thread giữ lại kể cả khi rảnh
maximumPoolSizeTrần thread khi hàng đợi đầy
workQueueQueue chờ; quyết định hành vi pool
handlerAbort (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-boundsố core + 1
I/O-boundcore × (1 + thời gian chờ / thời gian tính)
Hỗn hợpTách pool riêng cho từng loại tác vụ
Virtual threadKhô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 pool
  • thenCompose khi hàm trả CompletableFuture, tránh lồng CF<CF<T>>
  • handle nhận cả kết quả và lỗi; exceptionally chỉ xử lý lỗi
  • join() 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 threadVirtual thread
Bộ nhớ~1 MB stackVài KB, lớn dần
Số lượngHàng nghìnHàng triệu
Lập lịchOSJVM trên carrier thread
Hợp vớiCPU-boundI/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, synchronized là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ắc ScopedValue

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
CopyOnWriteArrayListGhi bằng cách copy mảng; đọc rất nhiều, ghi ít
BlockingQueueProducer-consumer, có put/take chặn
ConcurrentLinkedQueueQueue không khoá, không giới hạn
ConcurrentSkipListMapMap 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
  • unlock của monitor happens-before lock kế tiếp trên cùng monitor
  • Ghi volatile happens-before lần đọc volatile đó
  • Thread.start() happens-before mọi thứ trong run(); mọi thứ trong thread happens-before join() 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ự

ĐượcKhông được
Constructor phụ, static field, method, implement interfaceThêm field instance, setter, kế thừa class khác
Tự viết lại equals/hashCode/toStringKhai 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

  • when là guard — thứ tự case có ý nghĩa
  • case null phả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ảnAPI / cú pháp
9List.of, Map.of, Optional.or, Stream.takeWhile, JShell
10–11var, HttpClient, String.isBlank/strip/repeat/lines, Files.readString
12–13Collectors.teeing, String.indent/transform, text block (15)
14–16switch expression, instanceof pattern, record, Stream.toList, mapMulti
17–18sealed class, RandomGenerator, UTF-8 mặc định (18)
19–21virtual thread, pattern switch, record pattern, sequenced collection, Math.clamp
22–24FFM API chính thức, stream gatherers (24), synchronized không pin virtual thread (24)
25scoped 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ùng Cleaner hoặc try-with-resources
  • Thread.stop/suspend/resume không an toàn — đã bị vô hiệu, gọi sẽ ném UnsupportedOperationException
  • java.util.Date/Calendar nên thay bằng java.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)
InputInputStreamReader
OutputOutputStreamWriter
Dùng choẢnh, file nhị phân, socketVă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

  • Serializable là marker interface, không có method
  • serialVersionUID lệch → InvalidClassException
  • transient = bỏ qua khi serialize; static cũ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ìnhBlocking, 1 thread/connectionNon-blocking, selector
Đơn vịStreamChannel + Buffer
Dùng khiFile, ít kết nốiNhiề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ẫySaiĐúng
So sánh chuỗis1 == s2s1.equals(s2)
Integer ngoài cachea == b với 1000a.equals(b) hoặc intValue()
Sửa list immutableList.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-eachlist.remove(x)removeIf / iterator.remove
toMap trùng keyKhông có merge functiontoMap(k, v, (a,b) -> b)
Dùng lại streamGọi terminal op hai lầnTạo stream mới từ source
Optional.get()Không kiểm traorElseThrow / map
BigDecimalequals so sánhcompareTo(...) == 0
finally có returnNuốt exception/giá trịKhông return trong finally
DCL thiếu volatileObject chưa publish an toànThêm volatile hoặc holder idiom
SimpleDateFormat chia sẻKhông thread-safeDateTimeFormatter (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ứcBạn cần trả lời được
JuniorOOP 4 tính chất, String pool, == vs equals, ArrayList vs LinkedList, checked/unchecked, try-with-resources, lambda cơ bản
MiddleHashMap internals, equals/hashCode, generics + PECS, stream pipeline + collector, volatile vs synchronized, thread pool, immutable class, JVM memory
SeniorJMM/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.

Kiểm tra nhanh

Đúng 0/12 · Kỷ lục 0
Chọn đáp án — giải thích sẽ hiện bên dưới câu hỏi.