Các Bộ Thay Đổi Cơ Bản Trong Java SE: abstract, static và final

Trong lập trình Java, các bộ thay đổi (modifiers) đóng vai trò quan trọng trong việc định nghĩa hành vi và khả năng tiếp cận của các lớp, phương thức và biến. Ba bộ thay đổi thường gặp và cần nắm vững là abstract, staticfinal.

1. Từ Khóa abstract (Trừu Tượng)

Lớp Trừu Tượng là gì?

Một lớp được khai báo với từ khóa abstract được gọi là lớp trừu tượng. Những lớp này không thể được khởi tạo trực tiếp; nghĩa là, bạn không thể tạo đối tượng từ một lớp trừu tượng bằng cách sử dụng toán tử new.

Khi nào nên định nghĩa một lớp là lớp trừu tượng?

  • Khi lớp đó được thiết kế để làm lớp cha, cung cấp một khuôn mẫu chung cho các lớp con.
  • Khi bạn không mong muốn tạo các đối tượng trực tiếp từ lớp này, mà chỉ muốn sử dụng nó làm cơ sở cho các lớp con cụ thể hơn.

Phương Thức Trừu Tượng là gì?

Một phương thức được khai báo với từ khóa abstract và không có phần thân (implementation) được gọi là phương thức trừu tượng.

public abstract void thucHienCongViec(); // Phương thức trừu tượng không có thân

Nguyên tắc sử dụng abstract:

  1. Nếu một lớp chứa ít nhất một phương thức trừu tượng, lớp đó phải được khai báo là lớp trừu tượng.
  2. Nếu một lớp con kế thừa từ một lớp trừu tượng, nó phải cài đặt (override) tất cả các phương thức trừu tượng của lớp cha. Nếu không, lớp con đó cũng phải được khai báo là lớp trừu tượng.

Ví dụ về lớp và phương thức trừu tượng:

abstract class HinhHoc {
    protected String ten;

    public HinhHoc(String ten) {
        this.ten = ten;
    }

    // Phương thức trừu tượng để tính diện tích
    public abstract double tinhDienTich();

    // Phương thức cụ thể
    public void hienThiThongTin() {
        System.out.println("Đây là một hình học: " + ten);
    }
}

class HinhTron extends HinhHoc {
    private double banKinh;

    public HinhTron(String ten, double banKinh) {
        super(ten);
        this.banKinh = banKinh;
    }

    @Override
    public double tinhDienTich() {
        return Math.PI * banKinh * banKinh;
    }
}

// Sử dụng
public class ViDuAbstract {
    public static void main(String[] args) {
        HinhTron hinhTron = new HinhTron("Hình tròn A", 5.0);
        hinhTron.hienThiThongTin();
        System.out.println("Diện tích của " + hinhTron.ten + ": " + hinhTron.tinhDienTich());

        // HinhHoc h = new HinhHoc("Không xác định"); // Lỗi: không thể khởi tạo lớp trừu tượng
    }
}

2. Từ Khóa static (Tĩnh)

Từ khóa static được sử dụng để khai báo các thành viên (biến hoặc phương thức) thuộc về lớp thay vì thuộc về một thể hiện (đối tượng) cụ thể của lớp. Các thành viên static được chia sẻ bởi tất cả các đối tượng của lớp đó, và chỉ tồn tại một bản sao duy nhất trong bộ nhớ.

Đặc điểm của thành viên static:

  • Thuộc về lớp: Không cần tạo đối tượng để truy cập.
  • Chia sẻ: Tất cả các đối tượng của lớp đều chia sẻ cùng một bản sao của biến static hoặc cùng một phương thức static.
  • Thời gian sống: Các thành viên static được tải vào bộ nhớ khi lớp được tải, trước khi bất kỳ đối tượng nào được tạo.

Cách truy cập thành viên static:

Thường truy cập bằng tên lớp và dấu chấm: TenLop.tenBienStatic hoặc TenLop.phuongThucStatic().

double piValue = Math.PI; // Truy cập biến static PI trong lớp Math
int num = Integer.parseInt("123"); // Truy cập phương thức static parseInt trong lớp Integer

Lưu ý khi sử dụng static:

  • Phương thức static chỉ có thể truy cập trực tiếp các thành viên static khác của cùng lớp.
  • Phương thức static không thể sử dụng từ khóa this hoặc super vì chúng không liên quan đến một đối tượng cụ thể.
  • Phương thức không static (thể hiện) có thể truy cập cả thành viên static và không static.
  • Để một phương thức static truy cập một thành viên không static, cần tạo một đối tượng của lớp đó.

