Trong ngôn ngữ lập trình Java, từ khóa final đóng vai trò quan trọng trong việc định nghĩa các thực thể không thể thay đổi hoặc không thể mở rộng. Nó có thể được áp dụng cho ba loại thành phần chính: biến (dữ liệu), phương thức và lớp.
1. final với Biến (Dữ liệu)
Khi final được sử dụng với các biến, nó biến biến đó thành một hằng số hoặc một tham chiếu không thể thay đổi. Các biến trong Java có thể là biến cục bộ (local variables), biến thể hiện (instance variables) hoặc biến tĩnh (static/class variables). Cách final ảnh hưởng đến chúng phụ thuộc vào kiểu dữ liệu (nguyên thủy hay tham chiếu).
1.1. Biến kiểu nguyên thủy (Primitive Types)
Đối với các kiểu dữ liệu nguyên thủy (như int, boolean, double), từ khóa final đảm bảo rằng giá trị của biến sẽ không bao giờ thay đổi sau khi nó được gán lần đầu.
public class CacBienHangSo {
private final int MAX_GIA_TRI = 100; // Biến final cấp độ lớp (instance final variable)
public static final double HANG_SO_PI = 3.14159; // Biến final tĩnh (static final variable)
public void inGiaTri() {
final String LOI_THONG_BAO = "Lỗi không xác định"; // Biến final cục bộ
System.out.println("Giá trị max: " + MAX_GIA_TRI);
System.out.println("Hằng số PI: " + HANG_SO_PI);
System.out.println("Thông báo: " + LOI_THONG_BAO);
// MAX_GIA_TRI = 101; // Lỗi biên dịch: không thể gán lại cho biến final
}
}
1.2. Biến kiểu tham chiếu (Reference Types)
Khi final được áp dụng cho một biến kiểu tham chiếu, nó có nghĩa là tham chiếu đó (tức là con trỏ tới đối tượng) không thể được thay đổi để trỏ tới một đối tượng khác. Tuy nhiên, bản thân đối tượng mà tham chiếu đó đang trỏ tới vẫn có thể được sửa đổi, miễn là đối tượng đó không phải là bất biến (immutable).
import java.util.ArrayList;
import java.util.List;
public class ThamChieuFinal {
private final List<String> danhSachMuc;
public ThamChieuFinal() {
danhSachMuc = new ArrayList<>();
danhSachMuc.add("Mục A");
}
public void themVaoDanhSach(String mucMoi) {
// danhSachMuc = new ArrayList<>(); // Lỗi biên dịch: không thể gán lại tham chiếu final
danhSachMuc.add(mucMoi); // Hợp lệ: đối tượng List vẫn có thể được thay đổi
System.out.println("Danh sách hiện tại: " + danhSachMuc);
}
}
1.3. Hằng số thời gian biên dịch (Compile-time Constants)
Nếu một biến final có kiểu nguyên thủy hoặc kiểu String và được khởi tạo bằng một giá trị hằng số (literal value) ngay tại thời điểm khai báo, nó sẽ trở thành một "hằng số thời gian biên dịch". Giá trị của nó được biết trước khi chương trình chạy và có thể được tối ưu hóa bởi trình biên dịch.
public class HangSoBienDich {
public final static int SO_LUONG_MAC_DINH = 5; // Hằng số thời gian biên dịch tĩnh
public final String TEN_UNG_DUNG = "MyApp"; // Hằng số thời gian biên dịch thể hiện
}
1.4. Biến final được khởi tạo tại thời gian chạy (Runtime-initialized final variables)
Không phải mọi biến final đều là hằng số thời gian biên dịch. Nếu giá trị của biến final được quyết định trong thời gian chạy (ví dụ, thông qua một phương thức hoặc dữ liệu đầu vào), nó vẫn là final (không thể gán lại) nhưng không phải là hằng số thời gian biên dịch.
import java.util.Random;
public class BienFinalKhoiTaoRuntime {
private final int maNgauNhien; // Biến thể hiện final, khởi tạo trong constructor
private final static String TOKEN_BI_MAT; // Biến tĩnh final, khởi tạo trong static block
static {
TOKEN_BI_MAT = "TOKEN-" + new Random().nextInt(9999);
}
public BienFinalKhoiTaoRuntime() {
this.maNgauNhien = new Random().nextInt(100);
}
public void hienThiThongTin() {
System.out.println("Mã ngẫu nhiên cá nhân: " + maNgauNhien);
System.out.println("Token bí mật hệ thống: " + TOKEN_BI_MAT);
}
}
1.5. Quy tắc khởi tạo biến final
Biến final phải được khởi tạo chính xác một lần. Đối với biến tĩnh final, việc này phải diễn ra trong khai báo hoặc trong một khối tĩnh (static block). Đối với biến thể hiện final, có thể khởi tạo trong khai báo, trong khối khởi tạo (initializer block) hoặc trong tất cả các hàm tạo (constructors). Biến cục bộ final phải được khởi tạo trước khi sử dụng lần đầu.
2. final với Phương thức
Khi một phương thức được khai báo với từ khóa final, nó 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 một hành vi cụ thể của lớp cha không bị thay đổi trong các lớp kế thừa, duy trì tính nhất quán của thiết kế.
public class CoCheHanCheGhiDe {
public final void phuongThucCotLoi() {
System.out.println("Đây là một chức năng cốt lõi, không thể thay đổi.");
}
public void phuongThucMoRong() {
System.out.println("Đây là một chức năng có thể được mở rộng hoặc thay đổi.");
}
}
class LopPhu extends CoCheHanCheGhiDe {
// @Override
// public void phuongThucCotLoi() { // Lỗi biên dịch: không thể ghi đè phương thức final
// System.out.println("Thử ghi đè phương thức cốt lõi...");
// }
@Override
public void phuongThucMoRong() {
System.out.println("Lớp phụ đã thay đổi chức năng mở rộng.");
}
}
Cần lưu ý rằng phương thức private không thể bị ghi đè do phạm vi truy cập của chúng chỉ giới hạn trong lớp định nghĩa. Do đó, việc thêm từ khóa final vào một phương thức private là dư thừa và không mang lại lợi ích nào thêm.
3. final với Lớp
Khi một lớp được khai báo là final, nó không thể bị kế thừa (extended) bởi bất kỳ lớp nào khác. Điều này thường được sử dụng cho các lớp tiện ích (utility classes) hoặc các lớp mà bạn muốn đảm bảo tính bất biến hoàn toàn, như lớp String trong Java hoặc các lớp wrapper cho kiểu nguyên thủy (Integer, Boolean, v.v.).
public final class DichVuTienIch {
private final String tenDichVu;
public DichVuTienIch(String ten) {
this.tenDichVu = ten;
}
public String layTenDichVu() {
return tenDichVu;
}
public final void thucHienCongViec() {
System.out.println("Dịch vụ " + tenDichVu + " đang thực hiện công việc.");
}
}
// class DichVuMoRong extends DichVuTienIch { // Lỗi biên dịch: không thể kế thừa từ lớp final
// // ...
// }
Trong một lớp final, tất cả các phương thức mặc định đã là final về mặt ý nghĩa (vì không có lớp con nào để ghi đè chúng). Do đó, việc khai báo các phương thức riêng lẻ bên trong một lớp final với từ khóa final là không cần thiết, mặc dù về mặt cú pháp là hợp lệ.