Được chình sửa ngày 02/02/2023.
Chào mừng các bạn đến với bài học Java số 26, bài học về Từ khóa Final. Bài học này nằm trong chuỗi bài học lập trình ngôn ngữ Java của Yellow Code Books.
Chắc bạn còn nhớ bài học về hằng số, khi đó bạn đã biết, để khai báo một hằng, bạn phải dùng từ khóa final. Sang đến các bài học về OOP, hằng số vẫn được dùng với ý nghĩa nguyên vẹn nếu như bạn khai báo một thuộc tính của lớp với từ khóa final này. Vậy thì, ngoài ý nghĩa là một hằng số ra, final còn làm được gì trong các mối quan hệ OOP này. Mời bạn cùng xem tiếp bài học hôm nay để hiểu rõ hơn về final nhé.
Biến final & Thuộc Tính final
Như mình có nói, từ khóa final khi dùng cho biến trong một phương thức, cũng có ý nghĩa tương tự như khi dùng từ khóa này với thuộc tính của lớp. Nó đều mang giá trị là một hằng số không thể thay đổi được.
Dưới đây là một ví dụ của việc khai báo final cho một biến, kiến thức cổ xưa này chắc bạn cũng đã nắm rất kỹ rồi.
protected float tinhChuVi() { final float PI = 3.14f; // Khai báo final cho biến return 2 * PI * banKinh; }
Còn về việc khai báo final cho một thuộc tính, thì cũng cổ xưa không kém, bạn đã từng làm quen rất nhiều khi khai báo thuộc tính hằng số PI trong lớp HinhTron ở các bài học trước. Tuy nhiên, bạn nên nhớ rằng, các khai báo khả năng truy cập vào các thuộc tính final hay không final là hoàn toàn tương tự nhau nhé.
public class HinhTron { private final float PI = 3.14f; // Khai báo final cho thuộc tính protected float banKinh; // Constructor public HinhTron(float banKinh) { this.banKinh = banKinh; } protected float tinhChuVi() { return 2 * PI * banKinh; } protected float tinhDienTich() { return PI * banKinh * banKinh; } }
Và như mình cũng có khuyên bạn khi nói về hằng số, rằng bạn nên viết hoa hết các ký tự biến hay thuộc tính với khai báo final, nó giúp code của chúng ta rõ ràng và mạch lạc hơn.
Thuộc Tính final Trống
Phần này mình muốn nói thêm về final khi dùng cho biến hay thuộc tính. Đó là khi bạn khai báo chúng là final, bạn không cần thiết phải chỉ định ngay giá trị cho thuộc tính hay biến đó, mà có thể để trống, để sau này khi có giá trị cụ thể, thì bạn gán vào các thuộc tính hay biến final đó sau, và chỉ gán một lần duy nhất thôi nhé. Điều này giúp cho final tưởng như rất cứng nhắc này lại trở nên linh động hơn một chút.
Ví dụ sau mô phỏng cho việc set giá trị cho biến final PI sau khi chúng ta tìm thấy một giá trị PI chính xác hơn bằng phương thức.
protected float tinhChuVi() { final float PI; // Khai báo hằng PI = tinhPI(); // Gán giá trị cho hằng sau khi có giá trị cụ thể return 2 * PI * banKinh; }
Còn ví dụ sau mô phỏng việc set giá trị cho thuộc tính final PI sau khi tìm thấy giá trị PI chính thức, nhưng hơi khác với ví dụ về biến trên kia một chút, rằng với thuộc tính final, thì bạn chỉ được phép gán giá trị sau cho final ở constructor mà thôi.
public class HinhTron { private final float PI; // Khai báo hằng protected float banKinh; // Constructor public HinhTron(float banKinh) { // Gán giá trị cho hằng ở constructor PI = (float) tinhPi(); this.banKinh = banKinh; } protected float tinhChuVi() { return 2 * PI * banKinh; } protected float tinhDienTich() { return PI * banKinh * banKinh; } // Bạn đừng quan tâm quá đến code của phương thức này private double tinhPi() { return Math.PI; } }
Phương Thức final
Cũng dễ dàng suy luận thôi. Nếu như với thuộc tính, khi bị chỉ định là final, thì có nghĩa là giá trị của thuộc tính đó sẽ không thể thay đổi được nữa. Thì với phương thức, khi bạn chỉ định final cho nó, thì xem như phương thức đó không thể override lại được, cũng tương đương với việc bạn không thay đổi được phương thức đó từ lớp con.
Quay lại lop HinhTron, giả sử các phương thức tinhChuVi() và tinhDienTich() đều được khai báo là final.
public class HinhTron { private final float PI = 3.14f; protected float banKinh; // Constructor public HinhTron(float banKinh) { this.banKinh = banKinh; } protected final float tinhChuVi() { return 2 * PI * banKinh; } protected final float tinhDienTich() { return PI * banKinh * banKinh; } }
Vậy thì ở lớp HinhTru kế thừa từ HinhTron, bạn có thể thấy rằng việc HinhTru sử dụng các phương thức tinhChuVi() và tinhDienTich() từ HinhTron là hoàn toàn bình thường.
public class HinhTru extends HinhTron { public float chieuCao; // Constructor public HinhTru(float banKinh, float chieuCao) { super(banKinh); this.chieuCao = chieuCao; } public float tinhTheTich() { return tinhDienTich() * chieuCao; } }
Nhưng như mình có nói, lỗi chỉ thực sự xảy ra khi HinhTru override lại một phương thức final nào đó từ HinhTron mà thôi. Ví dụ như nếu bạn xây dựng thêm em này ở HinhTru thì hệ thống mới báo lỗi.
@Override protected float tinhChuVi() { return super.tinhChuVi(); }
Với việc thiết lập một phương thức là final này, như bạn đã biết, các lập trình viên sẽ tránh không cho các lớp khác kế thừa từ phương thức nào đó của một lớp. Việc làm này vẫn giúp chia sẻ phương thức đó cho các lớp con kế thừa (miễn đừng khai báo private), nhưng sẽ không cho phép bất cứ sự sửa chữa nào, đảm bảo sự nguyên vẹn ban đầu của phương thức. Bạn đã thấy sự vi diệu của OOP chưa nào.
Vâng, bạn có thể chỉ định final cho bất cứ phương thức nào. Nhưng, bạn nên lưu ý rằng, bạn không thể chỉ định final cho một constructor nhé, vì constructor có bao giờ bị override đâu nào.
Lớp final
Tác dụng của final đến với lớp tương tự như tác dụng của em ấy đến với phương thức. Đó là bạn sẽ không thể kế thừa từ bất cứ lớp nào được khai báo là final.
Giả sử lớp HinhTron có khai báo final cho lớp như thế này, thì lớp HinhTru kế thừa từ HinhTron mà chúng ta đã biết sẽ báo lỗi.
final class HinhTron { // ... }
Nếu bạn xuất bản một lớp, và chỉ cho người khác sử dụng chúng mà thôi, không cho ai có quyền kế thừa để override bất kỳ phương thức nào, thì cứ đặt lớp đó là final nhé.
Có nhiều lớp được dựng sẵn trong Java mà bạn đã biết được khai báo là final. Chẳng hạn lớp String. Với việc khai báo final cho String thì bạn không thể tạo ra một lớp con nào khác của String, bạn chỉ việc dùng String mà thôi.
public final class String implements java.io.Serializable, Comparable<java.lang.String>, CharSequence, Constable, ConstantDesc { // ... }
Kết Luận
Chúng ta kết thúc bài học final nhẹ nhàng ở đây. Tuy nhiên bài học khá hữu dụng trong việc vận dụng một từ khóa final để mang lại một số ý nghĩa về bảo vệ thông tin của một đối tượng. Nó sẽ hữu ích khi bạn muốn xây dựng các gói thư viện Java và xuất bản nó cho nhiều người dùng. Khi đó, việc cho phép một lớp khác có quyền kế thừa, hay override một phương thức nào đó của lớp hay không, là quyền ở bạn, như lớp String mà mình có giới thiệu trong bài học.
Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy ủng hộ blog bằng cách:
– Đánh giá 5 sao ở mỗi bài viết nếu thấy thích.
– Comment bên dưới mỗi bài viết nếu có thắc mắc.
– Để lại địa chỉ email của bạn ở thanh bên phải để nhận được thông báo sớm nhất khi có bài viết mới.
– Chia sẻ các bài viết của Yellow Code Books đến nhiều người khác.
– Ủng hộ blog theo hướng dẫn ở thanh bên phải để blog ngày càng phát triển hơn.
Bài Kế Tiếp
Chúng ta sẽ nói thêm về việc gói ghém, hay bảo vệ dữ liệu trong các lớp của Java qua các phương thức getter và setter nhé.
phương thức final thì có thể được kế thừa nhưng không thể override được phải không anh?
Đúng rồi bạn
e nghĩ ở mỗi bài học a nên cố gắng xen thêm bài tập vận dụng vào thì sẽ hay hơn ạ, còn bài viết thì quá tuyệt rồi, không còn gì để chê. Cám ơn anh
Cảm ơn bạn đã đóng góp, mình sẽ cân nhắc và cố gắng viết thật tốt nhất có thể nhé.
Anh ơi anh viết bài hay quá ạ. Đọc xong mà em thông não nhiều ghê á.
Anh viết hay quá
Khó hiểu quá