0

"HỔNG" kiến thức nền tảng!

Java Core Thực Chiến

Chào mọi người, tiếp nối series "vừa làm đồ án vừa ôn phỏng vấn" hôm trước chúng ta đã cùng nhau vượt ải SQL.
Hôm nay, Hoàn sẽ chuyển sang cửa ải khác khoai hơn: Java Core.

Lúc trước, mình từng có một suy nghĩ khá ngây thơ:

Đi làm Backend bây giờ xài Spring Boot, cứ gõ mấy cái annotation @Controller, @Service, gọi hàm thêm/sửa/xóa là chạy tằng tằng.
Mấy cái lý thuyết khô khan chắc chỉ để thi thôi, đào sâu làm gì.

Nhưng khi đi phỏng vấn thực tế, các anh chị phập một cú tỉnh mộng.

Họ hiếm khi bắt ứng viên cấu hình nguyên một project.
Thay vào đó họ đưa ra vài dòng code ngắn và hỏi:

  • Biến này lưu ở đâu trong RAM?
  • Kết quả trả về là gì?
  • Tại sao lại thế?

Sự thật là:

Framework có thể update mỗi năm, nhưng core thì hàng chục năm vẫn vậy.

Nếu không hiểu cơ chế lưu trữ hay cách Java truyền dữ liệu, code khi đưa lên production rất dễ:

  • dính lỗi memory leak
  • sai logic khó hiểu
  • performance kém

Hôm nay, cùng Hoàn chiến một trong những kiến thức kinh điển để chuẩn bị phỏng vấn.


1. Java Memory

Khi Java chạy, RAM chia thành nhiều vùng.
Nhưng dev backend thường chỉ cần hiểu 2 vùng chính:

Vùng Chứa gì
Stack biến, reference
Heap object thật

Stack

Vùng nhớ Stack có đặc điểm:

  • nhỏ
  • rất nhanh
  • tự dọn dẹp

Stack chứa:

  • các biến nguyên thủy (int, double, boolean...)
  • các biến tham chiếu (reference variables)

Khi một hàm chạy xong, Stack sẽ tự động xóa toàn bộ biến trong hàm đó.


Heap

Vùng nhớ Heap là nơi chứa object thật.

Các object được tạo ra bằng từ khóa:

new

Dữ liệu ở đây không tự mất đi, mà phải đợi Garbage Collector (GC) dọn dẹp.


Ví dụ

KhachHang kh = new KhachHang("Hoàn");

Trong RAM sẽ xảy ra 2 việc.

a) Heap

Tạo object thật:

Object KhachHang
ten = "Hoàn"

b) Stack

Tạo biến tham chiếu kh:

kh → địa chỉ của object trên Heap

Hình dung:

STACK              HEAP
-----              ----------------
kh   ----------->  Object KhachHang
                   ten = "Hoàn"

👉 kh không phải object

👉 nó chỉ là biến giữ địa chỉ của object


🔥 Chốt hạ: Java chỉ có Pass-by-value

Khi truyền một biến object vào hàm:

Java copy giá trị địa chỉ của biến đó, chứ không truyền object gốc.


2. String Pool và Tính bất biến

Trong Java, String không phải kiểu nguyên thủy như int hay double.

Nó là một object, nhưng lại có cơ chế đặc biệt:

  • String Pool
  • Immutable (bất biến)

Đây là nơi chứa rất nhiều cú lừa khi đi phỏng vấn.


2.1 Cú lừa kinh điển: ==.equals()

Giả sử hệ thống có tính năng nhập mã giảm giá:

String maHeThong = "SALE83";
String maKhachNhap = new String("SALE83");

if (maHeThong == maKhachNhap) {
    System.out.println("Mã hợp lệ, giảm giá ngay!");
} else {
    System.out.println("Mã sai rồi!");
}

Kết quả:

Mã sai rồi!

Vì sao?

Toán tử == so sánh địa chỉ bộ nhớ.

maHeThong  → Object A
maKhachNhap → Object B

Hai object khác nhau → false.


.equals()

.equals() so sánh nội dung thực sự bên trong object.

maHeThong.equals(maKhachNhap)

Java sẽ kiểm tra:

SALE83 == SALE83

true


Bài học

Khi so sánh String trong Java:

KHÔNG dùng ==
LUÔN dùng .equals()

2.2 String Pool - Bể chứa tiết kiệm RAM

Java có một khu vực đặc biệt trong Heap gọi là:

String Pool

