Lập trình hướng đối tượng Java: Giải thích chi tiết kèm ví dụ

Java được xem là ngôn ngữ đại diện tiêu biểu nhất cho OOP vì mọi thành phần trong Java đều được xây dựng dựa trên các lớp và đối tượng. Việc áp dụng lập trình hướng đối tượng trong Java giúp bạn tối ưu hóa khả năng tái sử dụng code, khiến hệ thống trở nên linh hoạt, dễ dàng mở rộng mà không làm phá vỡ cấu trúc sẵn có.

Đọc bài viết này để hiểu rõ về:

  • Bản chất lập trình hướng đối tượng (OOP) là gì? 
  • Class và Object trong Java là gì?
  • Vì sao OOP quan trọng trong Java, đặc biệt trong các dự án lớn?
  • 4 tính chất trụ cột của OOP trong Java: Đóng gói, Kế thừa, Đa hình, Trừu tượng 
  • Các câu hỏi thường gặp về lập trình hướng đối tượng Java

Tổng quan về lập trình hướng đối tượng trong Java

Định nghĩa OOP trong Java

Lập trình hướng đối tượng (OOP) là một phương pháp luận lập trình dựa trên khái niệm về các đối tượng. 

Trong Java, OOP không chỉ là một tính năng mà là điểm cốt lõi của ngôn ngữ này. Thay vì tập trung vào các bước thực hiện lệnh, OOP tập trung vào việc xác định các đối tượng mà ta muốn thao tác (ai/cái gì) và cách chúng tương tác với nhau.

Ngoài Java, có rất nhiều ngôn ngữ lập trình phổ biến khác cũng áp dụng mạnh mẽ mô hình Lập trình hướng đối tượng (OOP). Dưới đây là một vài ví dụ tiêu biểu:

  • Python: Là một ngôn ngữ đa mô hình, nhưng OOP là một trong những phong cách lập trình chính của nó. Python được ưa chuộng vì cú pháp đơn giản, dễ đọc và tính linh hoạt cao.
  • C++: Là phần mở rộng của ngôn ngữ C và được xem là một trong những ngôn ngữ OOP mạnh mẽ nhất, được sử dụng rộng rãi trong phát triển game, hệ điều hành và các ứng dụng hiệu suất cao.
  • C# (C-Sharp): Được phát triển bởi Microsoft, là một ngôn ngữ OOP hiện đại, thường được dùng trong phát triển ứng dụng Windows, web (với .NET), và game (với Unity).
  • PHP: Ban đầu là một ngôn ngữ lập trình hướng thủ tục, nhưng các phiên bản hiện đại đã hỗ trợ đầy đủ các tính năng của OOP và được sử dụng rộng rãi trong phát triển web.
  • Ruby: Một ngôn ngữ có triết lý tập trung sâu sắc vào OOP, nổi tiếng với sự thanh lịch và được sử dụng nhiều trong phát triển web thông qua framework Ruby on Rails.
  • Smalltalk: Một trong những ngôn ngữ lập trình đầu tiên được tạo ra để trở thành một ngôn ngữ “thuần” hướng đối tượng.

Mọi thứ trong Java, từ các thư viện chuẩn đến các ứng dụng bạn xây dựng, đều được tổ chức dưới dạng các Lớp (Class) và Đối tượng (Object).

Đọc thêm: OOP là gì? 4 đặc tính cơ bản của OOP

Class và Object trong Java

Trong Java, mọi logic tính toán và xử lý dữ liệu đều xoay quanh mối quan hệ giữa Class và Object. Để dễ hiểu, hãy coi Class là bản thiết kế trên giấy và Object là sản phẩm thực tế được chế tạo từ bản thiết kế đó.

Class (Lớp)

Class là một kiểu dữ liệu do người dùng định nghĩa. Nó đóng vai trò là một cái khuôn hoặc bản thiết kế dùng để mô tả trạng thái và hành vi của một nhóm đối tượng cùng loại.

Một Class thông thường bao gồm:

  • Thuộc tính (Fields/Attributes): Các biến lưu trữ dữ liệu (trạng thái).
  • Phương thức (Methods): Các hàm định nghĩa hành động mà đối tượng có thể thực hiện (hành vi).
  • Hàm khởi tạo (Constructor): Phương thức đặc biệt dùng để khởi tạo đối tượng.

