Tiếp nối Phần 1 với các câu hỏi phỏng vấn OOP cơ bản, bài viết này sẽ tiếp tục mang đến cho bạn 25 câu hỏi phỏng vấn OOP nâng cao, được tuyển chọn kỹ lưỡng để đánh giá khả năng tư duy và kiến thức chuyên sâu về lập trình hướng đối tượng. Với những câu hỏi này, bạn sẽ không chỉ củng cố kiến thức nền tảng mà còn rèn luyện kỹ năng giải quyết vấn đề thực tế trong môi trường làm việc chuyên nghiệp.

Đọc bài viết để biết thêm các thông tin:

  • Các câu hỏi phỏng vấn OOP nâng cao
  • Các câu hỏi phỏng vấn OOP thực hành

Nếu bạn đang chuẩn bị cho vị trí Fresher/ Junior Developer hoặc cần “ôn tập” các kiến thức OOP cơ bản, bạn có thể tham khảo Phần 1 của Các câu hỏi phỏng vấn OOP.

Các câu hỏi phỏng vấn OOP nâng cao

Những hạn chế của OOP là gì?

Mặc dù sở hữu nhiều ưu điểm tuyệt vời, thế nhưng OOP vẫn có một số hạn chế như:

  • Không lý tưởng cho các vấn đề nhỏ: OOP có thể quá phức tạp đối với các dự án quy mô nhỏ, dẫn đến chi phí không cần thiết, nên sử dụng các cấu trúc dữ liệu cơ bản và các hàm đơn giản để giải quyết hiệu quả hơn.
  • Yêu cầu thử nghiệm mở rộng: Mã OOP thường yêu cầu thử nghiệm kỹ lưỡng để đảm bảo chức năng phù hợp do tính phức tạp của nó.
  • Tốn thời gian: Việc phát triển các giải pháp OOP có thể mất nhiều thời gian hơn do cần phải lập kế hoạch và thiết kế cấu trúc đối tượng.
  • Cần lập kế hoạch phù hợp: Sử dụng OOP hiệu quả đòi hỏi phải lập kế hoạch cẩn thận ngay từ đầu để xác định các class, mối quan hệ và tương tác.
  • Thay đổi tư duy: Các lập trình viên phải áp dụng tư duy tập trung vào giải quyết vấn đề hướng đối tượng, điều này đòi hỏi quá trình học tập và thay đổi tư duy.

Bộ Access specifiers (chỉ định truy cập) là gì?

Các chỉ định truy cập là một loại từ khóa đặc biệt, được sử dụng để kiểm soát hoặc chỉ định khả năng truy cập của các thực thể như class, method… Các chỉ định truy cập này cũng đóng vai trò rất quan trọng trong việc đạt được tính đóng gói – một trong những tính năng chính của OOP. Một số chỉ định truy cập hoặc bộ điều chỉnh truy cập bao gồm:

  • Public: Cho phép các thành phần của class truy cập không hạn chế từ bất kỳ đâu trong chương trình.
  • Private: Tùy chọn này hạn chế quyền truy cập vào các thành phần chỉ trong class nơi chúng được khai báo, ngăn chặn quyền truy cập từ bên ngoài.
  • Protected: Cho phép truy cập vào các thành phần trong trong class nơi chúng được khai báo và trong các subclass của nó, nhưng không phải từ bên ngoài.

Interface là gì?

Interface (Giao diện) là kiểu dữ liệu do người dùng định nghĩa và là tập hợp các phương thức trừu tượng (abstract method).

Một class triển khai một hoặc nhiều interface, do đó kế thừa các phương thức trừu tượng của interface. Một class mô tả các thuộc tính và hành vi của object,  thông thường, interface không chứa code triển khai cho các phương thức (ngoại trừ phương thức mặc định trong một số ngôn ngữ hiện đại). Class thể hiện “how” và interface biểu thị “what”.

Abstract class và interface khác nhau như thế nào?

Cả abstract class và interface đều là các loại class đặc biệt chỉ bao gồm khai báo các phương thức, không phải triển khai chúng. Tuy nhiên, abstract class hoàn toàn khác biệt với interface. Sau đây là một số điểm khác biệt chính:

Abstract class Interface
Một Abstract class có thể có cả phương thức trừu tượng và không trừu tượng. Một Interface chỉ có thể có các phương thức trừu tượng.
Một Abstract class có thể có các biến final, non-final, static và non-static. Interface chỉ có các biến static và final.
Abstract class không hỗ trợ đa kế thừa Interface hỗ trợ đa kế thừa.

