Câu hỏi & gợi ý trả lời
Q1. Sự khác biệt giữa toán tử == và method equals() trong Java là gì?
Toán tử `==` so sánh giá trị nguyên thủy (primitive) hoặc địa chỉ ô nhớ giữa hai tham chiếu đối tượng (reference equality). Method `equals()` dùng để so sánh giá trị logic nội dung (value equality). Mặc định `equals()` trong class `Object` sử dụng `==`, nhưng các lớp như `String`, `Integer` đã override lại để so sánh nội dung bên trong.
Q2. Tại sao khi override equals() bắt buộc phải override hashCode()?
Theo quy ước của Java (Java Contract), 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, các cấu trúc dữ liệu dạng băm như `HashMap`, `HashSet` sẽ tính sai bucket index, dẫn đến việc không thể tìm thấy phần tử hoặc bị lưu trùng lặp dữ liệu.
Q3. Phân biệt String, StringBuilder và StringBuffer?
`String` là immutable (bất biến), mỗi thao tác chỉnh sửa đều tạo đối tượng mới trên Heap/String Pool. `StringBuilder` và `StringBuffer` đều là mutable (cho phép sửa chuỗi trên cùng ô nhớ); trong đó `StringBuffer` là thread-safe nhờ các method được đánh dấu `synchronized`, còn `StringBuilder` không thread-safe nhưng đem lại hiệu năng cao hơn trong môi trường đơn luồng.
Q4. Cơ chế hoạt động bên dưới của HashMap trong Java diễn ra như thế nào?
`HashMap` lưu dữ liệu dạng key-value dựa trên mảng các Node (buckets), dùng `hashCode()` của key để tính index. Khi xảy ra collision (trùng index), các node được liên kết dưới dạng LinkedList. Từ Java 8, nếu số phần tử trong một bucket vượt quá 8 (TREEIFY_THRESHOLD) và kích thước bảng từ 64 trở lên, danh sách liên kết sẽ chuyển thành Red-Black Tree để cải thiện tốc độ tìm kiếm từ O(n) xuống O(log n).
Q5. Điểm khác nhau giữa Fail-Fast và Fail-Safe Iterator là gì?
`Fail-Fast` iterator (như `ArrayList`, `HashMap`) sẽ lập tức ném ra ngoại lệ `ConcurrentModificationException` nếu phát hiện collection bị thay đổi cấu trúc trong lúc đang duyệt mà không thông qua chính iterator đó. `Fail-Safe` iterator (như `ConcurrentHashMap`, `CopyOnWriteArrayList`) hoạt động trên bản sao (clone) của dữ liệu tại thời điểm tạo iterator, do đó không ném ngoại lệ khi có luồng khác ghi đè.
Q6. Phân biệt Heap Memory và Stack Memory trong JVM?
`Stack Memory` lưu trữ các biến nguyên thủy cục bộ và tham chiếu đối tượng của từng thread riêng biệt, giải phóng ngay khi kết thúc method execution theo cơ chế LIFO. `Heap Memory` là vùng nhớ dùng chung cho toàn bộ ứng dụng, nơi chứa tất cả các đối tượng (objects) được tạo bởi từ khóa `new`, và việc dọn dẹp vùng nhớ này do Garbage Collector đảm nhiệm.
Q7. Garbage Collector (GC) trong Java hoạt động theo nguyên lý nào?
GC giải phóng bộ nhớ bằng cách quét đồ thị đối tượng và thu hồi các object không còn đường dẫn tham chiếu từ các `GC Roots` (luồng đang chạy, biến static, biến cục bộ trên Stack). Heap được chia thành Young Generation (Eden, Survivor spaces) và Old Generation để tối ưu hóa tần suất thu gom dựa trên giả thuyết phần lớn đối tượng sẽ chết ngay sau khi sinh ra.
Q8. Phân biệt giữa final, finally và finalize()?
`final` là keyword dùng để tạo hằng số (biến), chặn override (method), hoặc chặn kế thừa (class). `finally` là khối lệnh đi kèm cặp `try-catch`, đảm bảo luôn được thực thi để đóng kết nối hoặc dọn dẹp tài nguyên. `finalize()` là method của `Object` được gọi trước khi GC thu hồi bộ nhớ, tuy nhiên method này đã bị deprecated từ Java 9 do không đảm bảo thời gian chạy.
Q9. Từ khóa volatile có tác dụng gì trong lập trình Concurrency?
`volatile` đảm bảo tính hiển thị (visibility): mọi thao tác đọc/ghi biến được đồng bộ trực tiếp với Main Memory thay vì lưu trong CPU Cache của từng luồng. Ngoài ra, nó ngăn chặn compiler và CPU tái sắp xếp thứ tự lệnh (instruction reordering). Tuy nhiên, `volatile` không đảm bảo tính nguyên tử (atomicity) cho các phép toán tổ hợp như `count++`.
Q10. Sự khác biệt giữa synchronized block và ReentrantLock là gì?
`synchronized` là cơ chế khóa ngầm định (implicit) cấp ngôn ngữ, tự động giải phóng lock khi ra khỏi block code nhưng không thể ngắt quãng (interrupt) hay kiểm tra trạng thái khóa. `ReentrantLock` là class thuộc `java.util.concurrent.locks`, yêu cầu mở/khóa tường minh (`lock()`, `unlock()` trong `finally`), hỗ trợ các tính năng mở rộng như `tryLock()`, timeout, và fairness policy.
Q11. Phân biệt Intermediate Operations và Terminal Operations trong Stream API?
`Intermediate operations` (như `filter()`, `map()`, `sorted()`) biến đổi một Stream thành Stream khác, mang tính lười (lazy evaluation) và chưa thực thi xử lý ngay. `Terminal operations` (như `collect()`, `forEach()`, `reduce()`) kích hoạt toàn bộ chuỗi xử lý và trả về kết quả cụ thể (non-stream result) hoặc side-effect, sau đó Stream sẽ bị đóng lại.
Q12. Sự khác biệt giữa Abstract Class và Interface từ Java 8 trở đi là gì?
Abstract Class thể hiện mối quan hệ kế thừa bản chất ("IS-A"), có thể chứa constructor, biến trạng thái (stateful fields) và chỉ hỗ trợ đơn kế thừa. Interface thể hiện khả năng ("CAN-DO"), hỗ trợ đa triển khai; từ Java 8 Interface có thể chứa `default` và `static` methods, nhưng toàn bộ biến khai báo trong Interface mặc định vẫn là `public static final`.
Q13. Checked Exception và Unchecked Exception khác nhau như thế nào?
`Checked Exception` (kế thừa trực tiếp từ `Exception` ngoại trừ `RuntimeException`) được kiểm tra tại compile-time, bắt buộc lập trình viên phải xử lý bằng `try-catch` hoặc khai báo `throws` (ví dụ: `IOException`, `SQLException`). `Unchecked Exception` (kế thừa từ `RuntimeException`) xảy ra tại runtime do lỗi logic lập trình (ví dụ: `NullPointerException`, `IllegalArgumentException`) và không bắt buộc xử lý tại compile-time.
Q14. ThreadLocal là gì và rủi ro phổ biến nhất khi sử dụng nó là gì?
`ThreadLocal` cung cấp các biến có phạm vi độc lập cho từng luồng riêng biệt, giúp truy cập dữ liệu theo ngữ cảnh (context) mà không cần truyền tham số qua nhiều tầng. Rủi ro lớn nhất là gây rò rỉ bộ nhớ (Memory Leak) trong môi trường Thread Pool (như Tomcat Worker Threads) nếu không chủ động gọi method `remove()` trong khối `finally` sau khi hoàn tất xử lý.