Object (Đối tượng)

Object là một thực thể cụ thể được tạo ra từ Class. Khi một đối tượng được khởi tạo, bộ nhớ sẽ được cấp phát để lưu trữ các giá trị cụ thể cho các thuộc tính mà Class đã định nghĩa.

Mỗi đối tượng có ba đặc điểm chính:

  1. Trạng thái (State): Được thể hiện qua giá trị của các thuộc tính.
  2. Hành vi (Behavior): Được thể hiện qua việc gọi các phương thức.
  3. Danh tính (Identity): Mỗi đối tượng có một địa chỉ bộ nhớ riêng biệt, ngay cả khi chúng có cùng giá trị thuộc tính.

Ví dụ minh họa thực tế:

Giả sử ta cần quản lý các loại xe hơi trong phần mềm:

  • Class Car: Sẽ định nghĩa các đặc điểm chung như brand (thương hiệu), color (màu sắc), maxSpeed (tốc độ tối đa) và các hành động như start() (khởi động), brake() (phanh).
  • Object toyota: Là một thực thể có brand = “Toyota”, color = “Red”.
  • Object tesla: Là một thực thể có brand = “Tesla”, color = “White”.

Cách triển khai trong Java:

// Định nghĩa lớp (Class)
class Car {
    String brand;
    
    void drive() {
        System.out.println(brand + " is driving...");
    }
}

public class Main {
    public static void main(String[] args) {
        // Khởi tạo đối tượng (Object)
        Car myCar = new Car(); 
        myCar.brand = "Toyota";
        myCar.drive(); // Kết quả: Toyota is driving...
    }
}

Tại sao nên sử dụng OOP trong Java?

Trong lập trình hướng thủ tục, khi dự án phình to, mã nguồn thường trở thành một “nồi lẩu thập cẩm” với các hàm đan xen chằng chịt. Một thay đổi nhỏ ở hàm này có thể gây lỗi dây chuyền ở nơi khác.

Việc áp dụng mô hình hướng đối tượng giúp quá trình phát triển phần mềm trở nên khoa học và hiệu quả hơn bằng cách:

Phân tách module (Modularity)

Chia dự án thành các đối tượng độc lập. Khi có lỗi, bạn chỉ cần kiểm tra và sửa đổi bên trong đối tượng đó mà không làm ảnh hưởng đến toàn bộ hệ thống.

Khả năng mở rộng (Scalability)

Với các dự án lớn cần thêm tính năng mới, bạn chỉ việc tạo ra các đối tượng mới hoặc mở rộng các đối tượng cũ thay vì phải viết lại cấu trúc chương trình.

Tái sử dụng mã nguồn

OOP giúp tối ưu hóa cấu trúc code thông qua khả năng tái sử dụng. Nhờ tính kế thừa, các lớp con có thể sử dụng lại các thuộc tính và phương thức từ lớp cha, giúp loại bỏ việc viết lại các đoạn mã có chức năng tương tự. Khi cần thay đổi một logic chung, bạn chỉ cần cập nhật tại lớp gốc, mọi thành phần kế thừa sẽ tự động được cập nhật. 

Điều này giúp team Dev phối hợp làm việc trên cùng một dự án lớn mà không bị chồng chéo mã nguồn, giảm thiểu rủi ro sai sót khi dự án mở rộng.

Tăng cường tính bảo mật

Tính đóng gói trong Java cung cấp cơ chế bảo vệ dữ liệu nghiêm ngặt:

  • Che giấu dữ liệu: Việc sử dụng các từ khóa truy cập (access modifiers) như private giúp ngăn chặn các tác động trực tiếp từ bên ngoài vào trạng thái nội bộ của đối tượng.
  • Giao diện lập trình (API) nội bộ: Người dùng chỉ có thể tương tác với đối tượng qua các phương thức được định nghĩa sẵn (Getter/Setter), đảm bảo dữ liệu luôn được kiểm tra tính hợp lệ trước khi thay đổi.
  • Mô phỏng thực tế để tối ưu tư duy: 

Thay vì xử lý các luồng logic phức tạp và rời rạc, OOP cho phép lập trình viên tổ chức mã nguồn theo các thực thể thực tế. Các đối tượng trong code như KhachHang, DonHang, hay SanPham phản ánh chính xác các thực thể trong dự án. 