Sự khác biệt giữa structure và class

Class Structure
Một class giống như một bản thiết kế để tạo ra các đối tượng trong lập trình hướng đối tượng (OOP). Nó kết hợp dữ liệu và hành vi thành một đơn vị duy nhất. Các đối tượng được tạo từ một class là các thể hiện của class đó, mỗi thể hiện có tệp dữ liệu riêng. Các class rất cần thiết để mô hình hóa các khái niệm trong thế giới thực và triển khai các tính năng OOP như kế thừa và đa hình. Structure là cách nhóm các biến có kiểu khác nhau dưới một tên duy nhất. Nó thường được sử dụng trong các ngôn ngữ như C để xác định kiểu dữ liệu tùy chỉnh chứa nhiều phần tử. Không giống như các class, structure không hỗ trợ method hoặc kế thừa. Chúng chủ yếu được sử dụng để sắp xếp dữ liệu liên quan để quản lý và thao tác dễ dàng hơn trong một chương trình.

Xử lý ngoại lệ (exception handling) có nghĩa là gì?

Xử lý ngoại lệ trong lập trình hướng đối tượng là một khái niệm cơ bản được sử dụng để quản lý lỗi và các tình huống ngoại lệ có thể phát sinh trong quá trình thực thi chương trình. Nó sử dụng các từ khóa try, catch và throw để phát hiện, xử lý và phục hồi lỗi một cách nhẹ nhàng, ngăn ngừa sự cố chương trình và đảm bảo tính mạnh mẽ. Từ khóa try được sử dụng để bao bọc một khối mã có thể gây ra ngoại lệ. Từ khóa catch được sử dụng để bắt và xử lý các ngoại lệ cụ thể. Từ khóa throw được sử dụng để tự động ném một ngoại lệ.

Khi xảy ra lỗi, một đối tượng ngoại lệ (exception object) sẽ được ném ra, và luồng chương trình sẽ được chuyển hướng đến khối catch gần nhất phù hợp với kiểu ngoại lệ đã ném ra, cho phép xử lý lỗi và chiến lược phục hồi thích hợp. Nếu tìm thấy, khối catch sẽ được thực thi và chương trình tiếp tục chạy. Nếu không tìm thấy khối catch phù hợp, chương trình sẽ bị dừng đột ngột.

Garbage Collection (GC) là gì?

Lập trình hướng đối tượng xoay quanh các thực thể như đối tượng. Mỗi đối tượng sử dụng bộ nhớ và có thể có nhiều đối tượng của một class. Vì vậy, nếu các đối tượng này và bộ nhớ của chúng không được xử lý đúng cách, có thể dẫn đến một số lỗi liên quan đến bộ nhớ và hệ thống có thể bị lỗi.

Garbage Collection (quá trình thu gom rác) là cơ chế xử lý bộ nhớ trong chương trình. Thông qua GC, bộ nhớ được giải phóng bằng cách xóa các đối tượng không còn cần thiết.

Có thể tải Constructor trong 1 class không?

Có, chúng ta có thể quá tải Constructor trong một class trong Java. Constructor Overloading được thực hiện khi chúng ta muốn Constructor có Constructor khác nhau với tham số khác nhau (Number và Type).

Có thể tải Destructor trong 1 class không?

Câu trả lời là không, bởi một Destructor không thể được nạp chồng trong một class mà chỉ có thể có một Destructor trong một class.

Có thể chạy ứng dụng Java mà không cần triển khai khái niệm OOP không?

Có. Các ứng dụng Java dựa trên mô hình lập trình hướng đối tượng hoặc khái niệm OOP, do đó chúng nên được triển khai với OOP. Tuy nhiên chúng vẫn có thể triển khai theo hướng thủ tục giống như , C++ có thể được triển khai mà không cần OOP vì nó cũng hỗ trợ mô hình lập trình cấu trúc giống C.

Compile time Polymorphism là gì và có gì khác Runtime Polymorphism?

Khi một lệnh gọi đa hình được thực hiện và trình biên dịch biết hàm nào sẽ được gọi; điều này được gọi là đa hình thời gian biên dịch (Compile time Polymorphism hay còn gọi là Static Polymorphism).

