Nạp Chồng và Ghi Đè Phương Thức trong Java: Sự Khác Biệt Cốt Lõi

Trong lập trình hướng đối tượng với Java, hai khái niệm nạp chồng phương thức (method overloading) và ghi đè phương thức (method overriding) là nền tảng nhưng thường gây nhầm lẫn cho người mới học. Việc hiểu rõ sự khác biệt giữa chúng cùng với các quy tắc áp dụng cho các loại phương thức khác nhau như phương thức khởi tạo, phương thức tĩnh, phương thức final và phương thức private là rất quan trọng.

1. Nạp Chồng Phương Thức (Method Overloading)

Nạp chồng phương thức xảy ra khi có hai hoặc nhiều phương thức trong cùng một lớp có cùng tên nhưng khác nhau về danh sách tham số. Sự khác biệt về danh sách tham số có thể là số lượng tham số, kiểu dữ liệu của tham số hoặc thứ tự của các tham số.

Lưu ý:

  • Kiểu trả về của phương thức KHÔNG ảnh hưởng đến việc nạp chồng.
  • Công cụ truy cập (public, private, protected) KHÔNG ảnh hưởng đến việc nạp chồng.
  • Nạp chồng có thể áp dụng cho các phương thức khởi tạo (constructor), phương thức tĩnh (static) và phương thức final.

Ví dụ về Nạp Chồng Phương Thức:

public class Calculator {

    // Nạp chồng phương thức khởi tạo
    public Calculator() {
        System.out.println("Khởi tạo Calculator không tham số.");
    }

    public Calculator(String type) {
        System.out.println("Khởi tạo Calculator loại: " + type);
    }

    // Nạp chồng phương thức tính tổng
    public int sum(int a, int b) {
        System.out.println("Tính tổng 2 số nguyên.");
        return a + b;
    }

    public int sum(int a, int b, int c) {
        System.out.println("Tính tổng 3 số nguyên.");
        return a + b + c;
    }

    public double sum(double a, double b) {
        System.out.println("Tính tổng 2 số thực.");
        return a + b;
    }

    // Nạp chồng phương thức tĩnh (static method)
    public static void logMessage(String message) {
        System.out.println("[LOG] " + message);
    }

    public static void logMessage(String message, int repeat) {
        for (int i = 0; i < repeat; i++) {
            System.out.println("[LOG-" + (i + 1) + "] " + message);
        }
    }

    // Nạp chồng phương thức final
    public final void displayInfo() {
        System.out.println("Hiển thị thông tin cơ bản.");
    }

    public final void displayInfo(String detail) {
        System.out.println("Hiển thị thông tin chi tiết: " + detail);
    }

    public static void main(String[] args) {
        Calculator calcDefault = new Calculator();
        Calculator calcScientific = new Calculator("Khoa học");

        System.out.println("Kết quả sum(5, 10): " + calcDefault.sum(5, 10));
        System.out.println("Kết quả sum(1, 2, 3): " + calcDefault.sum(1, 2, 3));
        System.out.println("Kết quả sum(2.5, 3.5): " + calcDefault.sum(2.5, 3.5));

        Calculator.logMessage("Ứng dụng khởi động.");
        Calculator.logMessage("Cảnh báo lỗi!", 2);

        calcDefault.displayInfo();
        calcDefault.displayInfo("Phiên bản 1.0");
    }
}

Từ ví dụ trên, chúng ta có thể kết luận rằng các phương thức khởi tạo, phương thức tĩnh và phương thức final đều có thể được nạp chồng.

2. Ghi Đè Phương Thức (Method Overriding)

Ghi đè phương thức xảy ra khi một lớp con (subclass) cung cấp một triển khai cụ thể cho một phương thức đã được định nghĩa trong lớp cha (superclass). Để ghi đè, phương thức trong lớp con phải có:

  • Cùng tên với phương thức trong lớp cha.
  • Cùng danh sách tham số với phương thức trong lớp cha.
  • Cùng kiểu trả về hoặc kiểu trả về đồng biến (covariant return type) với phương thức trong lớp cha.
  • Công cụ truy cập (access modifier) tương đương hoặc mở rộng hơn so với phương thức trong lớp cha.
  • Không được phép ném ngoại lệ (exception) rộng hơn so với phương thức trong lớp cha.

Annotation @Override được khuyến khích sử dụng để báo hiệu cho trình biên dịch rằng bạn đang cố gắng ghi đè một phương thức và giúp phát hiện lỗi sớm nếu các quy tắc ghi đè không được tuân thủ.

Các trường hợp KHÔNG THỂ ghi đè:

Không phải tất cả các loại phương thức đều có thể được ghi đè. Dưới đây là các trường hợp điển hình:

Phương thức khởi tạo (Constructor)

Phương thức khởi tạo không phải là phương thức thông thường, chúng không được kế thừa theo nghĩa đen để ghi đè. Một lớp con sẽ gọi phương thức khởi tạo của lớp cha thông qua từ khóa super(), chứ không phải ghi đè nó.

Phương thức tĩnh (Static Method)

Phương thức tĩnh thuộc về lớp, không thuộc về đối tượng. Do đó, chúng không thể được ghi đè. Nếu một lớp con định nghĩa một phương thức tĩnh có cùng tên và danh sách tham số với phương thức tĩnh của lớp cha, đây được gọi là che giấu phương thức (method hiding), chứ không phải ghi đè.

Phương thức final

Từ khóa final được sử dụng để chỉ ra rằng một phương thức hoặc lớp không thể bị thay đổi hoặc mở rộng. Do đó, các phương thức được đánh dấu là final KHÔNG THỂ bị ghi đè bởi lớp con.

Phương thức private

