Java Core
Trang này chứa các mẹo và tài nguyên để chuẩn bị cho các cuộc phỏng vấn Java core.
Java Core
Các loại vùng nhớ trong Java
Trong Java, JVM (Java Virtual Machine) cấp phát bộ nhớ cho các tiến trình, phương thức và đối tượng khác nhau. Một số vùng nhớ quan trọng được JVM cấp phát bao gồm:
ClassLoader
Là một thành phần của JVM, chịu trách nhiệm tải các tệp lớp (class files) vào bộ nhớ.
Class (Method) Area
Lưu trữ các cấu trúc của từng lớp, bao gồm runtime constant pool, thông tin về các trường (fields) và phương thức (methods), cũng như mã bytecode của các phương thức.
Heap
Được tạo ra khi chương trình chạy (runtime) và là vùng nhớ chính để cấp phát đối tượng (objects) cũng như dữ liệu runtime.
Stack
Lưu trữ biến cục bộ (local variables) và kết quả trung gian trong quá trình thực thi.
Hỗ trợ gọi phương thức (method invocation) và trả về giá trị từ phương thức.
Mỗi luồng (thread) sẽ có một JVM Stack riêng khi được tạo.
Program Counter (PC) Register
Chứa địa chỉ của lệnh JVM hiện tại đang được thực thi.
Native Method Stack
Dành riêng cho các phương thức gốc (native methods) được sử dụng trong ứng dụng.
Những vùng nhớ này giúp JVM quản lý hiệu quả việc thực thi các chương trình Java và tối ưu hóa hiệu suất của hệ thống.
OOP là gì và ví dụ
Lập trình hướng đối tượng (Object-Oriented Programming - OOP) là một mô hình lập trình tổ chức và mô hình hóa phần mềm dưới dạng tập hợp các đối tượng, đại diện cho các thực thể trong thế giới thực.
Mỗi đối tượng đóng gói (encapsulate) dữ liệu (thuộc tính - attributes) và hành vi (phương thức - methods), đồng thời có thể tương tác với nhau để thực hiện các thao tác khác nhau trong chương trình.
Các Nguyên Tắc Chính Của Lập Trình Hướng Đối Tượng (OOP)
1. Trừu Tượng (Abstraction)
Trừu tượng là quá trình che giấu các chi tiết triển khai phức tạp và chỉ hiển thị những tính năng quan trọng của một đối tượng. Điều này giúp giảm độ phức tạp và làm cho hệ thống dễ sử dụng và bảo trì hơn.
📌 Ví dụ:
Trong một lớp Car, ta có thể cung cấp các phương thức như startEngine()
và stopEngine()
, nhưng giấu đi cách động cơ hoạt động bên trong.
2. Đóng Gói (Encapsulation)
Đóng gói là quá trình gộp dữ liệu (thuộc tính - attributes) và các phương thức (hàm - methods) thao tác trên dữ liệu đó vào cùng một đơn vị (class - lớp).
Ngoài ra, nó cũng bao gồm việc hạn chế truy cập trực tiếp đến một số thành phần bằng cách sử dụng các bộ điều chỉnh truy cập (access modifiers) như private
, protected
, và public
.
📌 Ví dụ: Lớp BankAccount với số dư được đóng gói
3. Kế Thừa (Inheritance)
Kế thừa là cơ chế cho phép một lớp (class con - subclass) kế thừa các thuộc tính và phương thức của một lớp khác (class cha - superclass). Điều này giúp tái sử dụng mã nguồn (code reuse) và tạo mối quan hệ phân cấp (hierarchical relationships) giữa các lớp.
📌 Ví dụ: Lớp Dog kế thừa từ lớp Animal
4. Đa Hình (Polymorphism)
Đa hình là khả năng cho phép một giao diện (interface) hoặc một phương thức có thể có nhiều cách triển khai khác nhau. Điều này giúp viết code linh hoạt hơn và dễ mở rộng.
Có hai loại đa hình chính trong Java:
Đa hình tại thời điểm biên dịch (Compile-time Polymorphism - Method Overloading)
Đa hình tại thời điểm chạy (Runtime Polymorphism - Method Overriding)
1️⃣ Đa hình qua nạp chồng phương thức (Method Overloading)
📌 Ví dụ: Một lớp Calculator
có nhiều phiên bản của phương thức add()
🔹 Method Overloading giúp tạo nhiều phiên bản của cùng một phương thức với tham số khác nhau.
2️⃣ Đa hình qua ghi đè phương thức (Method Overriding)
📌 Ví dụ: Lớp Shape
có nhiều cách triển khai phương thức draw()
🔹 Method Overriding cho phép lớp con thay đổi hành vi của phương thức lớp cha. 🔹 Đối tượng có thể được xử lý theo kiểu của lớp cha, giúp code linh hoạt hơn.
Khái Niệm Kế Thừa (Inheritance) trong OOP
Kế thừa (Inheritance) là một tính chất quan trọng trong Lập trình Hướng Đối Tượng (OOP), cho phép một lớp (class con - subclass) kế thừa các thuộc tính và phương thức của một lớp khác (class cha - superclass).
💡 Kế thừa giúp tái sử dụng mã nguồn và tổ chức mã theo mô hình phân cấp, đồng thời hỗ trợ ghi đè phương thức (method overriding) và đa hình (polymorphism).
Nguyên Tắc Hoạt Động
📌 Kế thừa được thể hiện bằng quan hệ "IS-A"
Ví dụ: "Dog IS-A Animal", "Car IS-A Vehicle"
Nếu một lớp con kế thừa từ lớp cha, lớp con sẽ có tất cả thuộc tính và phương thức của lớp cha, đồng thời có thể mở rộng thêm các tính năng mới.
Các Loại Kế Thừa Trong Java
Java hỗ trợ các loại kế thừa sau:
1️⃣ Single Inheritance (Kế thừa đơn): Một lớp con kế thừa từ một lớp cha duy nhất. 2️⃣ Multilevel Inheritance (Kế thừa đa cấp): Một lớp con kế thừa từ một lớp cha, lớp cha lại kế thừa từ một lớp khác. 3️⃣ Hierarchical Inheritance (Kế thừa phân cấp): Một lớp cha có nhiều lớp con kế thừa.
📌 Lưu ý: Java không hỗ trợ Multiple Inheritance (kế thừa đa lớp) trực tiếp để tránh vấn đề Diamond Problem. Tuy nhiên, có thể sử dụng Interface để khắc phục.
Lợi Ích Của Kế Thừa
✅ Tái sử dụng mã nguồn: Lớp con có thể sử dụng lại mã từ lớp cha mà không cần viết lại. ✅ Giảm dư thừa code: Giúp giảm trùng lặp code trong các lớp có chung đặc điểm. ✅ Dễ mở rộng: Khi cần thay đổi logic, chỉ cần chỉnh sửa trong lớp cha, tất cả lớp con sẽ cập nhật theo. ✅ Hỗ trợ đa hình (Polymorphism): Cho phép lớp con ghi đè phương thức của lớp cha để thay đổi hành vi theo nhu cầu.
Tại Sao Java Không Hỗ Trợ Kế Thừa Đa Lớp (Multiple Inheritance)?
🔹 Kế thừa đa lớp (Multiple Inheritance) là một cơ chế trong lập trình hướng đối tượng, trong đó một lớp con có thể kế thừa từ nhiều lớp cha.
🔹 Tuy nhiên, Java không hỗ trợ kế thừa đa lớp đối với các lớp (class) để tránh vấn đề nhập nhằng (ambiguity) và khó khăn trong bảo trì mã nguồn.
1️⃣ Vấn Đề Khi Sử Dụng Kế Thừa Đa Lớp
📌 Vấn đề lớn nhất của kế thừa đa lớp là xung đột phương thức (Method Conflict/Ambiguity). Khi một lớp kế thừa từ nhiều lớp cha, nếu các lớp cha có cùng một phương thức nhưng triển khai khác nhau, lớp con sẽ không biết chọn phương thức nào.
Ví dụ: Trường Hợp Gây Mâu Thuẫn Trong Kế Thừa Đa Lớp
Giả sử chúng ta có hai lớp cha TV và AtomBomb, cả hai đều có phương thức switchOn()
, nhưng chỉ có TV có switchOff()
. Nếu một lớp SmartDevice
kế thừa cả TV và AtomBomb, sẽ có xung đột khi gọi switchOn()
.
❌ LỖI: Không biết nên sử dụng switchOn()
của TV
hay AtomBomb
.
💡 Đây là lý do Java không hỗ trợ kế thừa đa lớp để tránh các xung đột như thế này!
2️⃣ Cách Giải Quyết Trong Java: Sử Dụng Interface
📌 Mặc dù Java không hỗ trợ kế thừa nhiều lớp, nhưng Java hỗ trợ kế thừa nhiều Interface.
🔹 Interface không có trạng thái (state), chỉ định nghĩa hành vi (method signature) mà không có triển khai (implementation). 🔹 Khi một lớp thực hiện nhiều interface, nó có trách nhiệm tự định nghĩa cách thực hiện các phương thức đó.
Ví dụ Sử Dụng Interface Thay Cho Kế Thừa Đa Lớp
✅ Giải pháp Interface giúp giải quyết vấn đề kế thừa đa lớp bằng cách loại bỏ xung đột trong triển khai phương thức.
Khái Niệm Composition Trong Lập Trình Hướng Đối Tượng (OOP)
🔹 Composition (Thành phần) là một dạng quan hệ "HAS-A" (có một) giữa hai lớp trong lập trình hướng đối tượng (OOP).
🔹 Khi một lớp chứa một đối tượng của lớp khác như một thành viên (instance variable), đó được gọi là Composition.
🔹 Ví dụ: Một lớp Car
có một SteeringWheel
, nên Car
có một SteeringWheel
.
1️⃣ Đặc Điểm Của Composition
✅ Mối quan hệ mạnh mẽ (Strong Association): Một lớp không thể tồn tại độc lập nếu không có lớp kia. ✅ Không thể tách rời: Nếu lớp chứa bị hủy, thì tất cả các đối tượng bên trong nó cũng bị hủy. ✅ Thường được triển khai bằng cách tạo một đối tượng của lớp khác làm thuộc tính (instance variable).
Sự Khác Biệt Giữa Aggregation và Composition Trong Lập Trình Hướng Đối Tượng (OOP)
Aggregation (Tổng hợp) và Composition (Thành phần) đều là kiểu quan hệ "HAS-A" (có một) giữa hai lớp trong lập trình hướng đối tượng. 🔹 Composition là mối quan hệ mạnh (Strong Association). Nếu đối tượng cha bị hủy, tất cả các đối tượng con cũng bị hủy theo. 🔹 Aggregation là mối quan hệ yếu (Weak Association). Nếu đối tượng cha bị hủy, đối tượng con vẫn có thể tồn tại độc lập.
Tại sao chúng ta cần Constructor trong Java?
Java là một ngôn ngữ lập trình hướng đối tượng, trong đó chúng ta tạo và sử dụng các đối tượng. Constructor là một đoạn mã tương tự như một phương thức, được sử dụng để tạo một đối tượng và thiết lập trạng thái ban đầu của nó. Constructor là một hàm đặc biệt có cùng tên với tên lớp. Nếu không có constructor, không có cách nào khác để tạo một đối tượng.
Theo mặc định, Java cung cấp một constructor mặc định cho mỗi đối tượng. Nếu chúng ta nạp chồng (overload) constructor, thì bắt buộc phải triển khai constructor mặc định.
Tại sao chúng ta cần Constructor mặc định trong các lớp Java?
Constructor mặc định là constructor không có tham số, được Java tự động tạo ra nếu không có constructor nào khác được định nghĩa trong lớp.
Theo đặc tả của Java, nếu một lớp không có constructor nào được khai báo, Java sẽ tự động cung cấp một constructor mặc định. Tuy nhiên, đặc tả này không đề cập đến trường hợp khi một lớp có constructor được nạp chồng (overloaded constructor).
Mỗi lớp cần ít nhất một constructor để tạo đối tượng. Khi có một constructor được nạp chồng, Java giả định rằng lập trình viên muốn xử lý khởi tạo theo cách riêng, do đó không tự động cung cấp constructor mặc định. Tuy nhiên, vì constructor mặc định là cần thiết theo đặc tả, việc thiếu nó sẽ dẫn đến lỗi biên dịch.
Tại sao constructor không thể là final, static hoặc abstract trong Java?
final: Khi một phương thức được khai báo là
final
, điều đó có nghĩa là nó không thể bị ghi đè (override) trong lớp con. Tuy nhiên, theo đặc tả của Java, constructor không thể bị ghi đè, vì vậy việc đánh dấu constructor làfinal
là không cần thiết.abstract: Một phương thức
abstract
không có phần thân và phải được triển khai trong lớp con. Tuy nhiên, constructor được gọi ngầm khi sử dụng từ khóanew
, do đó nó bắt buộc phải có phần thân. Vì vậy, constructor không thể làabstract
.static: Khi một phương thức là
static
, nó thuộc về lớp thay vì một đối tượng cụ thể. Tuy nhiên, constructor luôn được gọi để khởi tạo một đối tượng, do đó không có ý nghĩa khi đánh dấu constructor làstatic
.
Tại sao Java không có con trỏ?
Trong Java, thay vì sử dụng con trỏ, ngôn ngữ này sử dụng tham chiếu (references) để trỏ đến các đối tượng trong bộ nhớ. Tuy nhiên, các tham chiếu này không cho phép truy cập trực tiếp vào địa chỉ bộ nhớ. JVM có thể tự do di chuyển các đối tượng trong bộ nhớ mà không ảnh hưởng đến chương trình.
Việc loại bỏ con trỏ giúp Java quản lý bộ nhớ và thu gom rác (Garbage Collection) hiệu quả hơn. Đồng thời, nó mang lại sự thuận tiện cho lập trình viên khi không cần lo lắng về việc cấp phát và giải phóng bộ nhớ thủ công.
Mục đích của từ khóa ‘super’ trong Java
Từ khóa super
được sử dụng trong phương thức hoặc constructor của lớp con để tham chiếu đến lớp cha trực tiếp của đối tượng.
Gọi phương thức của lớp cha:
super
cho phép gọi một phương thức của lớp cha từ phương thức của lớp con.Gọi constructor của lớp cha:
super
có thể được sử dụng trong constructor của lớp con để gọi constructor của lớp cha, giúp kế thừa và khởi tạo đối tượng một cách hợp lý.
Tại sao chúng ta sử dụng biến static
trong Java?
static
trong Java?Trong Java, biến static
được sử dụng khi chúng ta muốn một thuộc tính chung cho tất cả các đối tượng của một lớp.
Biến cấp lớp: Biến
static
thuộc về lớp thay vì từng đối tượng cụ thể, giúp chia sẻ dữ liệu giữa tất cả các instance của lớp đó.Tiết kiệm bộ nhớ: Biến
static
chỉ được tải vào bộ nhớ một lần khi lớp được load, thay vì tạo một bản sao riêng cho mỗi đối tượng, giúp tối ưu hóa việc sử dụng bộ nhớ.
Mục đích của phương thức static
trong Java
static
trong JavaJava cung cấp phương thức static
để tạo hành vi ở cấp lớp, thay vì cấp đối tượng.
Chung cho tất cả các đối tượng: Phương thức
static
thuộc về lớp, không phải đối tượng cụ thể, nên có thể được gọi mà không cần tạo instance của lớp.Tiện lợi: Cho phép gọi trực tiếp thông qua tên lớp, giúp giảm chi phí khởi tạo đối tượng.
Truy cập và sửa đổi biến
static
: Phương thứcstatic
có thể thao tác với các biếnstatic
, giúp duy trì trạng thái chung ở cấp lớp.
Khi nào nên sử dụng khối static (static block
) trong Java?
static block
) trong Java?Khối static
được sử dụng khi cần khởi tạo các biến tĩnh (static
) một cách phức tạp trước khi chương trình chạy.
Khởi tạo biến
static
phức tạp: Khi biếnstatic
cần tính toán hoặc xử lý đặc biệt mà không thể thực hiện trực tiếp tại thời điểm khai báo.Thực thi trước
main
: Khốistatic
được thực thi ngay khi lớp được tải vào bộ nhớ, trước cả phương thứcmain()
.Có thể thay thế bằng phương thức
static
: Trong một số trường hợp, có thể dùng phương thứcstatic
để thay thế nếu muốn kiểm soát việc gọi khởi tạo.
Phương thức Nạp chồng (Method Overloading) và Ghi đè (Method Overriding) trong Java
Tên khác của Method Overloading
Phương thức nạp chồng (Method Overloading) còn được gọi là Đa hình tĩnh (Static Polymorphism).
Cách triển khai Method Overloading trong Java
Trong Java, một lớp có thể có nhiều phương thức cùng tên nhưng khác tham số. Để thực hiện Method Overloading, có thể:
Thay đổi số lượng tham số.
Thay đổi kiểu dữ liệu của tham số.
Thay đổi thứ tự kiểu dữ liệu của tham số.
Tại sao không thể nạp chồng phương thức chỉ bằng cách thay đổi kiểu trả về?
Nếu chỉ thay đổi kiểu trả về mà giữ nguyên danh sách tham số, trình biên dịch sẽ không thể xác định phương thức nào cần gọi.
Điều này dẫn đến hành vi mơ hồ (ambiguous behavior), do đó Java không cho phép nạp chồng bằng cách chỉ thay đổi kiểu trả về.
Có thể ghi đè một phương thức static
trong Java không?
Không. Java không cho phép ghi đè (
override
) một phương thứcstatic
.Nếu một phương thức
static
có cùng tên trong lớp con, nó chỉ đơn giản là một phương thức mới, không phải là một phương thức được ghi đè.
Sự khác biệt giữa Method Overloading và Method Overriding
Loại đa hình
Đa hình tĩnh (Static Polymorphism)
Đa hình động (Runtime Polymorphism)
Phạm vi áp dụng
Xảy ra trong cùng một lớp
Xảy ra giữa hai lớp có quan hệ kế thừa
Danh sách tham số
Phải khác nhau (số lượng, kiểu dữ liệu, thứ tự tham số)
Phải giống hệt nhau
Thời điểm xử lý
Được xử lý tại thời điểm biên dịch (compile-time)
Được xử lý tại thời điểm chạy (runtime)
Đa hình thời gian chạy (Runtime Polymorphism) trong Java
Đa hình thời gian chạy (Runtime Polymorphism) hay còn gọi là Đa hình động (Dynamic Polymorphism) là kiểu đa hình xảy ra trong thời gian chạy của chương trình.
Trong trường hợp ghi đè phương thức (Method Overriding), tại thời điểm biên dịch (compile-time), không thể xác định chính xác phương thức nào sẽ được gọi. JVM sẽ quyết định phương thức chính xác dựa trên kiểu đối tượng thực sự tại thời gian chạy (runtime).
Điều này có nghĩa là quyết định gọi phương thức nào chỉ được xác định khi chương trình đang chạy, không phải khi biên dịch.
Trừu tượng (Abstraction) trong lập trình hướng đối tượng
Trừu tượng là gì trong lập trình hướng đối tượng?
Trừu tượng (Abstraction) là quá trình ẩn đi các chi tiết triển khai của một đối tượng và chỉ hiển thị các tính năng quan trọng ra bên ngoài.
Khái niệm này không giống với lớp trừu tượng (Abstract Class) trong Java. Trừu tượng giúp xác định các điểm chung giữa các đối tượng và ẩn đi sự phức tạp trong quá trình triển khai, giúp lập trình viên tập trung vào giao diện mà đối tượng cung cấp ra bên ngoài.
Sự khác biệt giữa Trừu tượng và Đóng gói?
Trừu tượng tập trung vào thiết kế cấp độ lớp, giúp ẩn đi chi tiết triển khai và chỉ hiển thị các tính năng cần thiết.
Đóng gói (Encapsulation), còn gọi là che giấu thông tin, thường liên quan đến việc định nghĩa các thành viên dữ liệu ở mức
private
và cung cấpgetter
/setter
để truy cập chúng.
Lớp trừu tượng trong Java là gì?
Lớp trừu tượng (Abstract Class) trong Java là lớp có thể chứa một hoặc nhiều phương thức trừu tượng (abstract methods).
Phương thức trừu tượng chỉ được khai báo trong lớp trừu tượng mà không có phần triển khai.
Một lớp trừu tượng bắt buộc phải được kế thừa và các lớp con phải triển khai các phương thức trừu tượng của nó.
Không thể tạo đối tượng của một lớp trừu tượng bằng từ khóa
new
.
Tại sao một Interface không thể được đánh dấu là final
trong Java?
Một phương thức được đánh dấu là
final
thì không thể bị ghi đè.Trong khi đó, các phương thức trong Interface bắt buộc phải được triển khai trong lớp con.
Vì vậy, Java không cho phép đánh dấu Interface là
final
, bởi điều đó sẽ mâu thuẫn với bản chất của Interface.
Sự khác biệt giữa Lớp trừu tượng và Interface trong Java?
Tiêu chí
Lớp trừu tượng (Abstract Class)
Interface
Có thể chứa phương thức có phần thân không?
✅ Có thể có phương thức đã triển khai
❌ Chỉ có phương thức trừu tượng (trước Java 8)
Có thể chứa biến thành viên không?
✅ Có thể chứa biến instance
❌ Chỉ có hằng số (static final
)
Có constructor không?
✅ Có thể có constructor
❌ Không thể có constructor
Cách sử dụng
Sử dụng khi các lớp có điểm chung nhưng có thể có triển khai khác nhau
Sử dụng khi cần định nghĩa một giao diện chung mà nhiều lớp phải tuân theo
Số lượng lớp có thể kế thừa (extends)
❌ Một lớp chỉ có thể kế thừa một lớp trừu tượng
✅ Một lớp có thể triển khai nhiều interface
Lớp quan trọng nhất trong Java là gì?
Câu hỏi này mang tính mở và có nhiều cách trả lời.
Tuy nhiên, một trong những lớp quan trọng nhất trong Java là Object
, vì:
Đây là lớp cha của tất cả các lớp trong Java.
Cung cấp nhiều phương thức quan trọng như
equals()
,hashCode()
,toString()
,clone()
,…Là nền tảng của hệ thống kế thừa trong Java.
Serialization trong Java
Serialization là gì?
Serialization là quá trình chuyển đổi một đối tượng thành mảng byte. Mảng byte này chứa thông tin về class, phiên bản, và trạng thái nội bộ của đối tượng. JVM có thể sử dụng mảng byte này để truyền đối tượng qua mạng hoặc lưu trữ nó.
Mục đích của Serialization
Serialization được sử dụng trong nhiều trường hợp thực tế như:
Truyền thông (Communication):
Dùng để truyền đối tượng giữa hai máy qua mạng, ví dụ như trong RMI (Remote Method Invocation).
Lưu trữ (Persistence):
Lưu trạng thái của đối tượng vào file, database, hoặc hệ thống lưu trữ khác để có thể khôi phục sau này.
Bộ nhớ đệm (Caching):
Nếu một đối tượng tốn nhiều thời gian để khởi tạo, ta có thể serialize nó để lưu trữ và sau đó deserialize để sử dụng lại, giúp cải thiện hiệu suất.
Đồng bộ hóa giữa các JVM (Cross-JVM Synchronization):
Hữu ích khi cần đồng bộ dữ liệu giữa nhiều JVM với các kiến trúc khác nhau.
Deserialization là gì?
Deserialization là quá trình khôi phục đối tượng từ trạng thái đã được serialize. Đây là quá trình ngược lại của serialization.
Serialization: Chuyển đổi đối tượng thành chuỗi byte.
Deserialization: Chuyển chuỗi byte thành đối tượng ban đầu.
Ví dụ về Serialization và Deserialization trong Java
Lưu ý quan trọng về Serialization trong Java
Lớp phải triển khai
Serializable
Một lớp muốn được serialize phải implement
java.io.Serializable
.
Sử dụng
serialVersionUID
Tránh lỗi
InvalidClassException
khi deserialize do thay đổi class sau khi serialize.
Các trường
transient
không được serializeNếu một biến được đánh dấu là
transient
, nó sẽ không được lưu trữ khi serialize.
Các đối tượng trong một lớp cũng phải
Serializable
Nếu một lớp có chứa các đối tượng khác bên trong, thì các đối tượng này cũng phải implement
Serializable
.
Garbage Collection (GC) trong Java
1. Garbage Collection là gì?
Garbage Collection (GC) là cơ chế thu hồi bộ nhớ tự động của JVM, giúp giải phóng bộ nhớ của các đối tượng không còn được sử dụng trong chương trình.
GC giúp ngăn chặn memory leaks bằng cách loại bỏ các đối tượng không còn được tham chiếu.
Là một cơ chế quản lý bộ nhớ tự động, giúp lập trình viên không cần quản lý bộ nhớ thủ công như trong C/C++.
2. Tại sao Java cung cấp Garbage Collector?
Java không có con trỏ (pointers), nên bộ nhớ được quản lý hoàn toàn bởi JVM.
Bộ nhớ có giới hạn, nếu không giải phóng kịp thời, JVM có thể bị OutOfMemoryError.
Garbage Collector giúp thu hồi bộ nhớ của các đối tượng không còn được tham chiếu, tối ưu hóa hiệu suất hệ thống.
3. gc()
trong Java có tác dụng gì?
Java cung cấp hai phương thức để yêu cầu JVM chạy GC:
❗ Lưu ý: JVM không bắt buộc thực hiện GC ngay lập tức khi gọi gc()
. JVM có thể từ chối yêu cầu này nếu thấy không cần thiết.
4. Cách hoạt động của Garbage Collection trong Java
JVM có một tiến trình chạy ngầm (daemon process) gọi là Garbage Collector. Khi JVM nhận thấy bộ nhớ đang đầy, nó sẽ:
Tìm kiếm các đối tượng không còn được tham chiếu.
Thu hồi bộ nhớ của các đối tượng này.
Dọn dẹp bộ nhớ để tối ưu hóa hiệu suất JVM.
5. Các loại References trong Java
Java có 4 loại tham chiếu (References) giúp điều khiển cách GC thu hồi bộ nhớ:
Strong Reference (Tham chiếu mạnh)
Mặc định trong Java, nếu một đối tượng có tham chiếu mạnh, GC không thể thu hồi nó.
Soft Reference (Tham chiếu mềm)
Đối tượng có Soft Reference chỉ bị thu hồi khi JVM gần hết bộ nhớ.
Weak Reference (Tham chiếu yếu)
Đối tượng có Weak Reference sẽ bị thu hồi ngay trong lần GC tiếp theo.
Phantom Reference (Tham chiếu ma)
Chỉ được sử dụng khi muốn thực hiện cleanup đặc biệt trước khi GC thu hồi bộ nhớ.
6. Garbage Collector là loại tiến trình gì?
Garbage Collector là một tiến trình daemon (chạy nền trong JVM).
Nó tự động giám sát bộ nhớ và dọn dẹp đối tượng không còn được tham chiếu.
Chạy bất đồng bộ (asynchronously) mà không làm gián đoạn chương trình chính.
Lập trình Đa luồng (Multi-threading)
Cách thức hoạt động của Multi-threading trong Java
Java hỗ trợ lập trình đa luồng (Multi-threading), cho phép một tiến trình có thể thực thi đồng thời nhiều luồng (threads) cùng một lúc.
Trong Java, bạn có thể tạo một tiến trình (process) và sau đó tạo nhiều luồng trong tiến trình đó.
Mỗi luồng có thể hoạt động song song để thực hiện các tác vụ độc lập.
Java cung cấp các phương thức như start()
, notify()
, wait()
, sleep()
... để quản lý môi trường đa luồng.
Ưu điểm của Multi-threading
Cải thiện hiệu suất: Multi-threading giúp tối ưu hóa hiệu suất bằng cách thực thi nhiều tác vụ cùng lúc.
Truy cập đồng thời nhiều ứng dụng: Một tiến trình có thể truy cập nhiều ứng dụng cùng lúc thông qua các luồng khác nhau.
Giảm số lượng máy chủ cần thiết: Một tiến trình có thể tạo ra nhiều luồng, giảm thiểu số lượng máy chủ cần thiết để xử lý công việc.
Đơn giản hóa mã nguồn: Trong một số trường hợp, việc chia nhỏ công việc thành nhiều luồng có thể giúp mã nguồn dễ quản lý hơn so với xử lý tất cả trong một luồng duy nhất.
Nhược điểm của Multi-threading
Khó gỡ lỗi (Debugging khó khăn): Chương trình đa luồng thường phức tạp và khó phát hiện lỗi.
Khó quản lý tính đồng bộ (Concurrency issues): Các vấn đề liên quan đến đồng bộ dữ liệu có thể phát sinh do nhiều luồng truy cập vào cùng một tài nguyên.
Khó chuyển đổi từ lập trình đơn luồng (Single-threaded) sang đa luồng: Việc chuyển đổi một chương trình từ chạy đơn luồng sang đa luồng có thể gặp nhiều thách thức.
Tình trạng Deadlock: Khi nhiều luồng cùng chờ một tài nguyên mà tài nguyên này bị khóa bởi các luồng khác, hệ thống có thể rơi vào trạng thái "chết" (deadlock).
Thread trong Java là gì?
Trong Java, một Thread là một đơn vị xử lý độc lập, chạy bên trong một tiến trình (process). Nó có thể thực thi đồng thời với các luồng khác trong cùng một ứng dụng.
Mỗi luồng chạy trong một ngăn xếp riêng biệt (stack frame). Mặc định, Java tạo một luồng chính (main thread) khi phương thức main()
của chương trình được gọi.
Độ ưu tiên của Thread và cách sử dụng trong lập lịch (Scheduling)
Trong Java, mỗi luồng có một mức độ ưu tiên (priority) được xác định bằng một giá trị số nguyên.
Giá trị này ảnh hưởng đến cách bộ lập lịch (scheduler) lựa chọn luồng nào sẽ được thực thi trước.
Các luồng có độ ưu tiên cao hơn sẽ có cơ hội thực thi trước các luồng có độ ưu tiên thấp hơn.
Trình lập lịch trong JVM sẽ ưu tiên thực thi các luồng có độ ưu tiên cao trước, sau đó mới đến các luồng có độ ưu tiên thấp hơn.
Sự khác biệt giữa Pre-emptive Scheduling và Time Slicing Scheduling
Pre-emptive Scheduling: Luồng có độ ưu tiên cao nhất sẽ tiếp tục thực thi cho đến khi nó kết thúc, hoặc chuyển sang trạng thái chờ (
waiting
), hoặc bị một luồng có độ ưu tiên cao hơn thay thế.Time Slicing Scheduling: Mỗi luồng sẽ được cấp một khoảng thời gian CPU cố định để thực thi. Khi hết thời gian, luồng đó sẽ bị tạm dừng và trình lập lịch sẽ chọn luồng tiếp theo dựa trên độ ưu tiên và các yếu tố khác.
Có thể khởi động một luồng hai lần trong Java không?
Không. Trong Java, chúng ta chỉ có thể gọi phương thức start()
trên một luồng duy nhất một lần. Nếu gọi start()
lần thứ hai, chương trình sẽ ném ra ngoại lệ IllegalThreadStateException
.
Trong Java, có thể khóa một đối tượng để chỉ một luồng có thể truy cập không?
Có. Java cung cấp khối đồng bộ hóa (synchronized block) để khóa một đối tượng, đảm bảo rằng chỉ một luồng có thể truy cập vào tài nguyên đó tại một thời điểm.
Khi một luồng đã khóa một đối tượng, các luồng khác sẽ không thể truy cập đối tượng đó cho đến khi nó được giải phóng.
Điều này giúp đảm bảo tính toàn vẹn dữ liệu (data integrity) trong môi trường đa luồng.
Ví dụ:
Sử dụng từ khóa synchronized
giúp ngăn chặn nhiều luồng truy cập vào cùng một tài nguyên cùng lúc, tránh xung đột dữ liệu.
Sự khác biệt giữa Collection và Collections trong Java?
Những điểm khác biệt chính giữa Collection
và Collections
trong Java:
Loại:
Collection
là một interface trong Java.Collections
là một class tiện ích cung cấp các phương thức thao tác trên các collection.
Chức năng:
Collection
định nghĩa các phương thức cơ bản cho List, Set, Queue.Collections
chứa các hàm hỗ trợ như sắp xếp (sort()
), đồng bộ hóa (synchronizedCollection()
), tìm kiếm (binarySearch()
),...
Phạm vi phương thức:
Collection
có các phương thức ở cấp độ thể hiện (instance-level).Collections
chủ yếu có các phương thức static, có thể áp dụng cho bất kỳ collection nào.
Sự khác biệt giữa HashSet và TreeSet trong Java
Những điểm khác biệt chính giữa HashSet
và TreeSet
:
Thứ tự lưu trữ:
HashSet
: Không duy trì thứ tự của các phần tử.TreeSet
: Sắp xếp theo thứ tự tự nhiên (hoặc theo Comparator được chỉ định).
Giá trị null:
HashSet
: Cho phép chứa một phần tử null.TreeSet
: Không cho phép phần tử null (vì sử dụngcompareTo()
để so sánh).
Hiệu suất:
HashSet
: Các thao tác nhưadd()
,remove()
,contains()
có độ phức tạp O(1).TreeSet
: Các thao tác này có độ phức tạp O(log n) do sử dụng cấu trúc Red-Black Tree.
Tốc độ:
HashSet
nhanh hơnTreeSet
trong hầu hết các trường hợp.
Cấu trúc nội bộ:
HashSet
sử dụng HashMap nội bộ để lưu trữ dữ liệu.TreeSet
sử dụng NavigableMap (cụ thể là TreeMap).
Sự khác biệt giữa HashMap và Hashtable trong Java
Những điểm khác biệt chính giữa HashMap
và Hashtable
:
Đồng bộ hóa (Synchronization):
HashMap
không đồng bộ – không thread-safe.Hashtable
đồng bộ hóa – các phương thức đều đượcsynchronized
.
Giá trị null:
HashMap
cho phép một khóa null và nhiều giá trị null.Hashtable
không cho phép null (cả khóa và giá trị).
Thứ tự lưu trữ:
HashMap
không duy trì thứ tự chèn.LinkedHashMap
giữ thứ tự chèn, cònTreeMap
sắp xếp theo khóa.Hashtable
không đảm bảo thứ tự.
Iterator:
HashMap
sử dụngIterator
, là fail-fast (némConcurrentModificationException
nếu bị sửa đổi khi duyệt).Hashtable
sử dụngEnumerator
, không fail-fast.
Sự khác biệt giữa HashMap và TreeMap trong Java
Những điểm khác biệt chính giữa HashMap
và TreeMap
:
Thứ tự lưu trữ:
HashMap
không duy trì thứ tự.TreeMap
sắp xếp theo thứ tự tự nhiên (hoặcComparator
được chỉ định).
Cấu trúc nội bộ:
HashMap
sử dụng bảng băm (hashing).TreeMap
sử dụng cây đỏ-đen (Red-Black Tree).
Hiệu suất:
HashMap
có O(1) choget()
,put()
.TreeMap
có O(log n) cho các thao tác này.
Giá trị null:
HashMap
cho phép một khóa null.TreeMap
không cho phép null key, nhưng cho phép nhiều giá trị null.
So sánh khóa:
HashMap
sử dụng phương thứcequals()
để so sánh.TreeMap
sử dụng phương thứccompareTo()
.
Sự khác biệt giữa Comparable và Comparator
Những điểm khác biệt chính giữa Comparable
và Comparator
:
Loại:
Comparable<T>
là interface dùng để so sánh chính nó với đối tượng khác.Comparator<T>
là interface dùng để so sánh hai đối tượng bất kỳ.
Phương thức:
Comparable
:int compareTo(T o)
Comparator
:int compare(T o1, T o2)
Sắp xếp:
Comparable
chỉ tạo một kiểu sắp xếp duy nhất.Comparator
cho phép nhiều kiểu sắp xếp khác nhau.
Gói chứa:
Comparable
: thuộc góijava.lang
.Comparator
: thuộc góijava.util
.
Cách hoạt động của phương thức hashCode()
trong Java
hashCode()
trong JavahashCode()
là một phương thức trongObject
class, trả về một giá trị băm (int
).Giá trị băm này không duy nhất nhưng giúp tăng tốc độ tìm kiếm trong các cấu trúc dữ liệu như
HashMap
,HashSet
.Nếu hai đối tượng bằng nhau (
equals() == true
), thì hashCode() của chúng phải giống nhau.
Có nên sử dụng Generics trong Collection không?
Có! Sử dụng Generics giúp đảm bảo type-safety, tránh lỗi ép kiểu (
ClassCastException
).Generics làm cho code dễ đọc, dễ bảo trì hơn.
Ví dụ:
Câu hỏi tổng hợp
Tên khác của Shallow Copy trong Java là gì?
Object Cloning. Shallow Copy chỉ sao chép các giá trị tham chiếu của các đối tượng trong lớp.
Sự khác biệt giữa Shallow Copy và Deep Copy trong Java là gì?
Shallow Copy chỉ sao chép giá trị của các tham chiếu trong lớp, tức là các đối tượng con bên trong vẫn được chia sẻ giữa bản sao và bản gốc.
Deep Copy không chỉ sao chép các tham chiếu mà còn tạo ra bản sao mới của các đối tượng con, đảm bảo rằng bản sao không bị ảnh hưởng bởi thay đổi trong bản gốc.
Singleton Class là gì?
Singleton Class trong Java là một lớp chỉ có duy nhất một thể hiện (instance) tồn tại trong JVM tại mọi thời điểm. Constructor của lớp này được thiết kế sao cho không thể tạo nhiều hơn một đối tượng của cùng một lớp.
Sự khác biệt giữa Singleton Class và Static Class là gì?
Static Class: Chỉ chứa các phương thức tĩnh (static methods), hoạt động như một tập hợp các hàm tiện ích. Nó dựa trên lập trình hướng thủ tục (procedural programming).
Singleton Class: Là một mẫu thiết kế trong lập trình hướng đối tượng (OOP). Chỉ có một thể hiện duy nhất của lớp tồn tại trong JVM.
Hash Collision là gì? Java xử lý xung đột băm trong HashMap như thế nào?
Trong quá trình băm (hashing), đôi khi hai đối tượng khác nhau có cùng giá trị hashCode, nhưng chúng không thực sự bằng nhau. Điều này gây ra vấn đề khi Java cần lưu trữ cả hai đối tượng đó vào HashMap, dẫn đến hiện tượng Hash Collision (xung đột băm).
Có nhiều kỹ thuật để giải quyết hoặc tránh xung đột băm. Trong HashMap, Java sử dụng một danh sách liên kết (Linked List) hoặc cây đỏ đen (Red-Black Tree, từ Java 8 trở đi) để lưu trữ nhiều giá trị có cùng hashCode tại một vị trí.
Sự khác biệt chính giữa HashMap và ConcurrentHashMap trong Java là gì?
Đồng bộ hóa (Synchronization):
HashMap không được đồng bộ hóa (not synchronized).
ConcurrentHashMap được đồng bộ hóa (synchronized), giúp hỗ trợ môi trường đa luồng.
Khóa Null (Null Key):
HashMap có thể chứa một khóa null và nhiều giá trị null.
ConcurrentHashMap không cho phép null key hoặc null value.
Hỗ trợ đa luồng (Multi-threading):
HashMap không phù hợp với môi trường đa luồng mà không có sự đồng bộ.
ConcurrentHashMap được tối ưu cho môi trường đa luồng mà không làm chậm hiệu suất như việc sử dụng synchronized Map.
Tầm quan trọng của phương thức hashCode() và equals() là gì?
Trong HashMap, mỗi đối tượng khóa (Key Object) cần phải cài đặt chính xác phương thức hashCode() và equals():
Nếu hashCode() trả về cùng một giá trị cho tất cả các đối tượng khóa, thì khả năng xảy ra xung đột băm (Hash Collision) sẽ rất cao, làm giảm hiệu suất của HashMap.
Nếu equals() không được định nghĩa chính xác, HashMap có thể coi hai khóa khác nhau là giống nhau, dẫn đến việc ghi đè dữ liệu không mong muốn.
Việc triển khai đúng hashCode() và equals() giúp đảm bảo HashMap hoạt động hiệu quả và chính xác, tránh tình trạng ghi đè dữ liệu hoặc hiệu suất kém do xung đột băm.
Các trạng thái khác nhau của Thread trong Java
Dưới đây là các trạng thái khác nhau của một Thread trong Java:
New (Mới): Ở trạng thái New, thread chưa được khởi chạy.
Runnable (Có thể chạy): Một thread đang thực thi trong JVM sẽ ở trạng thái Runnable.
Blocked (Bị chặn): Một thread đang chờ một khóa (monitor lock) sẽ ở trạng thái Blocked.
Waiting (Đang chờ): Một thread đang chờ vô thời hạn để một thread khác thực hiện một hành động cụ thể sẽ ở trạng thái Waiting.
Timed_waiting (Chờ có thời hạn): Một thread đang chờ một hành động từ một thread khác trong một khoảng thời gian nhất định sẽ ở trạng thái Timed_waiting.
Terminated (Kết thúc): Một thread đã hoàn thành và thoát ra sẽ ở trạng thái Terminated.
Thế nào là một thao tác nguyên tử (Atomic Operation)?
Một thao tác nguyên tử là một thao tác hoàn thành trong một bước duy nhất so với các thread khác. Một thao tác nguyên tử hoặc được thực thi hoàn toàn hoặc không được thực thi chút nào, không có trạng thái trung gian.
Các điều kiện tối thiểu để xảy ra Deadlock trong chương trình
Để một deadlock (tình trạng kẹt lệnh) xảy ra, phải có đủ các điều kiện sau:
Mutual exclusion (Loại trừ lẫn nhau): Có ít nhất một tài nguyên chỉ có thể được một thread truy cập tại một thời điểm.
Resource holding (Giữ tài nguyên): Một thread khóa một tài nguyên và giữ nó, đồng thời cố gắng khóa một tài nguyên khác mà thread khác đang giữ.
No preemption (Không có thu hồi tài nguyên): Không có cơ chế thu hồi tài nguyên đã bị khóa sau một khoảng thời gian nhất định.
Circular wait (Chờ vòng tròn): Các thread khóa tài nguyên theo chu kỳ, tức là mỗi thread giữ một tài nguyên và chờ tài nguyên của thread khác, tạo ra vòng tròn chờ.
Làm thế nào để ngăn chặn Deadlock?
Để ngăn chặn Deadlock, cần loại bỏ ít nhất một trong các điều kiện gây ra Deadlock:
Mutual exclusion: Sử dụng cơ chế khóa lạc quan (optimistic locking) để tránh loại trừ lẫn nhau giữa các tài nguyên.
Resource holding: Một thread phải giải phóng tất cả khóa độc quyền nếu không thể lấy được tất cả các khóa cần thiết.
No preemption: Thiết lập thời gian chờ (timeout) để tài nguyên tự động được giải phóng sau một khoảng thời gian nhất định.
Circular wait: Đảm bảo tất cả các khóa được cấp phát theo cùng một thứ tự để tránh vòng tròn chờ.
Làm thế nào để phát hiện Deadlock?
Có thể sử dụng phương thức ThreadMXBean.findDeadlockedThreads()
trong Java để phát hiện deadlock.
Race Condition là gì?
Race condition (điều kiện tranh chấp) là một tình huống không mong muốn khi một chương trình cố gắng thực hiện hai hoặc nhiều thao tác cùng một lúc, nhưng do logic của chương trình, các thao tác này cần được thực hiện theo đúng trình tự để đảm bảo kết quả chính xác. Đây là một lỗi phổ biến trong lập trình đa luồng.
Race condition có thể được ngăn chặn bằng cách sử dụng cơ chế đồng bộ hóa (synchronization) hoặc các lớp hỗ trợ như AtomicInteger
.
Thao tác CAS là gì?
CAS, còn được gọi là thao tác So sánh và Hoán đổi (Compare-And-Swap). Trong một thao tác CAS, bộ xử lý cung cấp một lệnh riêng biệt có thể cập nhật giá trị của một thanh ghi chỉ khi giá trị được cung cấp bằng với giá trị hiện tại.
Thao tác CAS có thể được sử dụng như một phương án thay thế cho việc đồng bộ hóa. Giả sử luồng T1 có thể cập nhật một giá trị bằng cách truyền giá trị hiện tại của nó và giá trị mới cần cập nhật cho thao tác CAS. Trong trường hợp luồng T2 đã cập nhật giá trị hiện tại của luồng trước đó, giá trị hiện tại của luồng T1 không bằng giá trị hiện tại của T2. Do đó, thao tác cập nhật thất bại. Trong trường hợp này, luồng T1 sẽ đọc lại giá trị hiện tại và thử cập nhật lại.
Những lớp Java nào sử dụng thao tác CAS?
Các lớp Java như AtomicInteger
hoặc AtomicBoolean
sử dụng nội bộ các thao tác CAS để hỗ trợ đa luồng. Các lớp này nằm trong gói java.util.concurrent.atomic
.
Từ khóa volatile
hữu ích cho việc gì?
volatile
có ngữ nghĩa về khả năng hiển thị bộ nhớ. Về cơ bản, giá trị của một trường volatile
trở nên hiển thị với tất cả các trình đọc (đặc biệt là các luồng khác) sau khi một thao tác ghi hoàn tất trên nó. Nếu không có volatile
, trình đọc có thể nhìn thấy một giá trị chưa được cập nhật.
Những tình huống nào nên sử dụng luồng song song (parallel stream)?
Luồng song song trong Java 8 có chi phí hoạt động cao hơn nhiều so với luồng tuần tự. Cần một lượng thời gian đáng kể để điều phối các luồng. Chúng ta có thể sử dụng luồng song song trong các tình huống sau:
Khi có một số lượng lớn các mục cần xử lý và việc xử lý từng mục tốn thời gian và có thể song song hóa.
Khi có vấn đề về hiệu suất trong xử lý tuần tự.
Khi triển khai hiện tại chưa chạy trong môi trường đa luồng. Nếu đã có môi trường đa luồng, việc thêm luồng song song có thể làm giảm hiệu suất.
Khi các thao tác trên các phần tử của stream là độc lập.
Khi nào bạn sẽ sử dụng mẫu thiết kế Adapter trong Java?
Nếu chúng ta có hai lớp với các giao diện không tương thích, chúng ta sử dụng mẫu Adapter để làm cho chúng hoạt động. Chúng ta tạo một đối tượng Adapter có thể điều chỉnh giao diện của một lớp cho lớp khác. Nó thường được sử dụng để làm việc với các thư viện của bên thứ ba.
Chúng ta tạo một lớp Adapter giữa mã của bên thứ ba và lớp của chúng ta. Trong trường hợp có bất kỳ thay đổi nào trong mã của bên thứ ba, chúng ta chỉ cần thay đổi mã Adapter. Phần còn lại của mã của chúng ta có thể giữ nguyên và chỉ tương tác với Adapter.
Những mẫu kiến trúc nào bạn đã sử dụng?
Các mẫu kiến trúc được sử dụng để định nghĩa kiến trúc của một hệ thống phần mềm. Một số mẫu như sau:
MVC (Model View Controller): Mẫu này được sử dụng rộng rãi trong kiến trúc của framework Spring.
Publish-subscribe (Xuất bản-Đăng ký): Mẫu này là cơ sở của kiến trúc nhắn tin. Trong trường hợp này, các tin nhắn được xuất bản đến một Topic. Và người đăng ký đăng ký các topic mà họ quan tâm. Sau khi tin nhắn được xuất bản đến một topic mà người đăng ký quan tâm, tin nhắn được tiêu thụ bởi người đăng ký liên quan.
Service Locator (Định vị dịch vụ): Mẫu thiết kế này được sử dụng trong một dịch vụ như JNDI để định vị các dịch vụ có sẵn. Nó sử dụng một sổ đăng ký trung tâm để duy trì danh sách các dịch vụ.
n-Tier (n-Tầng): Đây là một mẫu thiết kế chung để chia kiến trúc thành nhiều tầng. Ví dụ: có kiến trúc 3 tầng với tầng trình bày, tầng ứng dụng và tầng truy cập dữ liệu. Nó còn được gọi là mẫu thiết kế đa lớp.
DAO (Data Access Object - Đối tượng truy cập dữ liệu): Mẫu này được sử dụng để cung cấp quyền truy cập vào các đối tượng cơ sở dữ liệu. Nguyên tắc cơ bản là chúng ta có thể thay đổi hệ thống cơ sở dữ liệu cơ bản mà không thay đổi logic nghiệp vụ. Vì logic nghiệp vụ giao tiếp với đối tượng DAO, nên không có tác động của việc thay đổi hệ thống cơ sở dữ liệu đối với logic nghiệp vụ.
IoC (Inversion of Control - Đảo ngược điều khiển): Đây là cốt lõi của Dependency Injection (Tiêm phụ thuộc) trong framework Spring. Chúng ta sử dụng mẫu thiết kế này để tăng tính mô đun của một ứng dụng. Chúng ta giữ cho các đối tượng được ghép nối lỏng lẻo với Dependency Injection.
Last updated