Mục đích:

Tránh tạo nhiều String giống nhau để tiết kiệm RAM

Ví dụ

String a = "java";
String b = "java";

Java sẽ:

  1. Kiểm tra String Pool
  2. Nếu đã có "java" → không tạo object mới

RAM lúc này:

String Pool

   "java"
    ↑   ↑
    a   b

ab trỏ tới cùng một object.


2.3 String Immutable

Immutable nghĩa là:

Giá trị của String không thể thay đổi sau khi được tạo

Ví dụ:

String s = "Hello";
s = s + " World";

Nhiều người nghĩ:

Hello → Hello World

Nhưng thực tế Java làm:

  1. Tạo object mới "Hello World"
  2. Cho s trỏ sang object mới
  3. Object "Hello" cũ trở thành garbage

2.4 Vấn đề Performance và giải pháp StringBuilder

Vấn đề

Nếu nối chuỗi bằng + trong vòng lặp lớn:

String result = "";

for(int i = 0; i < 10000; i++){
    result = result + i;
}

String là immutable nên mỗi lần +:

tạo object mới

Nếu lặp 10.000 lần:

10.000 object mới

Hệ quả:

  • tốn RAM
  • tạo nhiều garbage
  • GC chạy liên tục
  • server backend có thể lag

Giải pháp: StringBuilder

StringBuilder sb = new StringBuilder();

for(int i = 0; i < 10000; i++){
    sb.append(i);
}

String result = sb.toString();

Ưu điểm

StringBuildermutable.

Nó chỉ thay đổi nội dung trên cùng một object, không tạo object mới liên tục.

Performance tốt hơn rất nhiều.

3. OOP thực chiến

OOP (Object Oriented Programming) là nền tảng của Java. Nhưng khi đi phỏng vấn Backend, Interviewer không hỏi định nghĩa sách giáo khoa mà thường hỏi

  • Vì sao phải dùng OOP?
  • OOP giúp code backend tốt hơn ở điểm nào?
  • 4 tính chất của OOP áp dụng trong thực tế ra sao?

Java OOP xoay quanh 4 trụ cột chính:

  1. Encapsulation (Đóng gói)
  2. Inheritance (Kế thừa)
  3. Polymorphism (Đa hình)
  4. Abstraction (Trừu tượng)

3.1 Encapsulation (Đóng gói)

Encapsulation nghĩa là:

Ẩn dữ liệu bên trong object và chỉ cho phép truy cập thông qua method

Ví dụ class User:

public class User {

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Vì sao phải Encapsulation?

Nếu để public

user.age = -100;

Hệ thống có thể bị dữ liệu sai logic.

Khi dùng setter, ta có thể kiểm soát:

public void setAge(int age){
    if(age > 0){
        this.age = age;
    }
}

→ đảm bảo data integrity

👉 Bài học: Đóng gói không chỉ là tạo Getter/Setter cho có. Bản chất của nó là Bảo vệ toàn vẹn dữ liệu (Data Integrity), ép mọi luồng dữ liệu phải đi qua màng lọc kiểm tra logic.

3.2 Inheritance (Kế thừa)

**Inheritance** cho phép một class dùng lại thuộc tính và phương thức từ class khác.

Ví dụ thực tế Backend (Tránh lặp code): Trong Database, bảng nào cũng cần các cột id, created_at, updated_at.

// Class Cha
@MappedSuperclass
public class BaseEntity {
    private Long id;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

// Class Con
public class User extends BaseEntity {
    private String username;
}

public class Order extends BaseEntity {
    private double totalAmount;
}

Cả UserOrder đều tự động có id và thời gian tạo mà không cần viết lại.

3.3 Polymorphism (Đa hình)

Polymorphism nghĩa là :

Cùng một lời gọi hàm, nhưng nhiều cách thực thi khác nhau (tuỳ vào object đang chạy)

Đây là tính chất QUAN TRỌNG NHẤT để xây dựng Spring Boot.

Ví dụ tính năng thanh toán:

// Thằng Cha (Interface)
interface PaymentService {
    void pay(int amount);
}

// Thằng Con 1
class MomoPayment implements PaymentService {
    public void pay(int amount) { System.out.println("Thanh toán Momo"); }
}

// Thằng Con 2
class VnPayPayment implements PaymentService {
    public void pay(int amount) { System.out.println("Thanh toán VNPay"); }
}

**Sử dụng **

PaymentService p = new MomoPayment();
p.pay(100); // In ra: Thanh toán Momo

Tại sao nó quan trọng ? Nhờ tính đa hình, code của ta chỉ cần gọi thằng cha PaymentService chuyện bơm thằng con nào bào Momo hay VNPay là việc của Spring Boot giúp code cực kỳ linh hoạt.

3.4 Abstraction (Trừu tượng)

Abstraction nghĩa là:

Chỉ định ngĩa "Nó làm gì" Hành vi và giấu đi "nó làm như thế nào" (implementation)

Ví dụ thực tế trong kiến trúc Spring Boot: Chia code rõ ràng cho các tầng : Controller -> Service -> Repository

// 1. Chỉ định nghĩa chức năng (Interface)
public interface UserService {
    User getUserById(Long id);
}

// 2. Code chi tiết cách làm thực thi(Implementation)
@Service
public class UserServiceImpl implements UserService {
    public User getUserById(Long id) {
        // Viết logic móc database ở đây...
    }
}

Tác dụng: Controller chỉ cần: userService.getUserById(1L);

Không cần biết:

  • query MySQL
  • gọi Redis
  • hay call API

Bài học

Abstraction giúp:

  • giảm độ phức tạp
  • tách biệt các layer
  • code dễ maintain hơn

3.5 Các câu hỏi OOP "Hỏi xoáy đáp xoay" (Chắc chắn gặp)

Thuộc 4 tính chất trên chưa đủ để pass qua được phỏng vấn Dưới đây là những câu hỏi dùng để phân loại ứng viên:

Bạn là thợ code hay là một kỹ sư phần mềm

Câu 1: Phân biệt Interface và Abstract Class? Khi nào dùng?

Đây là câu hỏi quốc dân.

Đừng trả lời lan man -> chốt hạn luôn bằng bảng:

Tiêu chí Interface Abstract Class
Bản chất Thể hiện Hành vi, Năng lực (CAN-DO) Thể hiện Cội nguồn, Bản chất (IS-A)
Đa kế thừa Một class có thể implements nhiều Interface Một class chỉ được extends 1 Abstract Class
Biến (State) Chỉ chứa hằng số (public static final) Có thể chứa biến bình thường (State)
Mục đích Định nghĩa Contract Chia sẻ code chung

👉 Mẹo trả lời thực tế:

  • "Dạ, em dùng Abstract Class khi các class con có chung huyết thống và muốn dùng chung một số logic code (Ví dụ: NhanVien sinh ra ThoCatTocThuNgan)."
  • "Em dùng Interface khi các class chả liên quan gì đến nhau, nhưng em muốn ép chúng nó phải tuân thủ chung một quy tắc (Ví dụ: Thanh toán MomoVNPay). Interface cũng là nền tảng để Spring Boot làm Dependency Injection ạ."

Câu 2: Composition vs Inheritance? Tại sao nên hạn chế kế thừa?

Nếu bạn trả lời được câu này, nhà tuyển dụng sẽ đánh giá bạn có tư duy System Design cực tốt.

  • Inheritance (Kế thừa): Mối quan hệ IS-A (Là một).
    • Ví dụ: Chó là một Động vật.
    • Nhược điểm: Ràng buộc cực kỳ chặt (Tight Coupling). Thằng Cha hắt hơi là thằng Con cảm cúm. Thằng Cha sửa code là toàn bộ hệ thống class Con phải test lại.
  • Composition (Lắp ráp/Bao gộp): Mối quan hệ HAS-A (Có một).
    • Ví dụ: Ô tô có một Động cơ. 👉 Câu chốt ăn tiền: "Dạ đi làm thực tế, em tuân thủ nguyên tắc Favor Composition over Inheritance (Ưu tiên lắp ráp hơn kế thừa). Thay vì bắt class này kế thừa class kia lằng nhằng, em sẽ tách các tính năng ra thành các Object độc lập và 'nhúng' (inject) chúng vào nhau. Việc này giúp code lỏng lẻo hơn (Loose Coupling), rất dễ nâng cấp và bảo trì ạ."

Câu 3: Overloading vs Overriding?

Cả 2 đều là Đa hình (Polymorphism), nhưng thời điểm hoạt động khác nhau hoàn toàn:

  • Overloading (Nạp chồng - Compile time): Cùng một tên hàm trong 1 class, nhưng khác tham số truyền vào.
    • Ví dụ: tinhToan(int a)tinhToan(int a, int b). Code viết ra là Java biết ngay sẽ gọi hàm nào lúc biên dịch.
  • Overriding (Ghi đè - Runtime): Class Con viết lại (đè lên) hàm của class Cha.
    • Ví dụ: Cha có hàm hienThi(), Con cũng có hàm hienThi() nhưng in ra câu khác. Phải đợi đến lúc phần mềm thực sự chạy (Runtime), Java mới quyết định được là sẽ gọi hàm hienThi() của Cha hay của Con (dựa vào object được new ra).

Câu 4: Đa hình được dùng thế nào trong Spring Boot?

Nếu họ hỏi câu này, hãy lôi ngay khái niệm Dependency Injection (DI) ra vả lại:

"Dạ nhờ có tính Đa Hình, ở tầng Service em chỉ cần khai báo biến của thằng Cha (Interface). Còn việc lúc chạy nó gọi thằng Con nào là do Spring Boot tự động tiêm (Inject) vào thông qua annotation @Autowired. Nếu không có Đa Hình, code của em sẽ bị dính cứng (hardcode) vào một class cụ thể, lúc muốn đổi thư viện hay đổi database sẽ phải đập đi viết lại toàn bộ ạ."

4. Giải phẫu Collections Framework & Generics

Nếu OOP là xương sống thì Collections Framework chính là cơ bắp của Java Backend.

Đi phỏng vấn, đây là phần bị xoáy cực kỳ nhiều vì nó liên quan trực tiếp đến:

  • Hiệu năng hệ thống (Performance).
  • Cách xử lý và lưu trữ dữ liệu dưới RAM.
  • Tư duy chọn đúng cấu trúc dữ liệu cho đúng bài toán.

4.1 Collections Framework và Generics <T> là gì?

Hiểu đơn giản:
Collections = Tập hợp các cấu trúc dữ liệu xịn xò có sẵn trong Java.

Thay vì phải tự viết code mảng động, danh sách liên kết, cây nhị phân... Java đã code sẵn cho bạn.

Cây gia phả rút gọn:

Collection
   |--- List (Có thứ tự, cho phép trùng)
   |--- Set  (Không thứ tự, CẤM trùng lặp)
   |--- Queue (Hàng đợi FIFO)

Map (Đứng riêng một góc trời - Lưu theo cặp Key:Value)

Generics <T> sinh ra để làm gì?

Ngày xưa code Java không có cái ngoặc nhọn <T> này, bạn nhét String hay Integer vào chung một List đều được → Lúc lấy ra dùng rất dễ bị lỗi sập chương trình (ClassCastException).

👉 Chốt phỏng vấn:
"Dạ, Generics sinh ra để ép kiểu an toàn lúc viết code (Compile-time Type Safety). Nó báo lỗi ngay lúc code nếu nhét nhầm kiểu dữ liệu, giúp hệ thống không bị crash khi chạy thực tế."


4.2 List: Cuộc chiến kinh điển ArrayList vs LinkedList

Cả 2 đều cho phép lưu trùng lặp và truy xuất bằng Index. Nhưng dưới bộ nhớ RAM, chúng là 2 thế giới khác biệt.

🔹 ArrayList

List<String> list = new ArrayList<>();

Bản chất: Là một Mảng động (Dynamic Array). Các ô nhớ nằm liền kề nhau.

  • ✅ Ưu điểm: Truy vấn cực nhanh O(1). Chỉ cần biết Index là nhảy tót đến đúng ô nhớ đó.
  • ❌ Nhược điểm: Thêm/Xóa ở giữa rất chậm O(n) (Vì phải dịch chuyển toàn bộ các phần tử phía sau lùi lại 1 ô).

🔹 LinkedList

List<String> list = new LinkedList<>();

Bản chất: Là Danh sách liên kết (Doubly Linked List). Các ô nhớ (Node) nằm rải rác dưới Heap, ô trước cầm dây buộc vào ô sau.

  • ✅ Ưu điểm: Thêm/Xóa cực nhanh O(1) (Chỉ cần cắt dây buộc lại).
  • ❌ Nhược điểm: Truy vấn chậm O(n) (Phải dò từ đầu đến vị trí cần tìm).

👉 Câu chốt ăn tiền:

"Lý thuyết là vậy, nhưng đi làm thực tế Backend 95% em dùng ArrayList. Vì hệ thống chủ yếu là lấy dữ liệu ra đọc (Truy vấn), rất ít khi chèn vào giữa. Dùng LinkedList đẻ ra quá nhiều Node rải rác sẽ làm rác bộ nhớ (Garbage) khiến server chậm ạ."


4.3 Set: Sát thủ diệt trùng lặp (HashSet)

Đặc điểm:

  • Không có thứ tự Index
  • Tuyệt đối CẤM phần tử trùng lặp

Rất hợp để lưu danh sách ID, danh sách Email để không bị gửi thư rác 2 lần.

❓ Câu hỏi xoáy:

"Làm sao HashSet biết một phần tử đã tồn tại hay chưa để mà chặn nó lại? Nó dùng vòng lặp à?"

(Nếu bạn trả lời "Dùng vòng lặp" → Rớt ngay lập tức!)


🔥 Sự thật dưới RAM:

Bản chất của HashSet chính là một cái... HashMap giấu mặt.

  • Khi thêm phần tử → nó dùng phần tử đó làm KEY của HashMap
  • Dùng hashCode() để băm dữ liệu
  • Dùng equals() để kiểm tra trùng

👉 Tốc độ: O(1)


4.4 Map: Trùm cuối phỏng vấn (HashMap)

HashMap lưu dữ liệu theo cặp Key - Value.
Tốc độ tìm kiếm: O(1) (Nhanh nhất thế giới Java).


❓ HashMap hoạt động như thế nào?

3 bước cần nói khi phỏng vấn:

  1. Bản chất:
    Là sự kết hợp của:

    • Mảng (Array)
    • Danh sách liên kết (LinkedList)

    → Mảng được chia thành các Bucket


  1. Cơ chế Put (Thêm vào):
  • Băm Key bằng hashCode() → ra Index
  • Nhét vào đúng Bucket đó

  1. Collision (Đụng độ):

Nếu 2 Key khác nhau nhưng cùng Index:

  • Java xử lý bằng Chaining
  • Tạo thành LinkedList trong Bucket đó

🚀 Cải tiến Java 8+ (ăn điểm mạnh)

"Dạ thưa anh, từ Java 8, nếu LinkedList tại một Bucket dài quá 8 phần tử, nó sẽ chuyển thành Red-Black Tree để tối ưu từ O(n) xuống O(log n) ạ."


📌 Bảng tra cứu nhanh khi đi làm

Yêu cầu bài toán Collection nên dùng Ví dụ thực tế
Cần truy vấn nhanh bằng Index ArrayList Danh sách lịch sử giao dịch
Hàng đợi (FIFO) Queue / LinkedList Message Queue
Loại bỏ trùng lặp HashSet Danh sách Email
Lưu cặp Key-Value HashMap Tra tên theo CMND
Có thứ tự sắp xếp TreeMap Bảng xếp hạng

4.5 Bảng tổng kết "Vàng" List - Set - Map

Tiêu chí List (Danh sách) Set (Tập hợp) Map (Từ điển)
Đặc điểm cốt lõi Lưu theo một hàng dọc Lọc trùng lặp Lưu Key - Value
Thứ tự ✅ Có ❌ Không ❌ Không
Trùng lặp ✅ Cho phép ❌ Không Key ❌ / Value ✅
Truy xuất Theo Index contains() Theo Key
Class phổ biến ArrayList HashSet HashMap
Khi nào dùng Danh sách, phân trang Dữ liệu unique Tra cứu nhanh

👉 Mẹo nhớ nhanh

  • Nghe "Danh sách" → dùng List
  • Nghe "Không trùng" → dùng Set
  • Nghe "Tra cứu", "Key-Value" → dùng Map

5. Xử lý ngoại lệ (Exception) - Cái bẫy sập Database

Khi mới học, mình thường nghĩ: "Code có khả năng lỗi thì cứ bọc try-catch rồi in ra màn hình là xong". Nhưng đi làm thực tế, xử lý Exception sai cách trong Spring Boot có thể dẫn đến rác dữ liệu, thậm chí là mất tiền của khách hàng.

Nhà tuyển dụng sẽ không bắt bạn viết cú pháp try-catch, họ sẽ xoáy vào bản chất:

5.1 Phân biệt Checked Exception và Unchecked Exception?

Đây là kiến thức bắt buộc phải nằm lòng. Nhầm lẫn 2 cái này là toang kiến trúc!

Tiêu chí Checked Exception Unchecked Exception (RuntimeException)
Thời điểm phát hiện Lúc gõ code (Compile-time). Thằng IDE gạch đỏ ép bạn phải xử lý. Lúc chạy phần mềm (Runtime). Gõ code thoải mái không ai báo lỗi.
Bắt buộc xử lý? . Bắt buộc dùng try-catch hoặc throws ở đầu hàm. KHÔNG. Thường do lỗi logic của lập trình viên.
Ví dụ kinh điển IOException (Lỗi đọc file), SQLException (Lỗi DB) NullPointerException (NPE), IndexOutOfBoundsException

👉 Mẹo nhớ để chém gió: * Cái gì do yếu tố khách quan (mất mạng, file bị xóa, DB sập) -> Thường là Checked.

