Câu hỏi & gợi ý trả lời
Q1. Sự khác biệt giữa toán tử `==` và phương thức `.equals()` trong Java là gì?
Toán tử `==` so sánh địa chỉ tham chiếu vùng nhớ giữa hai đối tượng (hoặc so sánh giá trị đối với kiểu nguyên thủy `primitive`). Ngược lại, phương thức `equals()` được thiết kế để so sánh nội dung logic của đối tượng. Mặc định `Object.equals()` vẫn dùng `==`, do đó các class như `String`, `Integer` hoặc custom class cần override lại `equals()` (đồng thời override `hashCode()`) để kiểm tra tính bằng nhau theo giá trị.
Q2. Tại sao khi override phương thức `equals()` thì bắt buộc phải override `hashCode()`?
Đây là hợp đồng (contract) của Java: nếu hai đối tượng bằng nhau theo `equals()`, chúng bắt buộc phải trả về cùng một giá trị `hashCode()`. Nếu vi phạm quy tắc này, các cấu trúc dữ liệu dựa trên băm như `HashMap`, `HashSet` sẽ hoạt động sai lệch, khiến hệ thống không thể tìm thấy hoặc thao tác đúng phần tử dù `equals()` trả về `true` do phần tử bị phân bổ sai bucket.
Q3. Phân biệt `String`, `StringBuilder` và `StringBuffer`? Khi nào nên dùng loại nào?
`String` là bất biến (immutable), mỗi thao tác chỉnh sửa đều sinh ra đối tượng mới trên String Pool/Heap nên không tối ưu khi nối chuỗi nhiều lần. `StringBuilder` và `StringBuffer` đều là mutable, cho phép chỉnh sửa nội dung mà không tạo object mới. Trong đó, `StringBuffer` an toàn đa luồng (thread-safe) do các method đều được `synchronized`, còn `StringBuilder` không thread-safe nhưng đem lại hiệu năng cao hơn, rất phù hợp cho xử lý đơn luồng.
Q4. Phân biệt bộ nhớ `Heap` và `Stack` trong JVM?
`Stack` dùng để lưu trữ các biến cục bộ (primitive data) và địa chỉ tham chiếu đối tượng, hoạt động theo cơ chế LIFO và được cấp phát/giải phóng tự động theo từng luồng riêng biệt (thread-safe). `Heap` là vùng nhớ dùng chung cho toàn bộ ứng dụng, nơi lưu trữ tất cả các đối tượng (objects) và mảng được tạo bởi từ khóa `new`. `Heap` được quản lý bởi Garbage Collector và sẽ ném lỗi `OutOfMemoryError` nếu hết dung lượng.
Q5. Cơ chế Garbage Collection (GC) trong Java hoạt động như thế nào?
Garbage Collector tự động thu hồi bộ nhớ Heap bằng cách tìm và dọn dẹp các đối tượng không còn được tham chiếu (unreachable objects) thông qua thuật toán Mark-and-Sweep hoặc Generational Collection. Bộ nhớ Heap được phân thành Young Generation (Eden, Survivor) cho các object ngắn hạn và Old Generation cho các object sống qua nhiều chu kỳ GC. Khi bộ nhớ đầy, JVM sẽ kích hoạt Minor GC (dọn Young Gen) hoặc Major/Full GC (dọn toàn bộ Heap, gây hiện tượng Stop-the-World).
Q6. Phân biệt `abstract class` và `interface` từ Java 8 trở đi?
`abstract class` thể hiện quan hệ bản chất 'is-a', có thể chứa trạng thái (instance variables), constructor và các phương thức với mọi access modifier, hỗ trợ đơn kế thừa. `interface` thể hiện hợp đồng hành vi 'can-do', hỗ trợ đa kế thừa và từ Java 8 đã có thể chứa `default` method, `static` method (Java 9 thêm `private` method). Nên dùng `interface` khi muốn định nghĩa contract linh hoạt giữa các class không cùng phân cấp, và dùng `abstract class` khi cần chia sẻ code và state chung.
Q7. Cơ chế hoạt động bên dưới của `HashMap` trong Java diễn ra như thế nào?
`HashMap` lưu trữ dữ liệu dưới dạng mảng các Node (buckets) dựa trên hashing. Khi gọi `put(K, V)`, Java tính `hashCode()` của key để tìm vị trí index trong mảng; nếu xảy ra va chạm (hash collision), các node được nối với nhau qua LinkedList. Từ Java 8, nếu số lượng node trong một bucket vượt quá ngưỡng 8 (`TREEIFY_THRESHOLD`) và kích thước mảng từ 64 trở lên, LinkedList sẽ chuyển thành Red-Black Tree để tối ưu độ phức tạp tìm kiếm từ O(n) xuống O(log n).
Q8. Điểm khác nhau cơ bản giữa `fail-fast` và `fail-safe` iterator là gì?
`fail-fast` iterator (như trong `ArrayList`, `HashMap`) duyệt trực tiếp trên dữ liệu gốc và ném ngay `ConcurrentModificationException` nếu phát hiện collection bị thay đổi cấu trúc trong quá trình duyệt qua biến `modCount`. Ngược lại, `fail-safe` iterator (như trong `CopyOnWriteArrayList`, `ConcurrentHashMap`) hoạt động trên một bản sao (clone) của dữ liệu, không ném exception khi có thay đổi đồng thời nhưng dữ liệu đọc có thể có độ trễ (weakly consistent).
Q9. Phân biệt `Checked Exception` và `Unchecked Exception` trong Java?
`Checked Exception` kế thừa từ `Exception` (loại trừ `RuntimeException`), được kiểm tra tại compile-time và bắt buộc lập trình viên phải xử lý qua `try-catch` hoặc khai báo `throws` (ví dụ: `IOException`, `SQLException`). `Unchecked Exception` kế thừa từ `RuntimeException` hoặc `Error`, xảy ra tại runtime do lỗi logic lập trình (như `NullPointerException`, `IllegalArgumentException`) và không bắt buộc phải khai báo tường minh.
Q10. Từ khóa `volatile` có tác dụng gì và khác gì so với `synchronized`?
`volatile` đảm bảo tính hiển thị (visibility) bằng cách buộc các luồng luôn đọc/ghi giá trị biến trực tiếp từ Main Memory thay vì CPU Cache, đồng thời ngăn chặn compiler reordering. Tuy nhiên, `volatile` không đảm bảo tính nguyên tử (atomicity) cho các thao tác phức hợp như `count++`. Trong khi đó, `synchronized` cung cấp cơ chế khóa (locking), đảm bảo cả tính hiển thị lẫn tính nguyên tử nhưng có chi phí hiệu năng (overhead) cao hơn.
Q11. Trình bày cơ chế Thread Pool và lợi ích của `ExecutorService` trong Java?
Thay vì tốn chi phí hệ thống để tạo và hủy thread liên tục, Thread Pool duy trì sẵn một nhóm worker thread tái sử dụng để xử lý các task trong hàng đợi (BlockingQueue). `ExecutorService` cung cấp API cấp cao để quản lý vòng đời luồng, điều phối tải thông qua cấu hình `corePoolSize`, `maxPoolSize` và `keepAliveTime`, giúp kiểm soát tài nguyên hệ thống và ngăn chặn sự cố cạn kiệt tài nguyên (resource exhaustion).
Q12. Tại sao cần cẩn trọng khi sử dụng `ThreadLocal` với Thread Pool?
`ThreadLocal` cung cấp biến riêng biệt cho từng luồng mà không cần đồng bộ hoá. Khi dùng với Thread Pool, các worker thread được tái sử dụng liên tục chứ không bị hủy, do đó nếu không gọi phương thức `ThreadLocal.remove()` sau khi hoàn tất tác vụ, giá trị của request trước có thể bị rò rỉ sang request sau của user khác, đồng thời gây ra lỗi rò rỉ bộ nhớ (Memory Leak) nghiêm trọng.
Q13. Functional Interface là gì? Liệt kê 4 Functional Interface thông dụng trong Java 8.
Functional Interface là interface chỉ chứa duy nhất một phương thức trừu tượng (Single Abstract Method - SAM), thường đi kèm annotation `@FunctionalInterface` và có thể triển khai qua Lambda Expression hoặc Method Reference. Bốn loại phổ biến gồm: `Predicate<T>` (nhận T trả về boolean), `Function<T, R>` (nhận T biến đổi thành R), `Consumer<T>` (nhận T để xử lý, không trả về giá trị) và `Supplier<T>` (không nhận tham số, cung cấp kết quả kiểu T).
Q14. Phân biệt `intermediate operation` và `terminal operation` trong Stream API?
`intermediate operation` (như `filter`, `map`, `sorted`) biến đổi stream hiện tại thành một stream mới và có tính chất lười (lazy evaluation) — chỉ thực thi khi gặp terminal operation. `terminal operation` (như `collect`, `forEach`, `reduce`, `count`) kích hoạt toàn bộ pipeline xử lý dữ liệu, trả về kết quả cụ thể hoặc tạo side-effect, đồng thời đóng Stream lại khiến nó không thể tái sử dụng.