Các tính năng như đối số mặc định của hàm, overloading và template trong C++ hỗ trợ Compile time Polymorphism. Compile time Polymorphism đề cập đến loại Đa hình xảy ra tại thời điểm biên dịch, có nghĩa là trình biên dịch quyết định trình biên dịch quyết định phiên bản hàm nào sẽ được gọi dựa trên kiểu dữ liệu và số lượng đối số truyền vào.

Ví dụ:

class CompileTimePolymorphism{

   // Method thứ 1 với name add
   public int add(int x, int y){ 
   return x+y;
   }

   // Method thứ 2 với name add
   public int add(int x, int y, int z){
   return x+y+z;
   }

   // Method thứ 3 với name add
   public int add(double x, int y){ 
   return (int)x+y;
   }

   // Method thứ 4 với name add
   public int add(int x, double y){ 
   return x+(int)y;
   }

}

class Test{

   public static void main(String[] args){

   CompileTimePolymorphism demo=new CompileTimePolymorphism();

   // Trong statement sau đây, Compiler nhìn vào kiểu argument và quyết định gọi method 1

   System.out.println(demo.add(2,3));

   // Tương tự, trong statement sau đây, Compiler gọi method 2

   System.out.println(demo.add(2,3,4));

   // Tương tự, trong statement sau đây, Compiler gọi method 4

   System.out.println(demo.add(2,3.4));

   // Tương tự, trong statement sau đây, Compiler gọi method 3

   System.out.println(demo.add(2.5,3)); 

   }

}

Trong ví dụ trên, có 4 phiên bản của phương thức add. Phương thức đầu tiên lấy hai tham số trong khi phương thức thứ hai lấy ba tham số. Đối với phương thức thứ ba và thứ tư, có sự thay đổi thứ tự của các tham số. Trình biên dịch xem xét phương thức signature và quyết định phương thức nào sẽ gọi cho một lệnh gọi phương thức cụ thể tại thời điểm biên dịch.

Trong khi đó, Runtime Polymorphism (đa hình thời gian chạy), còn được gọi là Dynamic Polymorphism, đề cập đến loại Đa hình xảy ra tại thời điểm chạy. Điều đó có nghĩa là trình biên dịch không thể quyết định được. Do đó, hình dạng hoặc giá trị nào được thực hiện tùy thuộc vào quá trình thực thi. 

Ví dụ:

class AnyVehicle{

   public void move(){

   System.out.println(“Any vehicle should move!”);

   }

}

class Bike extends AnyVehicle{

   public void move(){

   System.out.println(“Bike can move too!”);

   }

}

class Test{

   public static void main(String[] args){

   AnyVehicle vehicle = new Bike();

   // Trong statement trên, như bạn thấy, object vehicle thuộc type AnyVehicle

   // Nhưng output của của statement sau sẽ là "Bike can move too!" 

   // bởi vì triển khai thực tế của object 'vehicle' được quyết định khi diễn ra runtime vehicle.move();

   vehicle = new AnyVehicle();

   // Bây giờ, output của statement sau sẽ là “Any vehicle should move!”, 

   vehicle.move();

   }

}

Có phải luôn cần tạo objects từ class không?

Không. Nếu base class bao gồm các non-static method, thì phải xây dựng một object. Nhưng không cần tạo object nào nếu class bao gồm các static method. Trong trường hợp này, bạn có thể sử dụng tên class để gọi trực tiếp các static method đó.

Một class chiếm bao nhiêu bộ nhớ?

Các class không sử dụng bộ nhớ. Chúng chỉ đóng vai trò là một template mà từ đó các item được tạo ra. Bây giờ, các đối tượng chính thức khởi tạo các thành phần và method của class khi chúng được tạo ra, sử dụng bộ nhớ trong quá trình này.

C++ hỗ trợ Polymorphism như thế nào?

C++ là ngôn ngữ lập trình hướng đối tượng và nó cũng hỗ trợ Đa hình (Polymorphism): 

  • Compile Time Polymorphism: C++ hỗ trợ đa hình thời gian biên dịch với sự trợ giúp của các tính năng như template, overloading và đối số mặc định. 
  • Runtime Polymorphism: C++ hỗ trợ đa hình thời gian chạy với sự trợ giúp của các tính năng như hàm ảo. Hàm ảo có hình dạng của hàm dựa trên loại đối tượng tham chiếu và được giải quyết tại thời gian chạy.

Có bao nhiêu loại constructor trong C++?

Default constructor: Default constructor là hàm tạo không sử dụng bất kỳ đối số nào. Nó không có tham số.

