Câu hỏi & gợi ý trả lời
Q1. Sự khác biệt giữa Heap và Stack Memory trong Java là gì?
Stack memory dùng để lưu trữ các biến cục bộ, tham chiếu đối tượng và các khung gọi hàm (method call frames) với cơ chế LIFO, được cấp phát và giải phóng tự động, nhanh chóng. Ngược lại, Heap memory là vùng nhớ chung dùng để chứa tất cả các đối tượng (Objects) thực tế và mảng. Heap được quản lý bởi Garbage Collector (GC), có kích thước lớn hơn nhưng tốc độ truy xuất chậm hơn Stack.
Q2. Tại sao String trong Java lại mang tính chất bất biến (Immutable)?
String là Immutable để phục vụ ba mục đích chính: Tối ưu bộ nhớ nhờ String Pool (nhiều biến cùng trỏ đến một literal), đảm bảo an toàn đa luồng (Thread-safety) mà không cần đồng bộ hoá, và tăng cường bảo mật khi dùng làm tham số cho kết nối mạng, file path hay key trong HashMap (vì HashCode không bao giờ thay đổi sau khi tạo).
Q3. Phân biệt String, StringBuilder và StringBuffer?
String là immutable, mỗi lần nối chuỗi sẽ tạo ra đối tượng mới trong bộ nhớ. StringBuilder và StringBuffer đều là mutable, cho phép chỉnh sửa nội dung trên cùng một đối tượng. Trong đó, StringBuffer là Thread-safe vì các method được đánh dấu `synchronized` (chậm hơn), còn StringBuilder không Thread-safe nhưng có hiệu năng xử lý chuỗi đơn luồng cao nhất.
Q4. Nguyên lý hoạt động bên trong (internal working) của HashMap trong Java là gì?
HashMap hoạt động dựa trên mảng các `Node<K, V>` (Buckets) và hàm `hashCode()`. Khi gọi `put(key, value)`, Java băm hash code của key để tìm vị trí bucket; nếu xảy ra xung đột (Collision), các node sẽ đượ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á ngưỡng 8 (TREEIFY_THRESHOLD) và kích thước mảng tối thiểu 64, danh sách liên kết sẽ tự động chuyển thành Red-Black Tree để giảm độ phức tạp tìm kiếm từ O(n) xuống O(log n).
Q5. Hợp đồng (Contract) giữa equals() và hashCode() là gì?
Nếu hai đối tượng bằng nhau theo phương thức `equals()` (nghĩa là `obj1.equals(obj2) == true`), thì `hashCode()` của chúng BẮT BUỘC phải trả về cùng một giá trị số nguyên. Ngược lại, nếu hai đối tượng có cùng `hashCode()`, chúng chưa chắc đã bằng nhau theo `equals()` (gọi là hash collision). Nếu ghi đè `equals()` mà không ghi đè `hashCode()`, các collection như HashSet hay HashMap sẽ hoạt động sai lệch.
Q6. Từ khoá `volatile` có ý nghĩa gì trong lập trình đa luồng Java?
Từ khoá `volatile` đảm bảo tính hiển thị (Visibility) của biến giữa các luồng bằng cách ép CPU luôn đọc và ghi giá trị trực tiếp từ bộ nhớ chính (Main Memory) thay vì lưu tạm trong CPU Cache. Tuy nhiên, `volatile` không đảm bảo tính nguyên tử (Atomicity); với các thao tác kép như `count++`, vẫn cần dùng `synchronized` hoặc các lớp `AtomicInteger`.
Q7. Phân biệt `fail-fast` và `fail-safe` Iterators?
Iterator `fail-fast` (như trong ArrayList, HashMap) sẽ ném ra ngoại lệ `ConcurrentModificationException` ngay lập tức nếu phát hiện cấu trúc collection bị thay đổi trong lúc duyệt mà không qua method của chính iterator. Ngược lại, `fail-safe` iterator (như trong CopyOnWriteArrayList, ConcurrentHashMap) duyệt trên một bản sao (clone) của dữ liệu gốc, không ném exception nhưng có thể không phản ánh dữ liệu mới nhất.
Q8. Khác nhau giữa Checked Exception và Unchecked Exception?
Checked Exception kế thừa từ `Exception` (loại trừ `RuntimeException`), xảy ra tại compile-time và 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` hoặc `Error`, xảy ra tại runtime và thường do lỗi logic code (ví dụ: `NullPointerException`, `IndexOutOfBoundsException`), không bắt buộc phải khai báo tường minh.
Q9. Phân biệt `Comparable` và `Comparator`?
`Comparable` là interface nằm trong package `java.lang`, định nghĩa thứ tự sắp xếp tự nhiên (natural ordering) của chính class đó thông qua method `compareTo(Object o)` (chỉnh sửa trực tiếp class). `Comparator` nằm trong `java.util`, cung cấp chiến lược sắp xếp tùy biến bên ngoài class thông qua method `compare(Object o1, Object o2)`, cho phép tạo nhiều tiêu chí sắp xếp khác nhau mà không sửa code gốc.
Q10. Garbage Collector (GC) hoạt động như thế nào trong JVM?
GC giải phóng bộ nhớ tự động bằng cách tìm và huỷ các đối tượng không còn được tham chiếu (unreachable) từ GC Roots (như local variables trên Stack, static variables, active threads). Bộ nhớ Heap thường chia thành Young Generation (Eden, Survivor spaces) chứa các object mới tạo và Old Generation chứa các object sống lâu qua nhiều chu kỳ GC. Thuật toán phổ biến bao gồm Mark-Sweep-Compact và các bộ GC hiện đại như G1, ZGC.
Q11. Memory Leak trong Java có thể xảy ra do những nguyên nhân nào?
Dù có GC, Memory Leak vẫn xảy ra khi các đối tượng không còn dùng đến nhưng vẫn bị giữ tham chiếu vô tình. Các nguyên nhân phổ biến gồm: Quên đóng tài nguyên (File, DB Connection), sử dụng `static` field giữ tham chiếu tới object lớn, không huỷ đăng ký Event Listener/Callback, hoặc dùng đối tượng tùy biến làm key trong `HashMap` nhưng không override đúng `equals()` và `hashCode()`.
Q12. Sự khác biệt giữa Thread Pool dùng `submit()` và `execute()` trong ExecutorService?
Phương thức `execute(Runnable)` thuộc interface `Executor`, chỉ nhận `Runnable`, không trả về kết quả và ném runtime exception trực tiếp lên thread con. Trong khi đó, `submit()` thuộc `ExecutorService`, có thể nhận cả `Runnable` lẫn `Callable<T>`, trả về một đối tượng `Future` để kiểm tra trạng thái, huỷ task hoặc lấy kết quả trả về thông qua `future.get()`.