  • Cái gì do mình code ẩu (gọi hàm từ object rỗng, chia cho số 0) -> Chắc chắn là Unchecked.

5.2 ❓ Câu hỏi "ăn tiền": Exception ảnh hưởng gì đến @Transactional?

Đây là câu hỏi phân loại ứng viên cực gắt cho vị trí Backend (đặc biệt khi làm với Spring Boot).

Khi làm tính năng liên quan đến nhiều bước (VD: Chuyển tiền = Trừ tiền người A + Cộng tiền người B), ta thường gắn @Transactional để: "Nếu có lỗi xảy ra ở giữa chừng, thì Rollback (hoàn tác) lại toàn bộ, coi như chưa có chuyện gì xảy ra".

CÚ LỪA KINH ĐIỂN CỦA SPRING BOOT:

Mặc định, @Transactional CHỈ ROLLBACK nếu code văng ra Unchecked Exception (RuntimeException). Nếu hệ thống văng ra Checked Exception, nó KHÔNG HỀ ROLLBACK! Tiền của A đã bị trừ, nhưng B không được cộng. Rất nguy hiểm!

👉 Cách trả lời chốt hạ: "Dạ thưa anh, vì cơ chế mặc định nguy hiểm này của Spring Boot, nên khi đi làm thực tế, ở các hàm quan trọng em luôn phải cấu hình rõ ràng là @Transactional(rollbackFor = Exception.class). Việc này bắt Spring Boot phải Rollback hệ thống với TẤT CẢ các loại lỗi (dù là Checked hay Unchecked) để đảm bảo tính toàn vẹn dữ liệu (Data Integrity) ạ."


5.3 Đi làm thực tế: Nên Catch (Bắt) hay Throw (Ném)?

Nhiều Intern/Fresher mắc một thói quen rất xấu: Ở đâu cũng viết try-catch. Code trong Controller cũng try, trong Service cũng try. Hậu quả là code lồng ghép vào nhau (spaghetti) cực kỳ rác và khó bảo trì.

Tư duy chuẩn thực chiến Backend:

  • Ở tầng Service: Khi có lỗi logic (ví dụ: Không tìm thấy User), TUYỆT ĐỐI ĐỪNG try-catch nuốt lỗi. Hãy ném nó ra (Throw):
    if (user == null) {
        throw new CustomNotFoundException("Không tìm thấy khách hàng này!");
    }
    

Vậy tóm lại ai là người bắt lỗi?

👉 "Dạ, em sẽ không catch rải rác. Em sử dụng @RestControllerAdvice (hoặc @ControllerAdvice) của Spring Boot để tạo ra một Global Exception Handler (Trạm xử lý lỗi tập trung)."

📌 Tổng kết phần 5: Exception (Bảng chốt phỏng vấn)

Chủ đề Ý chính Ghi nhớ nhanh
Exception là gì? Lỗi xảy ra khi chương trình chạy Runtime error
Checked Exception Bắt buộc xử lý (compile-time) Do yếu tố khách quan (IO, DB, network)
Unchecked Exception Không bắt buộc xử lý (runtime) Do code lỗi (NPE, logic sai)
try-catch Dùng để bắt lỗi Không lạm dụng, không nuốt lỗi
throw Chủ động ném lỗi Dùng trong business logic
throws Khai báo có thể có lỗi Đẩy trách nhiệm cho thằng gọi
@Transactional Quản lý transaction DB Mặc định chỉ rollback RuntimeException
⚠️ Bẫy nguy hiểm Checked Exception KHÔNG rollback Dễ gây mất dữ liệu
Cách fix @Transactional(rollbackFor = Exception.class) Rollback mọi loại lỗi
Tư duy thực tế Service → THROW, không catch Tránh code rác
Xử lý chuẩn Dùng Global Exception Handler @RestControllerAdvice
Best Practice Không catch lung tung Log + xử lý tập trung

6. Kỷ nguyên Java 8+ (Đừng code như "người tối cổ")

Đi phỏng vấn mà bạn vẫn dùng vòng lặp for lồng nhau lằng nhằng, hay check if (obj != null) thủ công thì rất dễ bị đánh trượt ngay vòng review code.

Java 8 mang đến một cuộc cách mạng về cách viết code ngắn gọn, sạch sẽ (Clean Code). Bạn bắt buộc phải dắt túi bộ 3 vũ khí sau:


6.1 Lambda Expression -> (Biết nói ngắn gọn)

Bản chất:
Giúp biến một hàm (function) thành một tham số để truyền đi truyền lại.
Thay vì phải tạo cả một class vô danh (Anonymous Class) dài dòng, bạn chỉ cần dùng mũi tên ->.

Code ngày xưa (Dài dòng, rối mắt):

// Tạo một luồng (Thread) mới
Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Code chạy lằng nhằng quá!");
    }
});

