JAVA CORE + OOP + SPRING BOOT — Nội dung ôn tập cho fresher Backend (VPBank)
1. Danh sách chủ đề con theo độ ưu tiên
Ưu tiên CAO (gần như chắc chắn bị hỏi)
- OOP 4 tính chất (Encapsulation, Inheritance, Polymorphism, Abstraction) + ví dụ thực tế trong hệ thống ngân hàng
- Abstract class vs Interface (khác biệt, khi nào dùng cái nào, default method Java 8)
- Collections: HashMap hoạt động bên trong (hashing, bucket, collision, treeify Java 8), ArrayList vs LinkedList, HashSet
- equals() và hashCode() contract
- String immutability, String vs StringBuilder vs StringBuffer, String pool
- Exception: checked vs unchecked, try-catch-finally, try-with-resources, custom exception
- Spring IoC/DI: constructor injection vs field injection
- @Component vs @Service vs @Repository vs @Controller
- REST Controller: @RestController, @GetMapping/@PostMapping, @PathVariable vs @RequestParam vs @RequestBody, HTTP status codes
- @Transactional: hoạt động qua proxy, rollback rules, self-invocation pitfall — cực kỳ quan trọng với ngân hàng
- Exception handling trong Spring: @ControllerAdvice, @ExceptionHandler
- Java 8+: lambda, Stream API (map/filter/collect), Optional
Ưu tiên TRUNG BÌNH
- Bean scope (singleton, prototype, request) và bean lifecycle cơ bản (@PostConstruct, @PreDestroy)
- Spring Boot auto-configuration (@SpringBootApplication, @EnableAutoConfiguration, starter dependencies)
- Validation: @Valid, @NotNull/@NotBlank/@Size, xử lý MethodArgumentNotValidException
- JPA/Hibernate: Entity, lazy vs eager fetching, N+1 problem và cách fix (JOIN FETCH, @EntityGraph)
- Multithreading cơ bản: thread safety, race condition, synchronized, volatile khái niệm, tại sao dùng BigDecimal + đồng bộ khi xử lý tiền
- JVM cơ bản: Heap vs Stack, GC làm gì (không cần chi tiết thuật toán)
- Immutable object là gì, cách tạo, tại sao thread-safe
- Comparable vs Comparator
Ưu tiên THẤP (biết khái niệm là đủ, ít khi đào sâu với fresher)
- ConcurrentHashMap vs Collections.synchronizedMap
- ClassLoader, JIT compiler
- Propagation levels của @Transactional (REQUIRED vs REQUIRES_NEW — biết 2 cái này là đủ)
- Isolation levels (chỉ cần nêu tên và hiểu dirty read — sẽ trùng với phần Database)
- Spring AOP chi tiết (chỉ cần hiểu proxy để giải thích @Transactional)
- Functional interface tự viết, method reference chi tiết
- G1GC, ZGC chi tiết — không cần cho fresher
2. Câu hỏi phỏng vấn khả năng cao + hướng trả lời
Câu 1: Trình bày 4 tính chất OOP, cho ví dụ thực tế
- Encapsulation: che giấu dữ liệu, field
private + getter/setter. Ví dụ ngân hàng: Account không cho set balance trực tiếp, chỉ qua method deposit()/withdraw() có kiểm tra nghiệp vụ.
- Inheritance: kế thừa,
SavingsAccount extends Account.
- Polymorphism: cùng method, hành vi khác nhau —
calculateInterest() override khác nhau ở SavingsAccount và CurrentAccount; biến kiểu Account trỏ tới object con (runtime polymorphism). Phân biệt overloading (compile-time) vs overriding (runtime).
- Abstraction: chỉ lộ "làm gì", giấu "làm thế nào" — interface
PaymentService có nhiều implementation (NapasPayment, VisaPayment).
- Ý ăn điểm: ví dụ theo domain ngân hàng thay vì Animal/Dog/Cat; nói được encapsulation giúp bảo vệ dữ liệu nhạy cảm (balance không bao giờ bị sửa tùy tiện).
- Lỗi thường gặp: học thuộc định nghĩa nhưng không cho được ví dụ code; nhầm overloading với overriding; nói abstraction giống hệt encapsulation mà không phân biệt được.
Câu 2: Abstract class vs Interface — khác nhau gì, khi nào dùng?
- Abstract class: có constructor, field trạng thái (instance variables), method có body lẫn abstract; một class chỉ extends 1 abstract class.
- Interface: là contract; một class implements nhiều interface; field mặc định
public static final; từ Java 8 có default và static method.
- Quy tắc chọn: quan hệ "is-a" + muốn chia sẻ code/state → abstract class; định nghĩa khả năng/hợp đồng (can-do) → interface.
- Ví dụ:
AbstractAccount chứa logic chung ghi audit log; interface Transferable cho các loại tài khoản chuyển được tiền.
- Ý ăn điểm: nhắc Java 8 default method làm ranh giới hẹp lại nhưng interface vẫn không có state; nhắc Spring khuyến khích "program to interface" để dễ test/mock.
- Lỗi thường gặp: nói "interface không có method body" (sai từ Java 8); không trả lời được "khi nào dùng cái nào".
Câu 3: HashMap hoạt động như thế nào bên trong?
put(key, value): tính hashCode() của key → xác định index bucket trong mảng → nếu bucket trống thì đặt vào, nếu trùng bucket (collision) thì so equals(): trùng key thì ghi đè, khác key thì nối vào linked list.
- Java 8+: khi 1 bucket có > 8 node và mảng ≥ 64 → linked list chuyển thành red-black tree → worst case từ O(n) xuống O(log n).
- Resize: vượt
capacity * loadFactor (mặc định 16 * 0.75) → gấp đôi mảng, rehash.
get() trung bình O(1).
- Ý ăn điểm: nói được collision handling + treeify Java 8; nói được HashMap không thread-safe → nhiều thread put cùng lúc có thể mất dữ liệu, hệ thống giao dịch phải dùng
ConcurrentHashMap.
- Lỗi thường gặp: chỉ nói "lưu key-value" mà không giải thích được bucket/collision; nói HashMap có thứ tự (sai — LinkedHashMap/TreeMap mới có).
Câu 4: ArrayList vs LinkedList?
- ArrayList: mảng động — get theo index O(1), thêm/xóa giữa danh sách O(n) do dịch phần tử.
- LinkedList: doubly linked list — thêm/xóa ở đầu/cuối O(1), nhưng truy cập theo index O(n).
- Thực tế: ArrayList thắng gần như mọi trường hợp nhờ CPU cache locality và ít overhead bộ nhớ (LinkedList mỗi node tốn thêm 2 con trỏ).
- Ý ăn điểm: kết luận thực dụng "mặc định dùng ArrayList, LinkedList chỉ khi thêm/xóa đầu-cuối liên tục kiểu queue/deque" — thể hiện hiểu chứ không học vẹt.
- Lỗi thường gặp: trả lời máy móc "LinkedList insert nhanh hơn" mà không nói phải duyệt tới vị trí đó đã O(n); không nêu được Big-O.
Câu 5: Contract giữa equals() và hashCode()? Chuyện gì xảy ra nếu vi phạm?
- Contract: 2 object equals → hashCode phải bằng nhau; hashCode bằng nhau → chưa chắc equals (collision hợp lệ).
- Override equals mà không override hashCode → khi bỏ vào HashMap/HashSet: 2 object "bằng nhau" rơi vào bucket khác nhau →
contains()/get() trả false/null, HashSet chứa "duplicate".
- Ví dụ:
Transaction với field transactionId — dùng làm key dedupe giao dịch, nếu sai hashCode thì check trùng giao dịch thất bại → duplicate payment, lỗi nghiêm trọng trong ngân hàng.
- Nên dùng field bất biến, ổn định để tính equals/hashCode; có thể sinh bằng IDE/Lombok
@EqualsAndHashCode hoặc record.
- Ý ăn điểm: nêu hậu quả cụ thể trên HashMap/HashSet + liên hệ dedupe giao dịch; nhắc
== so sánh reference còn equals so sánh nội dung.
- Lỗi thường gặp: chỉ thuộc contract mà không nói được hậu quả thực tế; nói "hashCode bằng nhau thì equals" (chiều ngược sai).
Câu 6: Tại sao String là immutable? Lợi ích gì?
- Immutable: mọi thao tác (concat, replace) tạo String mới, object gốc không đổi.
- Lợi ích: (1) String pool tiết kiệm bộ nhớ và cho phép chia sẻ; (2) thread-safe tự nhiên; (3) an toàn khi làm key HashMap (hashCode cache, không đổi); (4) bảo mật — String chứa username/URL/config không bị thay đổi sau khi validate.
- Nối chuỗi trong vòng lặp → dùng
StringBuilder (StringBuffer là bản synchronized, chậm hơn).
"a" == "a" có thể true do pool, nhưng luôn so sánh String bằng equals().
- Ý ăn điểm (ngân hàng): nói thêm — mật khẩu nên lưu
char[] thay vì String vì String immutable nằm lại trong heap đến khi GC, không chủ động xóa được; char[] có thể wipe ngay. Interviewer bank rất thích ý này.
- Lỗi thường gặp: dùng
== so sánh String; nói StringBuffer nhanh hơn StringBuilder (ngược).
Câu 7: Checked vs unchecked exception? Bạn thiết kế exception thế nào trong ứng dụng?
- Checked (extends
Exception): compiler bắt buộc catch/throws — IOException, SQLException. Unchecked (extends RuntimeException): NullPointerException, IllegalArgumentException.
finally luôn chạy (trừ System.exit); try-with-resources tự đóng resource implements AutoCloseable — nêu được điểm này là cộng điểm.
- Thiết kế: tạo custom exception theo nghiệp vụ —
InsufficientBalanceException extends RuntimeException — throw từ service, bắt tập trung ở @ControllerAdvice trả error response chuẩn.
- Ý ăn điểm (ngân hàng): Spring mặc định chỉ rollback transaction với unchecked exception, không rollback với checked — nên business exception thường để unchecked hoặc dùng
rollbackFor; error response không được lộ stack trace/thông tin nội bộ ra client (security).
- Lỗi thường gặp:
catch (Exception e) {} nuốt exception; không biết finally vẫn chạy khi có return trong try; nghĩ Error (OutOfMemoryError) nên catch.
Câu 8: Stream API và Optional — dùng khi nào, cho ví dụ?
- Stream: xử lý collection kiểu declarative — ví dụ:
transactions.stream().filter(t -> t.getStatus() == SUCCESS).map(Transaction::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add) — tính tổng giao dịch thành công.
- Phân biệt intermediate (filter, map — lazy) vs terminal (collect, forEach, reduce — trigger thực thi); stream chỉ dùng 1 lần.
- Optional: container chống NullPointerException —
findById trả Optional<Account>, xử lý bằng orElseThrow(() -> new AccountNotFoundException(...)).
- Lambda = implementation ngắn gọn của functional interface (interface có đúng 1 abstract method).
- Ý ăn điểm: viết được 1 pipeline stream trên giấy/bảng không cần IDE; biết
orElseThrow idiom chuẩn trong service layer; nhắc tính tiền dùng BigDecimal chứ không double trong ví dụ.
- Lỗi thường gặp: gọi
optional.get() không check; dùng Optional làm field/parameter (anti-pattern, nó sinh ra cho return type); nghĩ stream thay đổi collection gốc.
Câu 9: Heap vs Stack? Garbage Collection làm gì?
- Stack: mỗi thread một stack, chứa local variable, reference, stack frame mỗi lần gọi method; hết method là giải phóng; tràn → StackOverflowError (đệ quy vô hạn).
- Heap: chứa object, chia sẻ giữa các thread; hết → OutOfMemoryError.
- GC: tự động thu hồi object không còn reference tới (unreachable); mô hình generational — Young Gen (Eden + Survivor, minor GC thường xuyên) và Old Gen (major GC, đắt hơn) vì đa số object chết trẻ.
- Lập trình viên không gọi GC trực tiếp;
System.gc() chỉ là gợi ý.
- Ý ăn điểm: giải thích "stop-the-world" ngắn gọn — GC pause có thể ảnh hưởng latency hệ thống giao dịch realtime, đó là lý do bank quan tâm tuning; biết memory leak vẫn xảy ra trong Java (static collection giữ reference mãi).
- Lỗi thường gặp: nói "Java có GC nên không bao giờ leak memory"; nhầm StackOverflowError với OutOfMemoryError.
Câu 10: Race condition là gì? synchronized giải quyết thế nào? Cho ví dụ chuyển tiền.
- Race condition: nhiều thread đọc-ghi chung dữ liệu, kết quả phụ thuộc thứ tự thực thi. Ví dụ kinh điển: 2 thread cùng
balance = balance - amount — thao tác đọc-tính-ghi không atomic → mất tiền hoặc rút quá số dư (ví dụ ngân hàng, bắt buộc nêu).
synchronized: chỉ 1 thread giữ lock của object/method tại một thời điểm → critical section chạy tuần tự; đồng thời đảm bảo visibility (thay đổi được thread khác nhìn thấy).
- Giải pháp khác nêu tên:
AtomicInteger/AtomicLong (CAS), ReentrantLock, ConcurrentHashMap; và ở tầng ứng dụng thực tế bank dùng database transaction + optimistic/pessimistic locking chứ không chỉ lock trong JVM (vì chạy nhiều instance).
volatile chỉ đảm bảo visibility, không đảm bảo atomicity — không thay được synchronized cho count++.
- Ý ăn điểm: chỉ ra synchronized trong JVM không đủ khi app scale nhiều instance → cần lock ở DB; nói
balance++/check-then-act là 2 bước không atomic.
- Lỗi thường gặp: nghĩ volatile làm
i++ an toàn; nói "dùng synchronized cho tất cả" mà không biết trade-off performance/deadlock.
Câu 11: IoC và DI trong Spring là gì? Tại sao constructor injection tốt hơn field injection?
- IoC (Inversion of Control): framework (Spring container) tạo và quản lý vòng đời object (bean) thay cho việc tự
new.
- DI (Dependency Injection): container tiêm dependency vào bean — qua constructor, setter, hoặc field (@Autowired).
- Lợi ích: loose coupling, dễ thay implementation, dễ viết unit test (mock dependency).
- Constructor injection tốt hơn vì: dependency bắt buộc và rõ ràng, field có thể là
final (immutable), test không cần Spring context, phát hiện circular dependency ngay lúc khởi động.
- Ý ăn điểm: nói "constructor injection + final field" là best practice hiện tại,
@RequiredArgsConstructor của Lombok; giải thích được test được service mà không cần start Spring.
- Lỗi thường gặp: định nghĩa vòng vo IoC = DI (DI là một cách hiện thực IoC); chỉ biết
@Autowired trên field mà không biết vì sao bị chê.
Câu 12: @Component, @Service, @Repository, @Controller khác nhau gì? Bean scope mặc định là gì?
- Cả 4 đều là stereotype đánh dấu class thành Spring bean (component scan); về cơ chế tạo bean thì như nhau.
- Khác biệt: ngữ nghĩa phân lớp (đọc code rõ ràng) +
@Repository có thêm exception translation (SQLException → DataAccessException), @Controller/@RestController gắn với Spring MVC (@RestController = @Controller + @ResponseBody).
- Scope mặc định: singleton — 1 instance cho cả container → bean không được giữ state theo request (field mutable trong service là bug thread-safety).
- Các scope khác: prototype (mỗi lần inject tạo mới), request, session.
- Lifecycle cơ bản: instantiate → inject dependency →
@PostConstruct → sử dụng → @PreDestroy.
- Ý ăn điểm (ngân hàng): chỉ ra singleton + field mutable = race condition khi nhiều request đồng thời — ví dụ cache
currentUser vào field của service là lỗi bảo mật nghiêm trọng (user A thấy dữ liệu user B).
- Lỗi thường gặp: nói @Service và @Component "khác nhau về chức năng" một cách bịa đặt; không biết scope mặc định là singleton.
Câu 13: Spring Boot auto-configuration hoạt động thế nào? @SpringBootApplication gồm những gì?
@SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan.
- Auto-configuration: dựa trên classpath và các
@Conditional... (@ConditionalOnClass, @ConditionalOnMissingBean) — thấy spring-boot-starter-web có Tomcat + Spring MVC → tự cấu hình embedded server, DispatcherServlet; thấy driver + datasource properties → tự tạo DataSource.
- Starter dependencies: gom sẵn bộ dependency tương thích (spring-boot-starter-data-jpa, -web, -validation).
- Override: chỉ cần tự khai báo bean cùng loại (
@ConditionalOnMissingBean sẽ nhường) hoặc set trong application.yml.
- Ý ăn điểm: nêu được cơ chế conditional thay vì chỉ nói "Spring Boot tự làm hết"; biết
application.yml/properties và profile (dev/uat/prod — môi trường bank luôn tách profile).
- Lỗi thường gặp: nói Spring Boot là framework khác hẳn Spring (nó xây trên Spring); không phân biệt được Spring MVC và Spring Boot.
Câu 14: Thiết kế REST API tạo giao dịch chuyển tiền: annotation nào, validate ra sao, lỗi trả về thế nào?
@RestController + @PostMapping("/api/v1/transfers"), nhận @RequestBody @Valid TransferRequest; phân biệt @PathVariable (định danh resource) vs @RequestParam (filter/paging) vs @RequestBody (payload).
- Validation: Bean Validation trên DTO —
@NotBlank accountNumber, @NotNull @DecimalMin("0.01") amount (kiểu BigDecimal); vi phạm → MethodArgumentNotValidException.
- Exception handling tập trung:
@RestControllerAdvice + @ExceptionHandler → trả JSON error chuẩn (code, message, timestamp) — validation lỗi trả 400, không tìm thấy 404, không đủ số dư 400/422, lỗi hệ thống 500.
- HTTP status đúng ngữ nghĩa: 201 Created khi tạo thành công; POST không idempotent → thực tế bank thêm idempotency key chống double-submit (nêu được là điểm cộng lớn).
- Ý ăn điểm (ngân hàng): amount dùng
BigDecimal không dùng double (sai số float với tiền là điều cấm kỵ); error message không lộ chi tiết nội bộ; validate ở server bất kể client đã validate.
- Lỗi thường gặp: mọi lỗi đều trả 500 hoặc 200 kèm message lỗi; validate bằng if-else rải rác trong controller; dùng double cho tiền.
Câu 15: @Transactional hoạt động như thế nào? Vì sao gọi method @Transactional từ method khác cùng class lại không có transaction?
- Cơ chế: Spring tạo proxy bọc quanh bean (JDK dynamic proxy theo interface hoặc CGLIB subclass); lời gọi từ bên ngoài đi qua proxy → proxy mở transaction trước, commit khi thành công, rollback khi có exception.
- Self-invocation pitfall:
this.innerMethod() gọi nội bộ đi thẳng vào object thật, không qua proxy → @Transactional trên innerMethod bị bỏ qua. Fix: tách sang bean khác, hoặc inject chính nó/ApplicationContext để gọi qua proxy.
- Rollback mặc định: chỉ với
RuntimeException/Error; checked exception không rollback → cần @Transactional(rollbackFor = Exception.class) nếu muốn.
@Transactional chỉ hiệu lực trên method public (với proxy mặc định).
- Ví dụ ngân hàng bắt buộc nêu: chuyển tiền = trừ tài khoản A + cộng tài khoản B trong một transaction — lỗi ở bước 2 thì bước 1 phải rollback, đảm bảo Atomicity; biết thêm propagation REQUIRED (mặc định, join transaction hiện có) vs REQUIRES_NEW (ghi audit log độc lập, rollback nghiệp vụ vẫn giữ log) là điểm cộng lớn.
- Lỗi thường gặp: không biết self-invocation (câu bẫy kinh điển, fresher trượt rất nhiều ở đây); tưởng mọi exception đều rollback; đặt @Transactional trên private method.
Câu 16: Lazy vs Eager fetching trong JPA? N+1 problem là gì và fix thế nào?
FetchType.LAZY: chỉ load association khi truy cập lần đầu (mặc định của @OneToMany/@ManyToMany); EAGER: load ngay cùng entity chính (mặc định của @ManyToOne/@OneToOne).
- LazyInitializationException: truy cập lazy collection khi session/persistence context đã đóng (ví dụ trong controller sau khi transaction kết thúc).
- N+1: query 1 lần lấy N account, rồi mỗi account lại 1 query lấy danh sách transaction → N+1 query, hệ thống chậm nghiêm trọng khi N lớn.
- Fix:
JOIN FETCH trong JPQL, @EntityGraph, hoặc batch fetching (@BatchSize/default_batch_fetch_size); phát hiện bằng cách bật log SQL (show-sql) khi dev.
- Best practice: để LAZY mặc định + fetch chủ động theo use case; trả về DTO thay vì trả entity thẳng ra API (tránh lộ field nhạy cảm + tránh serialize lazy collection).
- Ý ăn điểm (ngân hàng): liên hệ hiệu năng — màn hình sao kê 1000 giao dịch mà dính N+1 là 1001 query; ý "không expose entity ra API vì lộ dữ liệu nhạy cảm" chạm đúng mối quan tâm bảo mật của bank.
- Lỗi thường gặp: fix N+1 bằng cách đổi hết sang EAGER (đẩy vấn đề nặng hơn); không giải thích được LazyInitializationException từ đâu ra.
3. Lưu ý xuyên suốt khi trả lời (góc độ ngân hàng — nên chủ động lồng vào)
- Tiền = BigDecimal: bất cứ khi nào ví dụ có amount/balance, dùng
BigDecimal (so sánh bằng compareTo, không equals vì scale). Nói được lý do sai số nhị phân của double là điểm cộng ở mọi câu.
- Transaction & Atomicity: mọi ví dụ chuyển tiền/ghi sổ đều nhấn "hoặc tất cả thành công, hoặc rollback toàn bộ" — kết nối @Transactional với chữ A trong ACID.
- Thread safety: hệ thống bank chịu concurrent request cao — chủ động nhắc singleton bean stateless, ConcurrentHashMap, lock ở DB khi multi-instance.
- Bảo mật dữ liệu: không lộ stack trace ra API, không expose entity, không log số thẻ/mật khẩu, password dùng char[] — rải các ý này tự nhiên vào câu trả lời.
- Idempotency: chống double-submit giao dịch (idempotency key, unique constraint trên transactionId) — fresher nêu được ý này thường gây ấn tượng mạnh.
- Khi không biết câu trả lời sâu (ví dụ GC tuning, isolation level chi tiết): trả lời phần cơ bản chắc chắn rồi nói thẳng giới hạn hiểu biết — interviewer bank đánh giá cao trung thực hơn là bịa.