Các phương thức private không được kế thừa bởi lớp con. Chúng chỉ hiển thị và có thể được truy cập trong chính lớp đã định nghĩa chúng. Do đó, không thể ghi đè một phương thức private.

Ví dụ minh họa các trường hợp KHÔNG THỂ ghi đè:

public class Shape {
    private String name;

    // Phương thức khởi tạo
    public Shape() {
        this("Hình dạng cơ bản");
        System.out.println("Khởi tạo Shape mặc định.");
    }

    public Shape(String name) {
        this.name = name;
        System.out.println("Khởi tạo Shape với tên: " + this.name);
    }

    // Phương thức tĩnh - không thể ghi đè
    public static void displayType() {
        System.out.println("Đây là một loại Hình dạng.");
    }

    // Phương thức final - không thể ghi đè
    public final void getDescription() {
        System.out.println("Mô tả chung cho " + name + ".");
    }

    // Phương thức private - không được kế thừa để ghi đè
    private void internalCalculation() {
        System.out.println("Thực hiện tính toán nội bộ của Shape.");
    }

    // Phương thức public thông thường - có thể ghi đè
    public void printDetails() {
        System.out.println("Chi tiết: " + name);
        internalCalculation(); // Lớp cha gọi phương thức private của nó
    }
}

public class Circle extends Shape {
    private double radius;

    // Phương thức khởi tạo: KHÔNG ghi đè Shape(), mà gọi nó thông qua super()
    public Circle(double radius) {
        super("Hình tròn"); // Gọi constructor của lớp cha
        this.radius = radius;
        System.out.println("Khởi tạo Circle với bán kính: " + radius);
    }

    // Cố gắng "ghi đè" phương thức tĩnh -> đây là che giấu (hiding)
    // @Override // Lỗi biên dịch: Static methods cannot be overridden
    public static void displayType() {
        System.out.println("Đây là một loại Hình tròn.");
    }

    // Cố gắng ghi đè phương thức final -> lỗi biên dịch
    // @Override // Lỗi biên dịch: Cannot override the final method from Shape
    // public final void getDescription() {
    //     System.out.println("Mô tả chi tiết cho hình tròn bán kính " + radius + ".");
    // }

    // Phương thức private mới trong Circle, KHÔNG liên quan đến internalCalculation() của Shape
    private void internalCalculation() {
        System.out.println("Thực hiện tính toán nội bộ của Circle.");
    }

    // Phương thức public thông thường - CÓ THỂ ghi đè
    @Override
    public void printDetails() {
        System.out.println("Hình tròn có bán kính: " + radius);
        internalCalculation(); // Lớp con gọi phương thức private của nó
    }

    public static void main(String[] args) {
        System.out.println("--- Đối tượng Shape ---");
        Shape baseShape = new Shape("Hình chữ nhật");
        baseShape.printDetails();
        baseShape.getDescription();
        Shape.displayType();

        System.out.println("\n--- Đối tượng Circle ---");
        Circle myCircle = new Circle(7.5);
        myCircle.printDetails();      // Gọi phương thức printDetails đã ghi đè của Circle
        myCircle.getDescription();    // Gọi phương thức final của Shape (không thể ghi đè)
        Circle.displayType();         // Gọi phương thức tĩnh bị che giấu của Circle

        System.out.println("\n--- Đa hình (Polymorphism) ---");
        Shape polymorphicShape = new Circle(12.0); // Upcasting
        polymorphicShape.printDetails();      // Runtime gọi printDetails() của Circle (ghi đè)
        polymorphicShape.getDescription();    // Runtime gọi getDescription() của Shape (final)
        Shape.displayType();                  // Compile-time gọi displayType() của Shape (tĩnh)
    }
}

3. Tóm Tắt và Lý Do

Dưới đây là bảng tổng hợp và lý giải ngắn gọn về khả năng nạp chồng và ghi đè cho từng loại phương thức:

Loại phương thức Có thể Nạp chồng? Có thể Ghi đè? Giải thích
Phương thức khởi tạo ✓ (Có) ✗ (Không) Phương thức khởi tạo có thể có nhiều phiên bản với danh sách tham số khác nhau. Chúng không được kế thừa để ghi đè mà được gọi bởi lớp con thông qua super().
Phương thức tĩnh (static) ✓ (Có) ✗ (Không) Phương thức tĩnh thuộc về lớp và được ràng buộc tại thời điểm biên dịch (compile-time binding). Ghi đè liên quan đến đa hình và ràng buộc tại thời điểm chạy (runtime binding), do đó chúng không thể ghi đè mà chỉ có thể bị che giấu (hidden).
Phương thức final ✓ (Có) ✗ (Không) Từ khóa final chỉ ra rằng phương thức này không thể bị thay đổi hoặc cung cấp triển khai mới trong các lớp con. Tuy nhiên, một lớp vẫn có thể định nghĩa các phiên bản nạp chồng khác nhau của phương thức final.
Phương thức private ✓ (Có) ✗ (Không) Phương thức private chỉ hiển thị trong lớp mà nó được định nghĩa. Nó không được kế thừa bởi lớp con, do đó không thể ghi đè. Lớp con có thể có một phương thức cùng tên nhưng đó là một phương thức hoàn toàn mới.
Phương thức thông thường (public, protected) ✓ (Có) ✓ (Có) Đây là loại phương thức điển hình cho cả nạp chồng và ghi đè, hỗ trợ tính đa hình của OOP.

Hiểu rõ sự khác biệt giữa nạp chồng và ghi đè là chìa khóa để thiết kế các hệ thống hướng đối tượng mạnh mẽ và dễ bảo trì trong Java.

Thẻ: Java Overloading Overriding oop StaticMethods

Đăng vào ngày 10 tháng 10 lúc 08:21