Java 8 (Sạch sẽ, quý tộc):

Thread t = new Thread(() -> System.out.println("Gọn gàng như một câu thơ!"));

6.2 Stream API (Dây chuyền nhà máy)

Đây là thứ bạn sẽ dùng MỖI NGÀY khi làm Backend.

Thay vì dùng vòng lặp for và các câu lệnh if để nhặt từng phần tử trong một List,
Stream API biến cái List đó thành một "dây chuyền sản xuất" chảy liên tục qua các màng lọc.

Ví dụ thực chiến:

Lọc ra danh sách TÊN của những khách hàng trên 18 tuổi.

Code kiểu cũ (Làm khổ bản thân):

List<String> tenKhachLon = new ArrayList<>();
for (KhachHang kh : khachHangs) {
    if (kh.getAge() > 18) {
        tenKhachLon.add(kh.getName());
    }
}

Dùng Stream API (Đẳng cấp):

List<String> tenKhachLon = khachHangs.stream()
    .filter(kh -> kh.getAge() > 18)  // Bước 1: Màng lọc tuổi
    .map(KhachHang::getName)         // Bước 2: Biến đổi (Chỉ lấy cái Tên)
    .collect(Collectors.toList());   // Bước 3: Đóng gói lại thành List mới

👉 Mẹo phỏng vấn:
Khi nhà tuyển dụng đưa một bài toán xử lý Mảng/List, hãy cố gắng dùng Stream (filter, map, sorted) để giải.
Họ sẽ đánh giá tư duy code của bạn rất cao!


6.3 Optional (Kẻ hủy diệt NullPointerException)

Lỗi ám ảnh nhất của mọi Dev Java chính là NullPointerException (NPE).

Ngày xưa, để tránh lỗi này, code của chúng ta ngập tràn những dòng if (user != null).
Java 8 đẻ ra Optional giống như một cái "hộp bọc hàng" an toàn.

Ví dụ thực tế trong Spring Boot:

Khi móc dữ liệu từ Database lên, lỡ User đó không tồn tại thì sao?

Nếu không dùng Optional:

User user = userRepository.findById(id);
if (user == null) {
    throw new Exception("Không tìm thấy!");
}
System.out.println(user.getName()); // Nhỡ quên check null là sập server ở đây

Dùng Optional (Thường thấy trong Spring Data JPA):

User user = userRepository.findById(id)
    .orElseThrow(() -> new RuntimeException("Không tìm thấy user!"));

System.out.println(user.getName()); // An toàn 100%

📌 Bảng tổng kết "vũ khí" Java 8+

Tính năng Giải quyết nỗi đau gì? Keyword "ăn tiền" khi phỏng vấn
Lambda -> Xóa bỏ các class vô danh (Anonymous) dài dòng Functional Interface, Arrow function
Stream API Thay thế vòng lặp for lằng nhằng khi thao tác Collection filter, map, collect, Declarative programming
Optional Chấm dứt thảm họa NullPointerException Wrapper class, orElse, orElseThrow, null-safety

7. Đa luồng (Multithreading) - Bài toán giành giật tài nguyên

Nhiều bạn có suy nghĩ: "Code Spring Boot framework lo hết rồi, quan tâm đa luồng (Thread) làm gì?" Nhưng thực tế, mỗi khi có một request từ người dùng gửi lên, Spring Boot sẽ tạo ra một luồng riêng để xử lý. Nếu web bạn có 1000 người truy cập cùng lúc -> Có 1000 luồng đang chạy song song!

Và đây là lúc nhà tuyển dụng tung ra câu hỏi "Tử thần".