Ví dụ về static:

class NhanVien {
    private String maNhanVien;
    private String ten;
    public static String TEN_CONG_TY = "ABC Corp"; // Biến static, chia sẻ cho tất cả nhân viên
    private static int soLuongNhanVien = 0; // Biến static đếm số lượng đối tượng

    public NhanVien(String maNhanVien, String ten) {
        this.maNhanVien = maNhanVien;
        this.ten = ten;
        soLuongNhanVien++; // Tăng số lượng khi có nhân viên mới
    }

    public void hienThiThongTin() {
        System.out.println("Mã NV: " + maNhanVien + ", Tên: " + ten + ", Công ty: " + TEN_CONG_TY);
    }

    public static int laySoLuongNhanVien() { // Phương thức static
        return soLuongNhanVien;
    }
}

public class ViDuStatic {
    public static void main(String[] args) {
        NhanVien nv1 = new NhanVien("NV001", "Nguyễn Văn A");
        NhanVien nv2 = new NhanVien("NV002", "Trần Thị B");

        nv1.hienThiThongTin();
        nv2.hienThiThongTin();

        // Truy cập biến static qua tên lớp
        System.out.println("Tên công ty (qua lớp): " + NhanVien.TEN_CONG_TY);
        // Truy cập phương thức static qua tên lớp
        System.out.println("Tổng số lượng nhân viên: " + NhanVien.laySoLuongNhanVien());
    }
}

Mẫu thiết kế Singleton (Đơn Thể):

Mẫu Singleton đảm bảo rằng một lớp chỉ có thể có một thể hiện duy nhất và cung cấp một điểm truy cập toàn cục tới thể hiện đó. Nó thường sử dụng static.

Cách triển khai Singleton:

  1. Đặt constructor là private để ngăn chặn việc khởi tạo từ bên ngoài.
  2. Cung cấp một phương thức static để trả về thể hiện duy nhất của lớp.

Phân loại:

a) Khởi tạo kiểu Eager (khởi tạo sớm): Thể hiện được tạo ngay khi lớp được tải.

class CauHinhUngDungEager {
    private static final CauHinhUngDungEager INSTANCE = new CauHinhUngDungEungDungEager(); // Khởi tạo ngay lập tức

    private String thongTinCauHinh;

    private CauHinhUngDungEager() {
        thongTinCauHinh = "Cấu hình mặc định (Eager)";
    }

    public static CauHinhUngDungEager getInstance() {
        return INSTANCE;
    }

    public String getThongTinCauHinh() {
        return thongTinCauHinh;
    }

    public void setThongTinCauHinh(String thongTinCauHinh) {
        this.thongTinCauHinh = thongTinCauHinh;
    }
}

b) Khởi tạo kiểu Lazy (khởi tạo muộn): Thể hiện được tạo khi nó được yêu cầu lần đầu tiên.

class CauHinhUngDungLazy {
    private static CauHinhUngDungLazy instance; // Chưa khởi tạo

    private String thongTinCauHinh;

    private CauHinhUngDungLazy() {
        thongTinCauHinh = "Cấu hình mặc định (Lazy)";
    }

    // Phương thức static để lấy thể hiện duy nhất
    public static CauHinhUngDungLazy getInstance() {
        if (instance == null) {
            // Đồng bộ hóa để đảm bảo an toàn luồng trong môi trường đa luồng
            synchronized (CauHinhUngDungLazy.class) {
                if (instance == null) { // Kiểm tra lại sau khi vào khối đồng bộ
                    instance = new CauHinhUngDungLazy();
                }
            }
        }
        return instance;
    }

    public String getThongTinCauHinh() {
        return thongTinCauHinh;
    }

    public void setThongTinCauHinh(String thongTinCauHinh) {
        this.thongTinCauHinh = thongTinCauHinh;
    }
}

public class ViDuSingleton {
    public static void main(String[] args) {
        CauHinhUngDungEager configEager1 = CauHinhUngDungEager.getInstance();
        System.out.println("Eager: " + configEager1.getThongTinCauHinh());
        configEager1.setThongTinCauHinh("Cấu hình cập nhật (Eager)");
        CauHinhUngDungEager configEager2 = CauHinhUngDungEager.getInstance();
        System.out.println("Eager (sau cập nhật): " + configEager2.getThongTinCauHinh());

        CauHinhUngDungLazy configLazy1 = CauHinhUngDungLazy.getInstance();
        System.out.println("Lazy: " + configLazy1.getThongTinCauHinh());
        configLazy1.setThongTinCauHinh("Cấu hình cập nhật (Lazy)");
        CauHinhUngDungLazy configLazy2 = CauHinhUngDungLazy.getInstance();
        System.out.println("Lazy (sau cập nhật): " + configLazy2.getThongTinCauHinh());
    }
}