Mỗi đối tượng tự quản lý logic và dữ liệu riêng của nó. Cách phân chia này giúp các nhóm lập trình viên có thể làm việc độc lập trên từng module mà không gây ảnh hưởng lẫn nhau, đặc biệt quan trọng trong các dự án lớn.

4 tính chất trụ cột của OOP trong Java

Sức mạnh lập trình hướng đối tượng Java nằm ở 4 đặc tính cốt lõi, giúp lập trình viên xây dựng những hệ thống phần mềm chuyên nghiệp và linh hoạt.

Tính Đóng gói (Encapsulation)

Tính đóng gói là việc che giấu các chi tiết thực hiện bên trong của một đối tượng và chỉ để lộ ra một giao diện giao tiếp cần thiết. Hãy tưởng tượng nó như một viên thuốc: bạn biết công dụng của nó nhưng không cần biết các thành phần hóa học bên trong được nén lại như thế nào.

Lợi ích cốt lõi:

  • Bảo mật dữ liệu: Ngăn chặn việc dữ liệu bên trong đối tượng bị thay đổi một cách tùy tiện từ bên ngoài, đảm bảo tính toàn vẹn của dữ liệu.
  • Giúp lập trình viên kiểm soát chính xác cách dữ liệu được truy cập và sửa đổi thông qua các phương thức được định nghĩa rõ ràng.

Cơ chế hoạt động:

  • Khai báo các biến/thuộc tính là private (chỉ có thể truy cập bên trong Class đó).
  • Cung cấp các hàm public (thường là Getter và Setter) để truy cập và chỉnh sửa các biến private theo một logic đã được kiểm soát.

Ví dụ cách triển khai:

class Student {
    // 1. Thuộc tính được khai báo là private (Đóng gói)
    private int age;

    // 2. Phương thức Setter public để kiểm soát giá trị
    public void setAge(int newAge) {
        // Chỉ cho phép gán tuổi nếu giá trị hợp lệ
        if (newAge > 0) {
            age = newAge;
        } else {
            System.out.println("Tuổi không hợp lệ.");
        }
    }

    // 3. Phương thức Getter public để lấy giá trị
    public int getAge() {
        return age;
    }
}

Tính Kế thừa (Inheritance)