class ABC
{
   int x;

   ABC()
   {
    x = 0;
   }
}

Parameterized constructor: Các hàm tạo có sử dụng một số đối số được gọi là Parameterized constructor.

class ABC
{
   int x;
 
   ABC(int y)
   {
    x = y;
   }
}

Copy Constructor: Copy constructor là hàm thành phần khởi tạo một đối tượng bằng cách sử dụng một đối tượng khác cùng class.

class ABC
{
   int x;

   ABC(int y)
   {
    x = y;
   }
   // Copy constructor
   ABC(ABC abc)
   {
    x = abc.x;
   }
}

Destructor là gì?

Destructor là một phương thức duy nhất trong lập trình hướng đối tượng được gọi tự động khi một đối tượng bị hủy hoặc nằm ngoài phạm vi. Mục đích chính của nó là giải phóng bất kỳ tài nguyên hoặc bộ nhớ nào được phân bổ bởi đối tượng trong suốt vòng đời của nó, chẳng hạn như đóng tệp, giải phóng kết nối database hoặc giải phóng dynamic memory. Điều này giúp quản lý bộ nhớ và dọn dẹp tài nguyên hợp lý trong chương trình.

Copy constructor là gì?

Bằng cách sao chép các thành phần của một đối tượng hiện có, Copy constructor sẽ khởi tạo các thành phần của một đối tượng mới được tạo. Đối số cho Copy constructor là tham chiếu đến một đối tượng cùng class. Người lập trình có tùy chọn định nghĩa trực tiếp Copy constructor. Trình biên dịch sẽ định nghĩa Copy constructor nếu người lập trình không định nghĩa.

Cohesion trong OOP là gì?

Cohesion trong OOP đề cập đến mức độ mà các thành phần trong một mô-đun (như một class) có liên quan và phục vụ một mục đích chung. Cohesion cao hơn đồng nghĩa các thành phần trong mô-đun có liên quan chặt chẽ và tập trung vào một nhiệm vụ hoặc chức năng cụ thể. Ưu điểm của Cohesion cao bao gồm khả năng bảo trì và tái sử dụng mã được cải thiện vì các mô-đun có chức năng cụ thể và được xác định rõ ràng sẽ dễ hiểu, dễ sửa đổi và tái sử dụng hơn ở các phần khác nhau của chương trình hoặc trong các chương trình khác.

Có những loại biến (variable) nào trong OOP?

  • Primitive Variable: Được sử dụng để biểu diễn các giá trị nguyên thủy như int, float…
  • Reference Variable: Được sử dụng để tham chiếu đến các đối tượng trong Java.
  • Instance Variable: Các biến có giá trị thay đổi từ đối tượng này sang đối tượng khác là biến thực thể (Instance Variable). Đối với mỗi đối tượng, một bản sao riêng biệt của biến thực thể được tạo ra. Biến thực thể được khai báo trong class và bên ngoài bất kỳ method/block/constructor nào.
  • Static Variable: Đối với biến tĩnh (Static Variable), một bản sao duy nhất của biến được tạo và bản sao đó được chia sẻ giữa mọi đối tượng class. Biến tĩnh được tạo trong quá trình tải class và bị hủy khi hủy class. Biến tĩnh có thể được truy cập trực tiếp từ vùng tĩnh và vùng thực thể. Chúng ta không cần phải thực hiện khởi tạo rõ ràng cho các biến tĩnh và JVM sẽ cung cấp các giá trị mặc định.
  • Local Variable: Biến được khai báo bên trong method hoặc clock hoặc constructor là biến cục bộ (Local Variable). Do đó, phạm vi của biến cục bộ giống với phạm vi của block mà chúng ta khai báo biến đó.

Các mức độ của data abstraction (trừu tượng hóa dữ liệu)

Các mức trừu tượng hóa dữ liệu đề cập đến các class trừu tượng khác nhau trong một hệ thống phần mềm, từ các chi tiết triển khai cấp thấp đến các mô hình khái niệm cấp cao. Sự trừu tượng hóa đạt được thông qua các class trừu tượng, interface và encapsulation, ẩn đi sự phức tạp và cung cấp các giao diện đơn giản hóa để tương tác với dữ liệu và chức năng. Các mức trừu tượng hóa này giúp quản lý sự phức tạp, cải thiện khả năng bảo trì và tạo điều kiện cho thiết kế mô-đun trong phát triển phần mềm