3. Từ Khóa final (Cuối Cùng)

Từ khóa final chỉ ra rằng một thực thể không thể được thay đổi hoặc mở rộng. Nó có thể được áp dụng cho biến, phương thức và lớp.

final cho biến:

  • Biến final trở thành hằng số và phải được khởi tạo một lần duy nhất (hoặc trực tiếp, trong constructor, hoặc trong khối khởi tạo). Giá trị của nó không thể thay đổi sau khi được gán.
  • Đối với biến kiểu nguyên thủy (int, double, ...), giá trị của biến là không đổi.
  • Đối với biến tham chiếu (tham chiếu đến đối tượng), bản thân tham chiếu là không đổi (nó luôn trỏ đến cùng một đối tượng). Tuy nhiên, các thuộc tính bên trong của đối tượng mà tham chiếu đó trỏ tới vẫn có thể được thay đổi nếu đối tượng đó là mutable (có thể thay đổi).
class SinhVien {
    int tuoi;

    public SinhVien(int tuoi) {
        this.tuoi = tuoi;
    }
}

public class ViDuFinal {
    public static void main(String[] args) {
        final int MAX_ATTEMPTS = 3; // Hằng số kiểu nguyên thủy
        // MAX_ATTEMPTS = 5; // Lỗi: không thể gán lại giá trị cho final variable

        final SinhVien sv1 = new SinhVien(20); // Tham chiếu sv1 là final
        System.out.println("Tuổi SV1 ban đầu: " + sv1.tuoi);
        sv1.tuoi = 21; // Hợp lệ: đối tượng mà sv1 trỏ tới có thể thay đổi thuộc tính
        System.out.println("Tuổi SV1 sau thay đổi thuộc tính: " + sv1.tuoi);

        // sv1 = new SinhVien(22); // Lỗi: không thể gán lại tham chiếu cho final variable
    }
}

final cho phương thức:

  • Một phương thức được khai báo là final không thể bị ghi đè (override) bởi các lớp con. Điều này hữu ích khi bạn muốn đảm bảo hành vi của phương thức không thay đổi trong các lớp kế thừa.
class DongVat {
    public final void tiengKeu() {
        System.out.println("Tiếng kêu của động vật.");
    }
}

class Cho extends DongVat {
    // @Override // Lỗi: không thể ghi đè phương thức final
    // public void tiengKeu() {
    //     System.out.println("Gâu gâu!");
    // }
}

final cho lớp:

  • Một lớp được khai báo là final không thể bị kế thừa. Điều này thường được sử dụng cho các lớp tiện ích hoặc các lớp mà bạn muốn bảo mật kiến trúc của chúng. Ví dụ: lớp String trong Java là một lớp final.
final class LopKhongKeThua {
    public void phuongThuc() {
        System.out.println("Đây là một lớp final.");
    }
}

// class LopCon extends LopKhongKeThua {} // Lỗi: không thể kế thừa từ lớp final

4. Các Khối Mã Lệnh (Code Blocks)

Java có hai loại khối mã lệnh chính: khối khởi tạo thể hiện (instance initializer block) và khối khởi tạo tĩnh (static initializer block).

Khối Khởi Tạo Thể Hiện (Instance Initializer Block):

  • Chạy mỗi khi một đối tượng của lớp được tạo.
  • Thực thi trước khi constructor được gọi.
  • Thứ tự thực thi của các khối khởi tạo thể hiện và việc khởi tạo thuộc tính thể hiện được xác định bởi thứ tự chúng xuất hiện trong mã nguồn.

Khối Khởi Tạo Tĩnh (Static Initializer Block):

  • Chạy một lần duy nhất khi lớp được tải vào bộ nhớ (class loading).
  • Thực thi trước mọi khối khởi tạo thể hiện và constructor.
  • Thứ tự thực thi của các khối khởi tạo tĩnh và việc khởi tạo thuộc tính tĩnh được xác định bởi thứ tự chúng xuất hiện trong mã nguồn.

Thời điểm Tải Lớp (Class Loading):

