Câu hỏi phỏng vấn Java OOP
Bộ câu hỏi này harvest từ khoá Java OO & Functional — mỗi câu có link bài học đào sâu cơ chế, không dừng ở đáp án thuộc lòng.
01
Abstract class khác interface ở điểm nào, và khi nào chọn mỗi bên?
JuniorInterface là hợp đồng năng lực ("can-do"): một lớp implements được nhiều interface, field chỉ là hằng
public static final, và từ Java 8 códefault method— thứ ra đời để thêm method vào interface đã publish mà không phá mọi class implement cũ (đó là cách JDK thêm đượcCollection.removeIf). Abstract class mô tả bản chất ("is-a"): giữ được state qua instance field, có constructor cho lớp con gọisuper(...), và gánh được Template Method — cha khoá khung thuật toán bằng methodfinal, con chỉ điền từng bước, nhưHttpServlethayInputStream. Vì Java đơn kế thừa class, chọn abstract class là tiêu mất khe kế thừa duy nhất của lớp con — nên Effective Java (Item 20) khuyên mặc định đi từ interface, chỉ nâng lên abstract class khi các implementation thật sự chia sẻ state và logic chung. Pattern chuẩn trong JDK là kết hợp cả hai:List(interface định nghĩa kiểu) +AbstractList(skeletal implementation gánh code chung) +ArrayList(concrete).Trả lời thuộc lòng bảng so sánh mà không biết default method từ Java 8 đã làm mờ ranh giới — và không biết hai interface có default method trùng chữ ký sẽ ép class phải tự override, chọn tường minh bằngA.super.method().02
Override khác overload thế nào? Java chọn method nào lúc runtime?
JuniorOverload là static binding: compiler chọn method ngay lúc biên dịch dựa trên kiểu khai báo và chữ ký tham số — không có chi phí runtime. Override là dynamic dispatch: compiler chỉ sinh lệnh
invokevirtual, còn JVM lúc chạy mới lần theo object trên heap, đọc class pointer để tìm vtable của kiểu thực, rồi gọi method ở đúng slot — vì vậyAnimal a = new Dog()gọia.speak()sẽ chạy bản củaDog. Ngoại lệ quan trọng:static,privatevàfinalmethod không tham gia dynamic dispatch (bind tĩnh bằnginvokestatic/invokespecial, nênfinalmethod được JIT inline thẳng) — và static method cùng tên ở lớp con chỉ là "hiding", gọi qua reference kiểu cha vẫn chạy bản của cha. Override hợp lệ còn phải giữ hợp đồng: không thu hẹp visibility, không ném checked exception rộng hơn của cha, và từ Java 5 được phép covariant return type.Bẫy kinh điển: gọi method non-final trong constructor của cha — dynamic dispatch chọn bản của con khi field của con còn chưa khởi tạo, in ranull; và nhầm rằng kiểu của biến quyết định method — với override, kiểu thực của object mới quyết định.03
Trừu tượng hoá (abstraction) trong OOP thực chất là gì — ngoài định nghĩa sách giáo khoa?
JuniorĐịnh nghĩa "giấu chi tiết implementation" chỉ mô tả kết quả; bản chất trừu tượng hoá là quá trình bỏ đi chi tiết không quan trọng theo ngữ cảnh và giữ lại điểm chung, để xử lý nhiều thứ khác nhau một cách đồng nhất. Vì phụ thuộc ngữ cảnh nên cùng một chiếc ô tô có nhiều abstraction đều đúng: trạm thu phí chỉ cần
type, weight, plateNumber; gara lại cầnengineCode, lastService— không có abstraction "đúng tuyệt đối", chỉ có đúng với use case. Trong 4 trụ cột OOP, ba cái còn lại là cơ chế có keyword riêng; abstraction là tư duy quyết định "mô hình hoá cái gì" — encapsulation chỉ là cơ chế che giấu theo abstraction đã chọn. Bài test nhanh: viết được code dùng abstraction mà không cần biết implementation cụ thể (kiểutotalBalance(List<Account>)); nếu caller phảiinstanceofrồi cast xuống class con để xử lý case riêng, abstraction đó đã sai.Trừu tượng hoá quá sớm cũng là lỗi: rule of 3 (Fowler) — chờ đủ 3 case thực tế hãy abstract, vì 2 case có thể chỉ là trùng hợp; interface chỉ có một implementation thường là dead weight chứ không phải thiết kế đẹp.04
Vì sao nói 'ưu tiên composition hơn inheritance'? Khi nào kế thừa vẫn hợp lý?
MidKế thừa khiến lớp con phụ thuộc cả vào chi tiết cài đặt của cha — ví dụ kinh điển:
InstrumentedHashSet extends HashSetoverride cảadd()lẫnaddAll()để đếm phần tử, nhưngaddAll3 phần tử lại đếm ra 6, vìHashSet.addAll()bên trong tự gọiadd()và dynamic dispatch trỏ ngược về bản override của lớp con. Đó là fragile base class problem: cha đổi cách cài đặt là con vỡ ngầm, không báo trước. Composition thay bằng giữ reference và delegate (private final Set<E> delegate) — đếm đúng, tách hẳn khỏi implementation của cha, và swap được lúc runtime: đổiHashSetsangTreeSetchỉ là đổi constructor argument. Kế thừa vẫn hợp lý khi đủ 3 điều kiện của Effective Java (Item 18): quan hệ is-a thoả Liskov thật sự, lớp cha được thiết kế và document chủ đích cho kế thừa (nhưAbstractList), và bạn kiểm soát được cả hai đầu.Is-a 'theo ngôn ngữ thường' chưa đủ: Square is-a Rectangle đúng toán học, nhưng Square phá invariant 'setWidth/setHeight độc lập' của Rectangle (vi phạm Liskov) — bài test đúng là 'caller đang dùng lớp cha có bị bất ngờ khi nhận lớp con không'.05
Sealed class giải quyết vấn đề gì mà abstract class thường không làm được?
MidTrước Java 17 chỉ có hai thái cực:
final(cấm hẳn kế thừa) hoặc mở cho tất cả — sealed thêm khoảng giữa:permitskhai báo đúng tập lớp con hợp lệ, và mỗi lớp con buộc phải tự chọn tiếpfinal,sealed, hoặcnon-sealed(mở lại). Giá trị lớn nhất nằm ở compile-time: vì compiler biết tập lớp con là đóng,switchpattern matching được kiểm tra exhaustive không cầndefault— thêm một lớp con mới vàopermitslà mọi switch chưa phủ case lập tức báo lỗi biên dịch, thay vì thành bug runtime như chuỗiif instanceofbị quên nhánh. Kết hợpsealed + record + switchcho Java một algebraic data type đúng nghĩa — kiểuResult<T> permits Success, Failurebuộc caller xử lý cả hai nhánh mà không cần null hay exception cho flow bình thường.Hai câu hỏi ngược hay gặp: (1) sealed có giúp JVM nhanh hơn không — không đáng kể, HotSpot đã devirtualize bằng class hierarchy analysis và inline cache từ trước khi sealed ra đời, giá trị của sealed nằm ở compile-time; (2) chỉ cần một nhánhnon-sealedlà switch phải gom nó bằng một case tổng quát, exhaustive không còn tuyệt đối.
Topic kế cùng track: Câu hỏi phỏng vấn Java Collections →
Trả lời trôi chảy bắt đầu từ hiểu cơ chế
Mỗi câu ở trên đều có bài học đứng sau. Học tuần tự cả khoá Java OO & Functional để không chỉ trả lời được, mà giải thích được vì sao.