7.1 Tình huống phỏng vấn kinh điển (Race Condition)

"Giả sử trong kho chỉ còn đúng 1 cái iPhone. Có 2 khách hàng A và B cùng bấm nút Mua Hàng vào đúng 10:00:00.000 (cùng 1 mili-giây). Theo em, hệ thống của em sẽ chạy như thế nào? Có bị bán âm kho không?"

Phân tích lỗi (Bán lố hàng - Over-selling):

  1. Luồng A đọc DB thấy: Số lượng = 1
  2. Luồng B cũng đọc DB thấy: Số lượng = 1
  3. Luồng A trừ kho: 1 - 1 = 0 (Lưu xuống DB).
  4. Luồng B trừ kho: 1 - 1 = 0 (Lưu đè xuống DB). 👉 Kết quả: Hệ thống báo 2 người đều mua thành công, nhưng kho chỉ có 1 cái. Lỗi nghiệp vụ cực kỳ nghiêm trọng!

7.2 Các cách giải quyết (Ghi điểm System Design)

Nhà tuyển dụng sẽ hỏi: "Vậy em xử lý tình huống trên như thế nào?"

❌ Cách 1: Dùng từ khóa synchronized (Trình độ Fresher mầm non)

Bạn bọc cái hàm mua hàng bằng từ khóa synchronized. Khi luồng A đang chạy, luồng B phải đứng ngoài cửa đợi.

  • Tại sao không nên dùng?
    1. Nó làm hệ thống bị thắt cổ chai, chạy cực kỳ chậm.
    2. vô dụng nếu công ty chạy 2 con Server song song (vì synchronized chỉ khóa được luồng trên 1 máy JVM).

✅ Cách 2: Dùng Lock ở tầng Database (Trình độ Thực chiến)

Đây là câu trả lời mà các Tech Lead muốn nghe. Thay vì khóa ở code Java, ta khóa ở ngay dòng dữ liệu dưới Database. Có 2 loại:

1. Optimistic Lock (Khóa lạc quan)

  • Cách làm: Thêm 1 cột version vào bảng Product. Khi update, phải kiểm tra version.
    • Luồng A update xong, version tăng lên 2.
    • Luồng B lúc sau update, mang version = 1 xuống DB đối chiếu -> Thấy sai lệch -> Bắn ra lỗi OptimisticLockException. Báo cho khách hàng B là "Sản phẩm đã thay đổi, vui lòng thử lại!".
  • Khi nào dùng: Thích hợp khi hệ thống ĐỌC nhiều, ít khi xảy ra tranh chấp. (Trong Spring Data JPA chỉ cần gắn annotation @Version là xong).

2. Pessimistic Lock (Khóa bi quan)

  • Cách làm: Dùng câu lệnh SQL SELECT ... FOR UPDATE. Dòng dữ liệu đó dưới DB sẽ bị khóa cứng. Luồng A đang select thì luồng B bắt buộc phải đứng chờ cho đến khi luồng A COMMIT xong giao dịch mới được đọc.
  • Khi nào dùng: Dùng cho các giao dịch liên quan đến Tiền bạc, Ví điện tử, Trừ tiền tài khoản (Vì tuyệt đối không được sai sót).

👉 Câu chốt ăn tiền: "Dạ thưa anh, để giải quyết bài toán tranh chấp tài nguyên, em sẽ không dùng synchronized ở tầng code vì không scale lên nhiều server được. Em sẽ dùng Pessimistic Lock nếu nghiệp vụ đó liên quan đến tiền bạc, hoặc Optimistic Lock nếu em muốn hệ thống chịu tải nhanh hơn và chấp nhận văng lỗi cho user thử lại ạ."


🎯 LỜI KẾT

Phù!!! Vậy là chúng ta đã quét qua một vòng những "vùng tối" kinh điển nhất của Java Core mà các nhà tuyển dụng cực kỳ ưa thích.

Code chạy được là một chuyện, hiểu sâu nó chạy dưới RAM thế nào, tại sao lại dùng cấu trúc dữ liệu này mà không dùng cái kia mới là thứ phân biệt giữa một "thợ gõ code" và một "Kỹ sư phần mềm" (Software Engineer).

Chúc anh em trang bị đủ đạn dược, tự tin "chém gió" và sớm chốt được tấm vé Offer ưng ý nhé! 😉 Nếu thấy series này hữu ích, đừng quên để lại 1 Upvote và Comment để Hoàn có động lực ra tiếp các bài viết sau nha!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.