Java Core - OOP
Câu hỏi: "Sự khác biệt giữa abstract class
và interface
là gì? Khi nào nên dùng cái nào?"
abstract class
và interface
là gì? Khi nào nên dùng cái nào?"Trả lời:
Sự khác biệt chính:
Abstract class:
Có thể chứa phương thức đã triển khai (có logic) và phương thức trừu tượng (không có logic).
Có thể có trạng thái (fields) và các phương thức với cài đặt mặc định.
Mỗi lớp chỉ có thể kế thừa một
abstract class
.
Interface:
Chỉ chứa phương thức trừu tượng (trước Java 8) hoặc phương thức mặc định (từ Java 8).
Không chứa trạng thái, chỉ có các hằng số.
Một lớp có thể triển khai nhiều
interface
.
Khi nào sử dụng cái nào?
Dùng
abstract class
khi bạn cần có các phương thức chung với cài đặt mặc định hoặc trạng thái chung.Dùng
interface
khi bạn chỉ cần định nghĩa các hành vi mà lớp phải thực thi, không cần chia sẻ logic chung.
Ví dụ:
abstract class
là một lớp có thể có cài đặt sẵn, ví dụ như một lớp động vật có phương thứcbreathe()
.interface
chỉ định nghĩa hành vi nhưmakeSound()
mà bất kỳ lớp nào cũng có thể thực thi.
Em có thể giải thích final, finally, finalize khác nhau ra sao không ?
final
: Dùng để khai báo biến, phương thức, hoặc lớp mà không thể thay đổi (không thể thay đổi giá trị, ghi đè phương thức, hoặc kế thừa lớp).finally
: Dùng trong khốitry-catch
để đảm bảo mã luôn được thực thi, dùng cho việc đóng tài nguyên, ví dụ như file hoặc kết nối cơ sở dữ liệu.finalize()
: Là một phương thức trong lớpObject
được gọi trước khi đối tượng bị garbage collected, nhưng nên tránh sử dụng vì không thể kiểm soát thời gian gọi nó.
Em hiểu thế nào về tính kế thừa, đóng gói, đa hình, trừu tượng trong OOP ? Và có ví dụ thực tế dựa vào dự án không ?
1. Kế thừa (Inheritance):
Định nghĩa: Kế thừa là khả năng của một lớp con (subclass) kế thừa các đặc tính và phương thức của lớp cha (superclass). Nó giúp tái sử dụng mã nguồn và xây dựng các mối quan hệ giữa các lớp.
Ví dụ trong dự án OCOP Marketplace: Giả sử trong hệ thống của bạn có các loại người dùng như Admin, Seller, và Buyer. Bạn có thể tạo một lớp cha
User
chứa các thuộc tính và phương thức chung cho tất cả các loại người dùng (nhưusername
,email
,password
), và các lớp con nhưAdmin
,Seller
,Buyer
sẽ kế thừa lớpUser
và có thêm các thuộc tính, phương thức riêng.Code ví dụ:
public abstract class User { private String username; private String email; private String password; // Các phương thức chung public void login() { System.out.println("User logged in"); } public abstract void displayRole(); } public class Admin extends User { @Override public void displayRole() { System.out.println("Admin role"); } } public class Seller extends User { @Override public void displayRole() { System.out.println("Seller role"); } } public class Buyer extends User { @Override public void displayRole() { System.out.println("Buyer role"); } }
2. Đóng gói (Encapsulation):
Định nghĩa: Đóng gói là việc ẩn các chi tiết bên trong của một đối tượng và chỉ cung cấp các phương thức công khai (public methods) để thao tác với dữ liệu của đối tượng đó. Điều này giúp bảo vệ dữ liệu và giảm sự phụ thuộc giữa các lớp.
Ví dụ trong dự án OCOP Marketplace: Giả sử mỗi sản phẩm trong hệ thống cần thông tin về giá, nhưng bạn không muốn cho phép giá thay đổi trực tiếp từ bên ngoài lớp
Product
. Bạn có thể sử dụng phương thức getter/setter để bảo vệ thuộc tínhprice
.Code ví dụ:
public class Product { private double price; // Đóng gói thông tin về giá public double getPrice() { return price; } public void setPrice(double price) { if (price >= 0) { // Kiểm tra giá hợp lệ this.price = price; } } }
3. Đa hình (Polymorphism):
Định nghĩa: Đa hình cho phép các đối tượng thuộc các lớp con khác nhau có thể thực thi các phương thức cùng tên nhưng với hành vi khác nhau. Điều này có thể được thực hiện thông qua overriding (ghi đè phương thức) và overloading (nạp chồng phương thức).
Ví dụ trong dự án OCOP Marketplace: Ví dụ về các lớp
Product
có thể có nhiều loại sản phẩm khác nhau (ví dụ: sản phẩm điện tử, sản phẩm thời trang). Mỗi loại sản phẩm có thể có phương thứcgetDiscount()
để tính giảm giá theo cách riêng của chúng, nhưng chúng sẽ đều có cùng tên phương thứcgetDiscount()
. Đây chính là đa hình.Code ví dụ:
public class Product { public double getDiscount() { return 0.1; // Giảm giá chung } } public class Electronics extends Product { @Override public double getDiscount() { return 0.15; // Giảm giá riêng cho sản phẩm điện tử } } public class Fashion extends Product { @Override public double getDiscount() { return 0.2; // Giảm giá riêng cho sản phẩm thời trang } }
Sau đó, bạn có thể dùng đa hình để xử lý giảm giá cho bất kỳ loại sản phẩm nào:
Product product = new Electronics(); System.out.println("Discount: " + product.getDiscount()); product = new Fashion(); System.out.println("Discount: " + product.getDiscount());
4. Trừu tượng (Abstraction):
Định nghĩa: Trừu tượng là việc ẩn các chi tiết cài đặt và chỉ cho phép truy cập vào các phương thức hoặc thuộc tính quan trọng nhất của đối tượng. Bạn có thể sử dụng abstract classes hoặc interfaces để trừu tượng hóa một lớp hoặc hành vi.
Ví dụ trong dự án OCOP Marketplace: Giả sử bạn có một hệ thống thanh toán với nhiều phương thức thanh toán khác nhau như Thanh toán qua thẻ tín dụng, Thanh toán qua ví điện tử. Bạn có thể tạo một lớp
Payment
trừu tượng để định nghĩa phương thứcpay()
, và các lớp con sẽ triển khai phương thức này theo cách riêng của chúng.Code ví dụ:
public abstract class Payment { public abstract void pay(); // Phương thức trừu tượng } public class CreditCardPayment extends Payment { @Override public void pay() { System.out.println("Paying with Credit Card"); } } public class EWalletPayment extends Payment { @Override public void pay() { System.out.println("Paying with E-Wallet"); } }
Trong trường hợp này, bạn sử dụng trừu tượng để chỉ định các phương thức thanh toán mà không cần biết chi tiết cài đặt của từng loại thanh toán. Điều này làm cho hệ thống dễ mở rộng khi thêm các phương thức thanh toán mới.
Tóm tắt:
Kế thừa giúp tái sử dụng mã và tạo mối quan hệ giữa các lớp.
Đóng gói giúp bảo vệ dữ liệu và làm cho mã dễ bảo trì hơn.
Đa hình cho phép các đối tượng khác nhau thực thi cùng một phương thức với hành vi khác nhau.
Trừu tượng giúp ẩn đi các chi tiết cài đặt và chỉ cung cấp giao diện cần thiết.
Các nguyên lý này giúp chúng ta xây dựng hệ thống dễ mở rộng, bảo trì, và có thể tái sử dụng được mã nguồn.
Em có thể mô tả về GC trong Java không ? Làm sao để GC hoạt động một cách hiệu quả ?
✅ GC (Garbage Collection) là gì?
Garbage Collection trong Java là cơ chế tự động thu hồi bộ nhớ mà JVM thực hiện để loại bỏ các đối tượng không còn được tham chiếu nữa, giúp tránh tình trạng rò rỉ bộ nhớ (memory leak) và giảm gánh nặng cho lập trình viên trong việc quản lý bộ nhớ thủ công như C/C++.
✅ GC hoạt động như thế nào?
JVM sẽ kiểm tra các đối tượng trong Heap để xem đối tượng nào không còn được sử dụng (tức là không có strong reference nào trỏ tới nó).
Khi tìm thấy, các đối tượng này sẽ bị loại bỏ và bộ nhớ được giải phóng.
✅ Các thuật toán GC phổ biến:
Mark and Sweep – Đánh dấu và quét: Đánh dấu các object còn sống, sau đó xóa các object không được đánh dấu.
Generational GC – Chia vùng nhớ Heap thành:
Young Generation (nơi tạo object mới): thu gom thường xuyên, nhanh.
Old Generation (Tenured): chứa object sống lâu, thu gom ít hơn.
G1 (Garbage First): GC hiện đại, chia Heap thành nhiều vùng nhỏ (regions), thu gom song song và hiệu quả cao, phù hợp cho ứng dụng lớn, realtime.
✅ Làm sao để GC hoạt động hiệu quả?
Thiết kế object sống ngắn: Đối tượng nên được tạo ra và chết sớm, giúp GC dễ thu gom ở Young Generation (nơi thu gom nhanh).
Tránh giữ tham chiếu không cần thiết:
Ví dụ: Khi dùng
List
, sau khi xử lý xong thìlist.clear()
hoặc gánnull
nếu không còn dùng nữa.Cẩn thận với các static reference – nếu giữ object trong static field sẽ làm GC không thể thu gom.
Dùng các loại reference đúng cách:
WeakReference
cho cache hoặc các object có thể bị thu gom khi cần.SoftReference
cho các object có thể giữ lại nếu còn đủ RAM.
Tuning GC thông qua JVM options:
Ví dụ:
bashCopyEdit-XX:+UseG1GC -Xms512m -Xmx1024m -XX:MaxGCPauseMillis=200
=> G1 GC, heap size và yêu cầu tối đa thời gian pause 200ms.
Theo dõi GC bằng tools:
JVisualVM, JConsole, GC logs, hoặc JFR (Java Flight Recorder) để theo dõi memory usage và tuning.
Java 8 và Java 21 khác nhau ra sao ? Những tính năng nào mới cần chú ý ?
✅ Tóm tắt nhanh:
Lambda, Stream
✅ Có
✅ Có
Record
❌ Không có
✅ Có
Pattern Matching
❌ Không có
✅ Có
Virtual Thread
❌ Không có
✅ Mới & rất mạnh
Switch nâng cao
❌ Không có
✅ Có
Memory API mới
❌ Không có
✅ Có (dễ gọi native hơn)
Em đã làm việc với multi threading chưa ? Mô tả cách em xử lý đa luồng trong Java.
Dạ, em đã có kinh nghiệm làm việc với đa luồng (multi-threading) trong Java, đặc biệt là trong các tác vụ như:
Gửi email OTP song song
Xử lý các job bất đồng bộ như lưu log, xử lý file upload, gửi notification
Dùng thread pool để giới hạn tài nguyên
✅ Cách em xử lý đa luồng trong Java:
1. Dùng ExecutorService
để quản lý thread pool
Thay vì tạo thủ công Thread
, em thường dùng ExecutorService
vì:
Tái sử dụng luồng
Giới hạn được số lượng luồng tối đa
Dễ shutdown và quản lý timeout
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
// task gửi OTP hoặc lưu log
});
2. Dùng CompletableFuture
(từ Java 8 trở đi)
Để xử lý bất đồng bộ có callback và chaining rõ ràng hơn:
CompletableFuture
.supplyAsync(() -> service.getUserInfo(id))
.thenApply(user -> service.sendWelcomeEmail(user))
.exceptionally(ex -> {
log.error("Có lỗi: " + ex.getMessage());
return null;
});
3. Dùng @Async
trong Spring Boot
Trong một số dự án Spring, em dùng annotation @Async
để chạy task bất đồng bộ:
@Async
public void sendOtpEmail(String email) {
// gửi email OTP
}
Yêu cầu bật
@EnableAsync
trong config.
✅ Khi nào em dùng đa luồng?
Khi cần xử lý các tác vụ I/O-bound như gọi API, gửi email, ghi log – để tránh block luồng chính
Khi cần cải thiện hiệu suất hệ thống bằng cách xử lý song song
Khi cần xử lý batch dữ liệu lớn mà tách nhỏ ra xử lý đa luồng
✅ Lưu ý khi dùng đa luồng:
Đồng bộ dữ liệu khi nhiều luồng truy cập chung (
synchronized
,ReentrantLock
,ConcurrentHashMap
)Tránh deadlock và race condition
Giám sát và xử lý exception trong thread (dùng try-catch hoặc
exceptionally
trongCompletableFuture
)
Nều gặp đoạn code có nhiều if else lồng nhau, em thường refactor như thế nào ?
Câu hỏi này rất thực tế, vì code có nhiều if-else
lồng nhau thường khó đọc, khó test và khó bảo trì. Khi gặp tình huống đó, em sẽ refactor theo một số hướng sau, tùy vào ngữ cảnh.
✅ 1. Sử dụng early return
để giảm mức lồng nhau
early return
để giảm mức lồng nhauTrước:
if (user != null) {
if (user.isActive()) {
if (user.getRole().equals("ADMIN")) {
// Do something
}
}
}
Sau:
if (user == null || !user.isActive() || !"ADMIN".equals(user.getRole())) {
return;
}
// Do something
Giúp code phẳng hơn, dễ đọc hơn.
✅ 2. Dùng switch
hoặc switch expression
(Java 14+)
switch
hoặc switch expression
(Java 14+)Thay vì:
if (status.equals("NEW")) { ... }
else if (status.equals("PROCESSING")) { ... }
else if (status.equals("DONE")) { ... }
Refactor:
switch (status) {
case "NEW" -> handleNew();
case "PROCESSING" -> handleProcessing();
case "DONE" -> handleDone();
}
✅ 3. Dùng Strategy Pattern nếu xử lý logic theo từng loại (phân nhánh phức tạp)
Ví dụ: xử lý thanh toán theo nhiều hình thức (CreditCard
, Momo
, Paypal
,...)
Em sẽ tạo interface:
public interface PaymentStrategy {
void pay(Order order);
}
Rồi inject Map<String, PaymentStrategy>
để thay if-else
.
✅ 4. Dùng Enum với behavior
Thay vì:
if (type.equals("PDF")) generatePdf();
else if (type.equals("EXCEL")) generateExcel();
Refactor với enum:
enum ReportType {
PDF { void generate() { /* ... */ }},
EXCEL { void generate() { /* ... */ }}
}
✅ 5. Áp dụng Optional và Stream để tránh null và rẽ nhánh đơn giản
✅ Kết luận:
Em luôn cố gắng giữ code ngắn gọn, rõ ràng và dễ test. Nếu có quá nhiều
if-else
, thì có thể code đang vi phạm Single Responsibility, cần chia nhỏ ra theo behavior hoặc dùng pattern phù hợp (Strategy, State, Factory, etc.).
So sánh shallow copy và deep copy trong Java. Khi nào dùng cái nào ?
Dạ, em xin trả lời câu hỏi này ngắn gọn, rõ ràng như khi đi phỏng vấn ạ:
🔹 Shallow Copy – Sao chép nông:
Khái niệm: Chỉ sao chép địa chỉ tham chiếu đến các đối tượng con. → Bản sao và bản gốc cùng trỏ đến các đối tượng con giống nhau.
Cách thực hiện: Dùng
clone()
hoặc copy constructor thông thường mà không tạo bản sao cho các field bên trong.Ví dụ:
class Person implements Cloneable {
String name;
Address address;
public Object clone() throws CloneNotSupportedException {
return super.clone(); // shallow copy
}
}
Thay đổi trong
address
của bản copy sẽ ảnh hưởng đến bản gốc.
🔹 Deep Copy – Sao chép sâu:
Khái niệm: Sao chép toàn bộ cấu trúc đối tượng, bao gồm cả các đối tượng con bên trong → Mỗi object là độc lập.
Cách thực hiện:
Triển khai
clone()
thủ công và clone các field sâu bên trong.Dùng thư viện như
Apache Commons Lang SerializationUtils.clone()
(nếu các object đềuSerializable
).Hoặc dùng Jackson để serialize sang JSON và parse lại (cách nhanh nhưng không tối ưu về hiệu suất).
Ví dụ:
class Person implements Cloneable {
String name;
Address address;
public Object clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = (Address) address.clone(); // deep copy
return cloned;
}
}
🟢 Khi nào dùng?
Shallow
Khi object đơn giản, không chứa object lồng nhau, hoặc không cần độc lập sâu
Deep
Khi cần độc lập hoàn toàn, tránh ảnh hưởng lẫn nhau
📝 Trong thực tế dự án:
Em từng dùng deep copy khi xử lý cấu trúc dữ liệu JSON chứa nhiều object con (ví dụ: cấu hình nested menu, hoặc giỏ hàng), để đảm bảo mỗi phiên bản không ảnh hưởng lẫn nhau khi thao tác.
Tại sao nên dùng Optinal trong Java ? Lợi ích là gì ?
Dạ, em xin trả lời ngắn gọn, rõ ràng theo phong cách đi phỏng vấn:
🔹 1. Tránh NullPointerException (NPE)
Optional
giúp diễn đạt rõ ràng rằng một biến có thể có hoặc không có giá trị.Thay vì phải kiểm tra null thủ công:
if (user != null && user.getEmail() != null) { ... }
Sử dụng
Optional
sẽ giúp code gọn hơn và an toàn hơn:userOptional.map(User::getEmail).ifPresent(System.out::println);
🔹 2. Giao tiếp rõ ràng giữa các API
Khi method trả về
Optional<T>
, nó nói rõ: “Giá trị này có thể vắng mặt”, người dùng API sẽ bắt buộc phải handle trường hợp không có giá trị.Ví dụ:
Optional<User> findById(Long id);
🔹 3. Kết hợp tốt với Functional Programming (Java 8+)
Optional
có nhiều method tiện lợi như:.map()
,.filter()
,.flatMap()
,.orElse()
,.orElseGet()
,.ifPresent()
Ví dụ:
String email = userOptional .filter(User::isActive) .map(User::getEmail) .orElse("no-reply@example.com");
✅ Tóm lại:
Tránh lỗi Null
An toàn hơn
Giao tiếp rõ ràng
Người dùng API phải handle null
Functional style
Gọn, dễ đọc, chain được logic
Em thường dùng
Optional
cho giá trị trả về của repository/service, còn với tham số truyền vào method, em tránh dùng Optional để không lạm dụng và tránh rối logic.
Khi nào nên override equals() ? Tại sao hashCode() và equals() nên override cùng nhau ?
Dạ, em xin trả lời như sau – đúng tinh thần một cuộc phỏng vấn Java backend:
✅ 1. Khi nào nên override equals()
?
equals()
?Khi đối tượng cần so sánh theo "giá trị" thay vì theo "tham chiếu", ta nên override equals()
.
Ví dụ:
Trong một hệ thống bán hàng, có class Product
:
class Product {
private String sku;
private String name;
}
→ Nếu muốn hai Product
được xem là bằng nhau nếu cùng sku
, ta override equals()
để định nghĩa logic so sánh riêng.
✅ 2. Tại sao phải override cả hashCode()
khi override equals()
?
hashCode()
khi override equals()
?Vì các collection như HashMap
, HashSet
dựa vào cả hashCode()
và equals()
để kiểm tra tính duy nhất.
🔸 Nguyên tắc quan trọng:
Nếu hai đối tượng
a.equals(b) == true
thì bắt buộca.hashCode() == b.hashCode()
.
📌 Nếu chỉ override equals()
mà không override hashCode()
, khi thêm object vào HashSet
, hệ thống sẽ không tìm đúng bucket, dẫn đến hành vi sai.
✅ Tóm lại:
Khi nào override equals()
?
Khi muốn so sánh theo giá trị, không phải tham chiếu
Có cần override hashCode()
?
Có, nếu đã override equals()
Nếu không override hashCode()
thì sao?
Các collection như HashSet
sẽ hoạt động sai
Tính đóng gói giúp gì cho maintainability ?
Dạ, em xin trả lời ngắn gọn và đúng trọng tâm như khi đi phỏng vấn:
🔹 1. Ẩn chi tiết triển khai (Implementation hiding)
Chỉ expose ra những gì cần thiết thông qua các method
get/set
hoặc service layer.Giúp thay đổi nội bộ mà không ảnh hưởng bên ngoài.
🧠 Ví dụ: Nếu sau này muốn đổi kiểu dữ liệu của một field (vd: int
→ BigDecimal
), chỉ cần thay đổi trong class, không ảnh hưởng code dùng bên ngoài nếu dùng getter.
🔹 2. Giảm phụ thuộc giữa các class
Khi dữ liệu được giữ private và chỉ truy cập qua method, sẽ giảm coupling → dễ thay đổi, dễ test.
🔹 3. Kiểm soát dữ liệu và logic
Có thể thêm validation, logging, lazy load... trong getter/setter mà client không biết.
✅ Kết luận
Tính đóng gói là nền tảng giúp hệ thống:
Dễ bảo trì
Dễ mở rộng
Ít bug khi refactor
→ Em luôn ưu tiên đóng gói tốt các entity và service logic để giữ hệ thống clean & maintainable.
Sự khác nhau giữa checked và unchecked exception ?
Dạ, câu này em xin trả lời ngắn gọn, dễ hiểu và đúng trọng tâm phỏng vấn như sau:
✅ Sự khác nhau giữa Checked và Unchecked Exception trong Java
Checked Exception
Unchecked Exception
Kế thừa từ
Exception
(trừ RuntimeException
)
RuntimeException
hoặc các class con
Kiểm tra lúc nào
Kiểm tra tại compile-time
Phát sinh tại runtime
Bắt buộc try/catch
✅ Bắt buộc hoặc khai báo throws
❌ Không bắt buộc
Ví dụ
IOException
, SQLException
NullPointerException
, IllegalArgumentException
🔹 Khi nào dùng cái nào?
Checked: Khi lỗi có thể phục hồi được → Ví dụ: đọc file, gọi API bên ngoài.
try { FileReader fr = new FileReader("data.txt"); } catch (IOException e) { // xử lý lỗi đọc file }
Unchecked: Lỗi do lập trình sai, không nên xử lý mà nên fix code.
String s = null; System.out.println(s.length()); // NullPointerException
✅ Tóm gọn:
Checked Exception bắt buộc xử lý – thường do yếu tố bên ngoài. Unchecked Exception là lỗi logic – nên fix code thay vì bắt lỗi.
Tại sao java không hỗ trợ mutiple inheritance với class ?
Dạ, em xin trình bày ngắn gọn, đúng tinh thần phỏng vấn như sau:
🔹 1. Tránh “Diamond Problem” – Vấn đề hình thoi
Giả sử:
class A {
void sayHello() { System.out.println("Hello from A"); }
}
class B extends A {
void sayHello() { System.out.println("Hello from B"); }
}
class C extends A {
void sayHello() { System.out.println("Hello from C"); }
}
// Nếu Java cho phép:
// class D extends B, C { } → Lỗi: Ambiguity!
Khi gọi sayHello()
từ D
, Java không biết nên gọi từ B hay từ C → gây mơ hồ, khó kiểm soát.
🔹 2. Đơn giản hóa hệ thống kế thừa
Java muốn giữ mọi thứ đơn giản, dễ hiểu, dễ bảo trì.
Kế thừa đơn (single inheritance) + interface đủ để xây hệ thống mạnh mẽ mà tránh xung đột.
🔹 3. Giải pháp thay thế: Interface
Từ Java 8,
interface
hỗ trợdefault method
, cho phép đa kế thừa hành vi, mà vẫn tránh mơ hồ bằng cách:Compiler sẽ yêu cầu dev override lại nếu có xung đột default method.
✅ Kết luận:
Java không hỗ trợ multiple inheritance với class để tránh sự mơ hồ, khó bảo trì, nhưng vẫn hỗ trợ thông qua
interface
, giúp linh hoạt mà an toàn.
Last updated