Coupling trong OOP là gì và tại sao nó lại hữu ích?

Tight Coupling – Nếu sự phụ thuộc giữa các thành phần cao, các thành phần này được gọi là Tight Coupling.

Ví dụ: 3 Class dưới đây phụ thuộc chặt chẽ vào nhau nên chúng có mối liên kết chặt chẽ.

class P
{
static int a = Q.j;
}

class Q
{
static int j = R.method();
}

class R
{
public static int method(){
return 3;
}

Loose Coupling – Nếu sự phụ thuộc giữa các thành phần thấp, nó được gọi là Loose Coupling. Loại Coupling này cũng được ưa chuộng sở dĩ do:

  • Làm tăng khả năng bảo trì của mã;
  • Cung cấp khả năng tái sử dụng mã.

Các câu hỏi phỏng vấn OOP thực hành

Output của đoạn mã dưới đây là gì?

class Scaler
{
   static int i;

   static
   {
       System.out.println(“a”);

       i = 100;
   }
}

public class StaticBlock
{
   static
   {
       System.out.println(“b”);
   }

   public static void main(String[] args)
   {
       System.out.println(“c”);

       System.out.println(Scaler.i);
   }
}

Output:

b

c

a

100

Output của đoạn mã dưới đây là gì?

#include<iostream> 

using namespace std; 
class BaseClass1 { 
public: 
    BaseClass1() 
    { cout << " BaseClass1 constructor called" << endl;  } 
}; 

class BaseClass2 { 
public: 
    BaseClass2() 
    { cout << "BaseClass2 constructor called" << endl;  } 
};

class DerivedClass: public BaseClass1, public BaseClass2 { 
  public: 
   DerivedClass() 
    {  cout << "DerivedClass constructor called" << endl;  } 
}; 

int main() 
{ 
  DerivedClass derived_class; 
  return 0; 
}

Output:

BaseClass1 constructor called 

BaseClass2 constructor called 

DerivedClass constructor called

Giải thích: Chương trình trên minh họa cho Đa kế thừa. Vì vậy, khi constructor của Derived class được gọi, nó sẽ tự động gọi các constructor của Base class theo thứ tự kế thừa từ trái sang phải.

Dự đoán kết quả output cho đoạn mã sau:

#include<iostream> 
using namespace std; 

class ClassA {  
public: 
   ClassA(int ii = 0) : i(ii) {} 
   void show() { cout << "i = " << i << endl;} 
private: 
   int i; 
}; 

class ClassB { 
public: 
   ClassB(int xx) : x(xx) {} 
   operator ClassA() const { return ClassA(x); } 
private: 
   int x; 
}; 

void g(ClassA a) 
{  a.show(); } 

int main() { 
 ClassB b(10); 
 g(b); 
 g(20); 
 getchar(); 
 return 0; 
}  

Output:

i = 10 

i = 20

Giải thích: ClassA chứa một constructor chuyển đổi. Do đó, các đối tượng của ClassA có thể có các giá trị số nguyên. Vì vậy, câu lệnh g(20) hoạt động. Ngoài ra, ClassB có một toán tử chuyển đổi được overloading. Vì vậy, câu lệnh g(b) cũng hoạt động.

Output của đoạn mã dưới đây là gì?

public class Demo{ 
   public static void main(String[] arr){ 
         System.out.println(“Main1”);
   } 
   public static void main(String arr){  
         System.out.println(“Main2”);
   } 
}

Output: 

Main1

Giải thích: Ở đây phương thức main()bị quá tải. Nhưng JVM chỉ hiểu phương thức main có đối số String[]trong định nghĩa của nó. Do đó Main1 được in ra và phương thức main được overloading bị bỏ qua.

Tổng kết Câu hỏi phỏng vấn OOP (Phần 2)

Qua 50 câu hỏi phỏng vấn OOP ở Phần 1 và Phần 2, chúng ta đã cùng nhau khám phá toàn diện về lập trình hướng đối tượng. Để trở thành một lập trình viên OOP xuất sắc, không chỉ nắm vững kiến thức lý thuyết mà bạn còn phải rèn luyện kỹ năng giải quyết vấn đề, tư duy logic và khả năng làm việc nhóm.

Bài viết Câu hỏi phỏng vấn OOP này từ ITviec hy vọng sẽ là hành trang khởi đầu giúp bạn thành công trong các cuộc phỏng vấn và trở thành một lập trình viên OOP chuyên nghiệp.