Một lớp được tải vào bộ nhớ khi nó được sử dụng lần đầu tiên, ví dụ:

  • Khi tạo một đối tượng của lớp đó (new TenLop()).
  • Khi truy cập một trường static của lớp (ví dụ: TenLop.BIEN_STATIC).
  • Khi gọi một phương thức static của lớp (ví dụ: TenLop.phuongThucStatic()).
  • Khi một lớp con của nó được tải (lớp cha sẽ được tải trước).

Ví dụ về Khối Mã Lệnh:

class MyClass {
    static {
        System.out.println("1. Khối khởi tạo tĩnh được thực thi (Class loaded).");
    }

    private static String staticField = initStaticField();

    private static String initStaticField() {
        System.out.println("2. Khởi tạo thuộc tính tĩnh.");
        return "Giá trị tĩnh";
    }

    {
        System.out.println("3. Khối khởi tạo thể hiện được thực thi (Instance created).");
    }

    private String instanceField = initInstanceField();

    private String initInstanceField() {
        System.out.println("4. Khởi tạo thuộc tính thể hiện.");
        return "Giá trị thể hiện";
    }

    public MyClass() {
        System.out.println("5. Constructor được gọi.");
    }
}

public class ViDuCodeBlock {
    public static void main(String[] args) {
        System.out.println("--- Bắt đầu main ---");
        MyClass obj1 = new MyClass();
        System.out.println("--- Kết thúc tạo obj1 ---");
        MyClass obj2 = new MyClass();
        System.out.println("--- Kết thúc tạo obj2 ---");
    }
}
/*
Kết quả đầu ra sẽ tương tự:
--- Bắt đầu main ---
1. Khối khởi tạo tĩnh được thực thi (Class loaded).
2. Khởi tạo thuộc tính tĩnh.
3. Khối khởi tạo thể hiện được thực thi (Instance created).
4. Khởi tạo thuộc tính thể hiện.
5. Constructor được gọi.
--- Kết thúc tạo obj1 ---
3. Khối khởi tạo thể hiện được thực thi (Instance created).
4. Khởi tạo thuộc tính thể hiện.
5. Constructor được gọi.
--- Kết thúc tạo obj2 ---
*/

5. Giao Diện (Interfaces)

Trong Java, một giao diện là một bản thiết kế của một lớp. Nó chỉ chứa các khai báo phương thức (từ Java 8 trở về trước, tất cả là abstract) và các hằng số public static final (ngầm định). Giao diện là một cơ chế để đạt được tính trừu tượng hoàn toàn và đa kế thừa trong Java (đối với hành vi).

Cú pháp cơ bản (trước Java 8):

public interface TenGiaoDien {
    // Các hằng số (ngầm định là public static final)
    int MAX_VALUE = 100;

    // Các phương thức trừu tượng (ngầm định là public abstract)
    void thucHien();
    String layThongTin();
}

Đặc điểm của Giao Diện:

  • Được định nghĩa bằng từ khóa interface.
  • Các thuộc tính trong giao diện mặc định là public static final.
  • Các phương thức trong giao diện mặc định là public abstract (trước Java 8).
  • Giao diện có thể kế thừa từ nhiều giao diện khác bằng từ khóa extends.
interface GiaoDienA { void methodA(); }
interface GiaoDienB extends GiaoDienA { void methodB(); } // GiaoDienB kế thừa methodA và methodB

Triển khai Giao Diện:

  • Một lớp triển khai một giao diện sử dụng từ khóa implements.
  • Một lớp có thể triển khai nhiều giao diện (đa kế thừa hành vi).
  • Nếu một lớp triển khai một giao diện, nó phải cung cấp cài đặt cho tất cả các phương thức trừu tượng của giao diện đó. Nếu không, lớp đó phải được khai báo là abstract.
  • Thứ tự: Nếu một lớp vừa kế thừa từ một lớp cha vừa triển khai các giao diện, từ khóa extends phải đứng trước implements.
class LopCha { /* ... */ }

class LopTrienKhai extends LopCha implements TenGiaoDien, GiaoDienKhac {
    @Override
    public void thucHien() {
        System.out.println("Thực hiện phương thức từ giao diện.");
    }

    @Override
    public String layThongTin() {
        return "Thông tin đã lấy.";
    }

    // ... cài đặt các phương thức khác từ GiaoDienKhac nếu có
}

Cải tiến Giao Diện trong Java 8:

  • Phương thức mặc định (default method): Cho phép thêm các phương thức có cài đặt vào giao diện mà không phá vỡ các lớp đã triển khai giao diện đó. Các lớp có thể tùy chọn ghi đè phương thức mặc định này.
  • Phương thức tĩnh (static method): Các phương thức static trong giao diện cũng có cài đặt và có thể được gọi trực tiếp bằng tên giao diện (TenGiaoDien.phuongThucStatic()). Chúng không thể bị ghi đè.