Tính Kế thừa cho phép một lớp (gọi là Lớp Con hay Subclass) thừa hưởng lại các thuộc tính (fields) và hành vi (methods) từ một lớp khác (gọi là Lớp Cha hay Superclass.

Đọc chi tiết: Kế thừa trong Java: Khái niệm, phân loại và những lưu ý

Lợi ích: 

  • Tái sử dụng mã nguồn (Code Reusability): Lớp con tự động có tất cả những gì lớp cha có, bạn không cần phải viết lại mã.
  • Phân cấp rõ ràng: Tạo ra một cấu trúc phân cấp tự nhiên và dễ quản lý cho chương trình.

Cơ chế hoạt động: Sử dụng từ khóa extends trong Java. Lớp con có thể sử dụng lại mã nguồn của lớp cha mà không cần viết lại, đồng thời có thể định nghĩa thêm các đặc tính mới của riêng nó.

Ví dụ: 

Lớp Animal (Động vật) có phương thức eat(). Các lớp con như Dog (Chó) và Cat (Mèo) sẽ tự động có phương thức eat() mà không cần viết lại.

// Lớp Cha (Superclass)
class Animal {
    public void eat() {
        System.out.println("Animal is eating.");
    }
}

// Lớp Con (Subclass) kế thừa từ Animal
class Dog extends Animal {
    public void bark() {
        System.out.println("Dog is barking: Woof woof!");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        // Dog có thể gọi phương thức của chính nó
        myDog.bark(); 
        // Dog có thể gọi phương thức được kế thừa từ Animal
        myDog.eat(); 
    }
}

Tính Đa hình (Polymorphism)

Tính Đa hình có nghĩa là “một tên, nhiều hình thức” (Poly – nhiều, Morph – hình thức). Nó cho phép một hành động được thực hiện theo nhiều cách khác nhau, tùy thuộc vào đối tượng đang thực hiện hành động đó.

Hai loại đa hình chính:

Loại Đa hìnhXảy ra khi Đặc điểmVí dụ 
Overloading (Nạp chồng)Xảy ra trong cùng một Lớp.Các phương thức có cùng tên nhưng khác nhau về số lượng hoặc kiểu dữ liệu của tham số. Quyết định được đưa ra khi biên dịch (Compile-time).Hàm add(int a, int b)add(int a, int b, int c)
Overriding (Ghi đè)Xảy ra giữa Lớp Cha và Lớp Con (yêu cầu Kế thừa).Lớp con định nghĩa lại một phương thức đã có ở lớp cha để thay đổi logic xử lý. Quyết định được đưa ra khi chương trình chạy (Runtime).Lớp Dog ghi đè phương thức makeSound() của lớp Animal.

Ví dụ cách sử dụng:

// Lớp Cha
class Animal {
    public void makeSound() {
        System.out.println("Animal makes a generic sound.");
    }
}

// Lớp Con Ghi đè phương thức makeSound()
class Cat extends Animal {
    @Override
    public void makeSound() {
        // Thay đổi logic cụ thể cho Cat
        System.out.println("Cat says: Meow meow.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myCat = new Cat(); 
        // Dù biến là kiểu Animal, nhưng phương thức của đối tượng Cat được gọi (Đa hình Runtime)
        myCat.makeSound(); // Kết quả: Cat says: Meow meow.
    }
}

Tính Trừu tượng (Abstraction)

Tính trừu tượng là khả năng tập trung vào những tính năng tổng quát của đối tượng thay vì đi sâu vào chi tiết cách thức hoạt động bên trong (tập trung vào “làm cái gì” thay vì “làm như thế nào”).

Cơ chế hoạt động:

Công cụ Đặc điểmKhi nào sử dụng
Abstract ClassCó thể chứa cả phương thức đã định nghĩa (có thân hàm) và phương thức trừu tượng (chỉ có chữ ký).Dùng khi các lớp có quan hệ họ hàng gần gũi (is-a) và muốn chia sẻ chung một phần mã nguồn.
InterfaceChỉ chứa các phương thức trừu tượng (trong các phiên bản Java cũ) hoặc hằng số. Là một bản cam kết về các hành vi mà một lớp phải thực hiện.Dùng khi muốn định nghĩa một khả năng (can-do) cho các lớp không nhất thiết cùng loại.

Phân biệt giữa Abstract Class và Interface:

  • Abstract Class: Animal có phương thức đã định nghĩa sleep() và phương thức trừu tượng makeSound(). Tất cả các lớp con (như Dog, Cat) đều có thể dùng chung sleep() nhưng bắt buộc phải tự định nghĩa makeSound() riêng.
  • Interface: Flyable. Cả Bird (Chim) và Airplane (Máy bay) đều có thể implements (thực thi) Interface này, mặc dù chúng không hề có quan hệ họ hàng.

Các câu hỏi thường gặp về lập trình hướng đối tượng Java

Tại sao Java không hỗ trợ đa kế thừa từ các Lớp (Class)?

Trong Java, một lớp không thể kế thừa trực tiếp từ hai hay nhiều lớp cha (ví dụ: class C extends A, B). Điều này nhằm tránh vấn đề kim cương (Diamond Problem) – tình trạng xung đột khi cả hai lớp cha cùng có một phương thức trùng tên nhưng khác cách xử lý, khiến lớp con không biết nên sử dụng phương thức nào.

Giải pháp: Java sử dụng Interface để thay thế, cho phép một lớp có thể thực thi (implement) nhiều Interface cùng lúc.

Overloading và Overriding khác nhau như thế nào?

Đây là hai khái niệm dễ gây nhầm lẫn trong tính Đa hình:

  • Overloading (Nạp chồng): Xảy ra trong cùng một lớp. Các hàm có cùng tên nhưng khác nhau về tham số (số lượng hoặc kiểu dữ liệu). Kết quả được xác định khi biên dịch (Compile-time).
  • Overriding (Ghi đè): Xảy ra giữa lớp cha và lớp con. Lớp con viết lại phương thức của lớp cha để thay đổi logic xử lý. Kết quả được xác định khi chương trình chạy (Runtime).

Khi nào nên dùng Abstract Class, khi nào nên dùng Interface?

Việc lựa chọn phụ thuộc vào mục đích thiết kế:

  • Dùng Abstract Class: Khi bạn muốn chia sẻ mã nguồn giữa các lớp có liên quan mật thiết (quan hệ “là một” – is a). Ví dụ: Circle và Square đều là Shape.
  • Dùng Interface: Khi bạn muốn định nghĩa một khả năng hoặc hành động cho các lớp không nhất thiết phải cùng loại (quan hệ “có thể làm gì” – can do). Ví dụ: Cả MayBay và Chim đều có thể Flyable.

Khái niệm OOP trong Java có khác biệt nhiều so với Python không?

Dù Java và Python đều hỗ trợ lập trình hướng đối tượng, nhưng cách tiếp cận có một số điểm khác biệt đáng chú ý:

Tiêu chíJavaPython
Triết lý thiết kếBắt buộc (Strict): Mọi dòng code phải nằm trong một Class. Java ép người dùng theo OOP ngay từ đầu.Tùy chọn (Flexible): Hỗ trợ đa mô hình. Bạn có thể viết hướng đối tượng, hướng chức năng hoặc hướng thủ tục.
Kiểu dữ liệu (Typing)Static Typing: Phải khai báo kiểu dữ liệu rõ ràng (ví dụ: Car myCar = new Car()). Kiểm tra lỗi ngay khi biên dịch.Dynamic Typing: Không cần khai báo kiểu (ví dụ: my_car = Car()). Kiểu dữ liệu được xác định khi chương trình đang chạy.
Tính đóng gói (Encapsulation)Nghiêm ngặt: Dùng private, protected, public để thực sự ngăn chặn truy cập trái phép từ bên ngoài.Dựa trên quy ước: Không có rào cản vật lý. Dùng dấu gạch dưới (ví dụ: _attr) để nhắc nhở lập trình viên không nên chạm vào.
Đa kế thừa (Multiple Inheritance)Không hỗ trợ trực tiếp: Một lớp chỉ có 1 cha. Giải quyết bằng cách cho phép triển khai (implement) nhiều Interface.Hỗ trợ trực tiếp: Một lớp có thể kế thừa từ nhiều lớp cha cùng lúc. Sử dụng thuật toán MRO để xử lý xung đột.
Hàm khởi tạo (Constructor)Sử dụng phương thức trùng tên với Class (ví dụ: public ClassName()).Sử dụng phương thức đặc biệt được định nghĩa sẵn là def __init__(self):.
Từ khóa tham chiếuSử dụng từ khóa this để tham chiếu đến đối tượng hiện tại.Sử dụng từ khóa self (phải khai báo tường minh làm tham số đầu tiên trong phương thức).
InterfaceCó từ khóa interface riêng biệt để định nghĩa các bản cam kết hành vi.Không có từ khóa interface. Thường sử dụng các lớp cơ sở trừu tượng (Abstract Base Classes) để thay thế.

Kết luận

Lập trình hướng đối tượng Java không chỉ là việc học các cú pháp hay từ khóa, mà là việc thay đổi tư duy từ giải quyết các bước công việc sang quản lý và tương tác giữa các thực thể. Việc nắm vững bốn cột trụ: Đóng gói, Kế thừa, Đa hình và Trừu tượng sẽ giúp bạn xây dựng được những hệ thống phần mềm có cấu trúc chặt chẽ, an toàn và dễ dàng mở rộng.

Dù các ngôn ngữ lập trình mới liên tục xuất hiện, tư duy OOP vẫn là nền tảng quan trọng giúp lập trình viên thích nghi nhanh chóng với mọi công nghệ. Khi đã hiểu rõ cách vận hành của Lớp và Đối tượng, bạn đã sẵn sàng để tiến xa hơn vào các chủ đề nâng cao như Design Patterns hay kiến trúc hệ thống chuyên nghiệp.

TÁC GIẢ
Tien Tran
Tien Tran

iOS Developer

Có 4 năm kinh nghiệm trong lĩnh vực phát triển ứng dụng mobile, được chứng minh qua lịch sử làm việc ở các công ty lớn (VCCorp, KiotViet, Vega Fintech). Với đam mê tìm hiểu, nghiên cứu những kiến thức chuyên môn cần có của một lập trình viên mobile hiện nay như Swift, Objective C, Flutter, Kotlin,... Tiến mong muốn chia sẻ kinh nghiệm làm việc và truyền cảm hứng cho mọi người muốn theo đuổi con đường trở thành một nhà phát triển ứng dụng di động chuyên nghiệp trong tương lai.