Xử lý xung đột phương thức mặc định:

Nếu một lớp triển khai hai giao diện có cùng một phương thức mặc định với cùng chữ ký, hoặc một giao diện có phương thức mặc định và giao diện khác có phương thức trừu tượng với cùng chữ ký, lớp triển khai bắt buộc phải ghi đè phương thức đó để giải quyết xung đột.

Ví dụ Giao Diện Java 8+:

interface DienThoai {
    String GOI_KHAN_CAP = "113"; // Hằng số

    void ngheGoi(); // Phương thức trừu tượng

    default void guiTinNhan() { // Phương thức mặc định
        System.out.println("Gửi tin nhắn mặc định.");
    }

    static void hienThiThongTinLoai() { // Phương thức tĩnh
        System.out.println("Đây là một thiết bị điện thoại.");
    }
}

class DienThoaiThongMinh implements DienThoai {
    @Override
    public void ngheGoi() {
        System.out.println("Đang thực hiện cuộc gọi bằng điện thoại thông minh.");
    }

    @Override
    public void guiTinNhan() { // Có thể ghi đè phương thức mặc định
        System.out.println("Gửi tin nhắn nâng cao từ điện thoại thông minh.");
    }
}

public class ViDuInterface {
    public static void main(String[] args) {
        DienThoaiThongMinh dt = new DienThoaiThongMinh();
        dt.ngheGoi();
        dt.guiTinNhan(); // Gọi phương thức ghi đè

        DienThoai.hienThiThongTinLoai(); // Gọi phương thức tĩnh của giao diện
        System.out.println("Số gọi khẩn cấp: " + DienThoai.GOI_KHAN_CAP); // Truy cập hằng số
    }
}

6. Lớp Nội Bộ (Inner Classes)

Lớp nội bộ là một lớp được định nghĩa bên trong một lớp khác. Chúng hữu ích để nhóm các lớp có liên quan chặt chẽ với nhau hoặc để tạo các lớp nhỏ, chuyên biệt mà không cần khai báo chúng ở cấp độ gói.

Lớp Nội Bộ Ẩn Danh (Anonymous Inner Classes):

Là một loại lớp nội bộ không có tên. Chúng được tạo và khởi tạo cùng một lúc, thường được sử dụng khi bạn cần một thể hiện của một giao diện hoặc một lớp trừu tượng chỉ một lần và không cần tạo một lớp cụ thể riêng biệt. Chúng thường được dùng để cung cấp một cài đặt ngắn gọn cho một phương thức.

Ứng dụng của Lớp Nội Bộ Ẩn Danh:

  • Khi cần triển khai một giao diện hoặc một lớp trừu tượng chỉ một lần.
  • Để truyền một đối tượng hành vi (ví dụ: trình xử lý sự kiện) làm tham số cho một phương thức.

Ví dụ về Lớp Nội Bộ Ẩn Danh:

interface ChaoMung {
    void inLoiChao(String ten);
}

public class ViDuAnonymousInnerClass {
    public static void main(String[] args) {
        // Tạo một thể hiện của giao diện ChaoMung bằng lớp nội bộ ẩn danh
        ChaoMung chaoTiengViet = new ChaoMung() {
            @Override
            public void inLoiChao(String ten) {
                System.out.println("Xin chào, " + ten + "!");
            }
        };

        chaoTiengViet.inLoiChao("An");

        ChaoMung chaoTiengAnh = new ChaoMung() {
            @Override
            public void inLoiChao(String ten) {
                System.out.println("Hello, " + ten + "!");
            }
        };

        chaoTiengAnh.inLoiChao("Peter");
    }
}

7. Gói (Packages)

Gói trong Java là một cơ chế dùng để nhóm các lớp, giao diện và các gói con có liên quan lại với nhau. Chúng giúp tổ chức mã nguồn, tránh xung đột tên và kiểm soát quyền truy cập.

  • Để sử dụng một lớp từ một gói khác, bạn cần sử dụng câu lệnh import.
  • Có thể nhập một lớp cụ thể (import java.util.ArrayList;) hoặc tất cả các lớp trong một gói bằng ký tự đại diện * (import java.util.*;).
  • Các lớp trong gói java.lang được nhập tự động.

Thẻ: Java JavaSE abstract static final

Đăng vào ngày 15 tháng 6 lúc 00:45