Java: Lớp lồng nhau (Nested Classes)
Java cho phép định nghĩa một lớp bên trong một lớp khác. Một lớp như vậy được gọi là lớp lồng nhau như trong hình sau:
Hình 1: Lớp lồng nhau
Đoạn mã 1 định nghĩa một lớp lồng nhau.
Đoạn mã 1:
class Outer{ ... class Nested{ ... } }
Lớp Outer là lớp bao bọc bên ngoài và lớp Nested là lớp được định nghĩa trong lớp Outer.
Các lớp lồng nhau được phân loại là tĩnh và không tĩnh. Các lớp lồng nhau được khai báo tĩnh đơn giản được gọi là các lớp lồng nhau tĩnh (static) trong khi các lớp lồng nhau không tĩnh được gọi là các lớp bên trong (inner). Điều này đã được thể hiện trong Đoạn mã 2.
Đoạn mã 2:
class Outer{ ... static class StaticNested{ ... } class Inner{ ... } }
Lưu ý rằng lớp StaticNested là một lớp lồng nhau đã được khai báo là tĩnh trong khi lớp lồng nhau không tĩnh, Inner, được khai báo mà không có từ khóa static.
Một lớp lồng nhau là một thành viên của lớp bao quanh nó. Lưu ý rằng các lớp lồng nhau không tĩnh hoặc các lớp bên trong có thể truy cập các thành viên của lớp bao quanh ngay cả khi chúng được khai báo là private. Mặt khác, các lớp lồng nhau tĩnh không thể truy cập bất kỳ thành viên nào khác của lớp bao quanh. Là một thành viên của lớp ngoài, một lớp lồng nhau có thể có bất kỳ định nghĩa truy cập nào như public, private, protected hoặc default (gói private).
Lợi ích của việc sử dụng lớp lồng nhau
Lý do giới thiệu tính năng thuận lợi này của việc xác định lớp lồng nhau trong Java như sau:
- Tạo nhóm các lớp một cách hợp lý : Nếu một lớp chỉ được sử dụng cho một lớp, thì nó có thể được nhúng vào trong lớp đó và hai lớp có thể được giữ cùng nhau. Nói cách khác, nó giúp nhóm các chức năng liên quan lại với nhau. Việc lồng các 'lớp trợ giúp' như vậy giúp gói hiệu quả và hợp lý hơn.
- Tăng tính đóng gói : Trong trường hợp hai lớp cấp cao nhất như lớp A và B, khi B muốn truy cập vào các thành viên của A là private, thì lớp B có thể được lồng trong lớp A để B có thể truy cập các thành viên được khai báo là private. Ngoài ra, điều này sẽ ẩn lớp B với thế giới bên ngoài. Do đó, nó giúp truy cập tất cả các thành viên của lớp bao quanh cấp cao nhất ngay cả khi chúng được khai báo là riêng tư.
- Tăng khả năng đọc và khả năng bảo trì của mã : Việc lồng các lớp nhỏ vào các lớp cấp cao hơn giúp đặt mã gần hơn với nơi nó sẽ được sử dụng.
Các loại lớp lồng nhau khác nhau như sau:
- Các lớp thành viên hoặc các lớp lồng nhau không tĩnh
- Các lớp cục bộ (local)
- Các lớp Ẩn danh (anonymous)
- Các lớp lồng nhau tĩnh
Các lớp thành viên
Một lớp thành viên là một lớp bên trong không tĩnh. Nó được khai báo như một thành viên của lớp bên ngoài hoặc lớp bao quanh. Lớp thành viên không thể có sửa đổi tĩnh vì nó được liên kết với các thể hiện của lớp bên ngoài. Một lớp bên trong có thể truy cập trực tiếp vào tất cả các thành viên, các trường và phương thức của lớp bên ngoài bao gồm cả các thành viên riêng. Tuy nhiên, ngược lại thì đúng. Nghĩa là, lớp ngoài không thể truy cập trực tiếp vào các thành viên của lớp bên trong ngay cả khi chúng được khai báo là public. Điều này là do các thành viên của lớp bên trong được khai báo trong phạm vi của lớp bên trong.
Một lớp bên trong có thể được khai báo là public, private, protected, abstract hoặc final. Các thể hiện của một lớp bên trong tồn tại trong một thể hiện của lớp bên ngoài. Để khởi tạo một lớp bên trong, người ta phải tạo một thể hiện của lớp bên ngoài. Sau đó, người ta có thể truy cập đối tượng lớp bên trong bên trong đối tượng lớp bên ngoài bằng cách sử dụng câu lệnh được định nghĩa như trong Đoạn mã 3.
Đoạn mã 3:
// Truy cập vào lớp bên trong sử dụng đối tượng lớp bên ngoài Outer.Inner objInner = objOuter.new Inner();
Đoạn mã 4 mô tả một ví dụ về lớp bên trong không tĩnh.
Đoạn mã 4:
class Server { String port; // variable to store port number /** * Connects to specified server * * @param IP a String variable storing IP address of server * @param port a String variable storing port number of server * @return void */ public void connectServer(String IP, String port) { System.out.println("Connecting to Server at:" + IP + ":" + port); } /** * Define an inner class */ class IPAddress { /** * Returns the IP address of a server * * @return String */ String getIP() { return "101.232 .28 .12"; } } } /** * Define the class TestConnection.java */ class TestConnection { /** * @param args the command line arguments */ public static void main(String[] args) { /** * @param args the command line arguments */ // Check the number of command line arguments if (args.length == 1) { // Instantiate the outer class Server objServer1 = new Server(); // Instantiate the inner class using outer class object Server.IPAddress objIP = objServer1.new IPAddress(); // Invoke the connectServer() method with the IP returned from // the getIP() method of the inner class objServer1.connectServer(objIP.getIP(), args[0]); } else { System.out.println("Usage:java Server <port - no >"); } } }
Lớp Server là một lớp bên ngoài bao gồm một biến port đại diện cho cổng mà máy chủ sẽ được kết nối. Ngoài ra, phương thức connectServer(String, String) chấp nhận địa chỉ IP và số cổng làm tham số. Lớp IPAddress bên trong bao gồm phương thức getIP() trả về địa chỉ IP của máy chủ.
Phương thức main() được định nghĩa trong lớp TestConnection. Trong phương thức main(), số lượng đối số được xác minh và theo đó đối tượng của lớp Server được tạo. Tiếp theo, đối tượng objServer1 được sử dụng để tạo đối tượng objIP của lớp bên trong IPAddress. Cuối cùng, đối tượng lớp ngoài được sử dụng để gọi phương thức connectServer(). Địa chỉ IP được truy xuất bằng câu lệnh objIP.getIP() . Hình 2 cho ta output của mã khi người dùng cung cấp '8080' làm số cổng tại dòng lệnh.
Hình 2: Output của Lớp Server.java bằng Lớp Bên trong
3. Lớp cục bộ (local)
Một lớp bên trong được định nghĩa trong một khối mã chẳng hạn như phần thân của một phương thức, hàm tạo hoặc bộ khởi tạo, được gọi là lớp bên trong cục bộ. Phạm vi của một lớp bên trong cục bộ chỉ nằm trong khối cụ thể đó. Không giống như lớp bên trong, lớp bên trong cục bộ không phải là thành viên của lớp bên ngoài và do đó, nó không thể có bất kỳ định nghĩa truy cập nào. Có nghĩa là, nó không thể sử dụng các bổ ngữ như public, protected, private hoặc static. Tuy nhiên, nó có thể truy cập tất cả các thành viên của lớp bên ngoài cũng như các biến final được khai báo trong phạm vi mà nó được định nghĩa.
Hình 3: hiển thị một lớp bên trong cục bộ.
Lớp nội bộ cục bộ có các tính năng sau:
- Nó được liên kết với một thể hiện của lớp bao quanh.
- Nó có thể truy cập bất kỳ thành viên nào, bao gồm cả các thành viên private, của lớp bao quanh.
- Nó có thể truy cập bất kỳ biến cục bộ, tham số phương thức hoặc tham số ngoại lệ nào nằm trong phạm vi của định nghĩa phương thức cục bộ, miễn là chúng được khai báo là final.
Đoạn mã 5 trình bày một ví dụ về lớp bên trong cục bộ.
Đoạn mã 5:
class Employee { /** * Evaluates employee status * * @param empID a String variable storing employee ID * @param empAge an integer variable storing employee age * @return void */ public void evaluateStatus(String empID, int empAge) { // local final variable final int age = 40; /** * Local inner class Rank * */ class Rank { /** * Returns the rank of an employee * * @param empID a String variable that stores the employee ID * @return char */ public char getRank(String empID) { System.out.println("Getting Rank of employee: " + empID); // assuming that rank ‘A’ was returned from server return 'A'; } } // Check the specified age if (empAge >= age) { // Instantiate the Rank class Rank objRank = new Rank(); // Retrieve the employee’s rank char rank = objRank.getRank(empID); // Verify the rank value if (rank == 'A') { System.out.println("Employee rank is:" + rank); System.out.println("Status:Eligible for upgrade"); } else { System.out.println("Status:Not Eligible for upgrade"); } } else { System.out.println("Status:Not Eligible for upgrade"); } } } /** * Define the class TestEmployee.java */ public class TestEmployee { /** * @param args the command line arguments */ public static void main(String[] args) { if (args.length == 2) { // Object of outer class Employee objEmp1 = new Employee(); // Invoke the evaluateStatus() method objEmp1.evaluateStatus(args[0], Integer.parseInt(args[1])); } else { System.out.println("Usage:java Employee <Emp - Id > <Age >"); } } }
Lớp Employee là lớp ngoài cùng với một phương thức có tên là evalStatus(String, int). Phương thức bao gồm một biến cục bộ final có tên là age. Lớp Rank là một lớp bên trong cục bộ trong phương thức. Lớp Rank bao gồm một phương thức getRank() trả về thứ hạng của Id nhân viên được chỉ định.
Tiếp theo, nếu tuổi của nhân viên lớn hơn 40, đối tượng của lớp Rank được tạo và cấp bậc được truy xuất. Nếu cấp bậc bằng 'A' thì nhân viên đó đủ điều kiện để nâng hạng, nếu không thì nhân viên đó không đủ điều kiện.
Phương thức main() được định nghĩa trong lớp TestEaffee. Trong phương thức main(), một đối tượng objEmp1 của lớp Employee được tạo và phương thức evalStatus() được gọi với các đối số thích hợp.
Hình 4 cho ta output của đoạn code khi người dùng truyền 'E001' làm Id nhân viên và 50 cho độ tuổi.
Hình 4: Output của lớp Employee.java sử dụng lớp bên trong cục bộ
4. Lớp ẩn danh
Một lớp bên trong được khai báo không có tên trong một khối mã chẳng hạn như phần thân của một phương thức được gọi là lớp bên trong ẩn danh. Vì một lớp ẩn danh không có tên được liên kết, nó chỉ có thể được truy cập tại điểm mà nó được định nghĩa.
Lớp ẩn danh là một loại lớp cục bộ không thể sử dụng các từ khóa extends và implements cũng như không thể chỉ định bất kỳ công cụ sửa đổi truy cập nào, chẳng hạn như public, private, protected và static. Nó không thể định nghĩa phương thức khởi tạo, trường tĩnh, phương thức hoặc lớp. Ngoài ra, nó không thể thực thi các interface ẩn danh vì một interface không thể được thực thi mà không có tên.
Vì một lớp ẩn danh không có tên, nên nó không thể có một phương thức khởi tạo được đặt tên nhưng nó có thể có một bộ khởi tạo thể hiện. Các quy tắc để truy cập một lớp ẩn danh cũng giống như quy tắc của lớp bên trong cục bộ.
Thông thường, lớp ẩn danh là một sự thực thi của lớp cha hoặc interface của nó và chứa việc triển khai các phương thức. Các lớp bên trong ẩn danh có phạm vi giới hạn đối với lớp bên ngoài. Chúng có thể truy cập các thành viên nội bộ hoặc private và các phương thức của lớp bên ngoài. Lớp ẩn danh hữu ích cho việc truy cập có kiểm soát vào các chi tiết nội bộ của lớp khác. Ngoài ra, nó hữu ích khi người dùng chỉ muốn một thể hiện của một lớp đặc biệt.
Hình 5: hiển thị một lớp ẩn danh.
Đoạn mã 6 mô tả một ví dụ về lớp ẩn danh.
Đoạn mã 6:
class Authenticate { /** * Define an anonymous class */ Account objAcc = new Account() { /** * Displays balance * @param accNo a String variable storing balance * @return void */ @Override public void displayBalance(String accNo) { System.out.println("Retrieving balance. Please wait..."); // Assume that the server returns 40000 System.out.println("Balance of account number " + accNo.toUpperCase() + " is $40000"); } }; // End of anonymous class } /** * Define the Account class */ class Account { /** * Displays balance * * @param accNo a String variable storing balance * @return void */ public void displayBalance(String accNo) { } } /** * Define the TestAuthentication class */ public class TestAuthentication { /** * @param args the command line arguments */ public static void main(String[] args) { // Instantiate the Authenticate class Authenticate objUser = new Authenticate(); // Check the number of command line arguments if (args.length == 3) { if (args[0].equals("admin") && args[1].equals("abc@123")) { // Invoke the displayBalance() method objUser.objAcc.displayBalance(args[2]); } else { System.out.println("Unauthorized user"); } } else { System.out.println("Usage: java Authenticate <user-name> <password> < account - no > "); } } }
Lớp Authenticate bao gồm một đối tượng ẩn danh kiểu Account. Phương thức displayBalance(String) được sử dụng để truy xuất và hiển thị số dư của số tài khoản được chỉ định.
Phương thức main() được định nghĩa trong lớp TestAuthentication. Trong phương thức main(), một đối tượng của lớp Authenticate được tạo. Số lượng đối số dòng lệnh được xác minh. Nếu số lượng đối số là đúng, tên người dùng và mật khẩu được xác minh và theo đó đối tượng của lớp ẩn danh được sử dụng để gọi phương thức displayBalance() với số tài khoản làm đối số.
Hình 6 cho ta output khi người dùng truyền 'admin', 'abc@123' và 'akdle26152' làm đối số.
Hình 6: Output của lớp Authenticate.java sử dụng lớp ẩn danh
5. Lớp lồng nhau tĩnh (static)
Một lớp lồng nhau tĩnh được liên kết với lớp ngoài giống như các biến và phương thức. Một lớp lồng nhau tĩnh không thể tham chiếu trực tiếp đến các biến thể hiện hoặc các phương thức của lớp bên ngoài giống như các phương thức tĩnh mà chỉ có thể truy cập thông qua một tham chiếu đối tượng.
Một lớp lồng nhau tĩnh, theo hành vi, là một lớp cấp cao nhất đã được lồng trong một lớp cấp cao nhất khác để tiện đóng gói.
Các lớp lồng nhau tĩnh được truy cập bằng cách sử dụng tên lớp đủ điều kiện, đó là OuterClass.StaticNestedClass. Một lớp lồng nhau tĩnh có thể có các mã định nghĩa truy cập public, protected, private, default hoặc gói riêng tư, final và abstract.
Đoạn mã 7 trình bày việc sử dụng lớp lồng nhau tĩnh.
Đoạn mã 7:
import java.util.Calendar; class AtmMachine { /** * Define the static nested class */ static class BankDetails { // Instantiate the Calendar class of java.util package static Calendar objNow = Calendar.getInstance(); /** * Displays the bank and transaction details * * @return void */ public static void printDetails() { System.out.println("State Bank of America"); System.out.println("Branch: New York"); System.out.println("Code: K3983LKSIE"); // retrieving current date and time using Calendar object System.out.println("Date-Time:" + objNow.getTime()); } } /** * Displays balance * * @param accNo a String variable that stores the account number * @return void */ public void displayBalance(String accNo) { // Assume that the server returns 200000 System.out.println("Balance of account number " + accNo.toUpperCase() + " is $200000"); } } /** * Define the TestATM class */ public class TestATM { /** * @param args the command line arguments */ public static void main(String[] args) { if (args.length == 1) { // verifying number of command line arguments // Instantiate the outer class AtmMachine objAtm = new AtmMachine(); // Invoke the static nested class method using outer class object AtmMachine.BankDetails.printDetails(); // Invoke the instance method of outer class objAtm.displayBalance(args[0]); } else { System.out.println("Usage: java AtmMachine <account-no>"); } } }
Lớp AtmMachine bao gồm một lớp lồng nhau tĩnh có tên là BankDetails. Lớp lồng nhau tĩnh tạo một đối tượng của lớp Calendar của gói java.util. Lớp Lịch bao gồm các phương thức cài sẵn để đặt hoặc truy xuất ngày và giờ hệ thống. Phương thức printDetails() được sử dụng để in chi tiết ngân hàng cùng với ngày và giờ hiện tại bằng phương thức getTime() của lớp Calendar.
Phương thức main() được định nghĩa trong lớp TestATM. Trong phương thức main(), số lượng đối số dòng lệnh được xác minh và theo đó một đối tượng của lớp AtmMachine được tạo. Tiếp theo, phương thức tĩnh printDetails() của lớp BankDetails lồng nhau được gọi và truy cập trực tiếp bằng cách sử dụng tên lớp của lớp ngoài AtmMachine. Cuối cùng, đối tượng objAtm của lớp ngoài được sử dụng để gọi phương thức displayBalance() của lớp ngoài với số tài khoản làm đối số.
Hình 7 cho ta output khi người dùng truyền 'akdle26152' làm số tài khoản.
Hình 7: Output của lớp AtmMachine.java sử dụng lớp lồng nhau tĩnh
Lưu ý rằng đầu ra của ngày và giờ hiển thị định dạng mặc định như được chỉ định trong việc triển khai phương thức getTime(). Định dạng có thể được sửa đổi theo yêu cầu của người dùng bằng cách sử dụng lớp SimpleDateFormat của gói java.text. SimpleDateFormat là một lớp cụ thể được sử dụng để định dạng và phân tích cú pháp ngày tháng theo cách địa phương cụ thể. Lớp SimpleDateFormat cho phép chỉ định các mẫu do người dùng xác định để định dạng ngày-giờ. Lớp BankDetails đã sửa đổi bằng cách sử dụng lớp SimpleDateFormat được hiển thị trong Đoạn mã 8.
Đoạn mã 8:
import java.text.SimpleDateFormat; import java.util.Calendar; class AtmMachine { /** * Define the static nested class */ static class BankDetails { // Instantiate the Calendar class of java.util package static Calendar objNow = Calendar.getInstance(); /** * Displays the bank and transaction details * * @return void */ public static void printDetails() { System.out.println("State Bank of America"); System.out.println("Branch: New York"); System.out.println("Code: K3983LKSIE"); // Format the output of date-time using SimpleDateFormat class SimpleDateFormat objFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"); // Retrieve the current date and time using Calendar object System.out.println("Date-Time:" + objFormat.format(objNow.getTime())); } } … … }
Phương thức khởi tạo lớp SimpleDateFormat nhận mẫu ngày tháng như một String. Trong Đoạn mã 8, mẫu 'dd / MM / yyyy HH: mm: ss' sử dụng một số ký hiệu là các chữ cái mẫu được lớp SimpleDateFormat nhận dạng. Bảng 1 liệt kê các mẫu tự được sử dụng trong đoạn mã cùng với mô tả của chúng.
Mẫu chữ |
Sự miêu tả |
---|---|
d |
Ngày trong tháng |
M |
Thang của năm |
y |
Năm |
H |
Giờ trong ngày (0-23) |
m |
Phút của một giờ |
s |
Giây của một phút |
Bảng 1: Các mẫu tự ngày tháng
Hình 8 cho ta kết quả của đoạn mã đã sửa đổi.
Hình 8: Output của lớp AtmMachine.java được sửa đổi sử dụng SimpleDateFormat
Lưu ý rằng ngày và giờ hiện được hiển thị ở định dạng được chỉ định sẽ dễ hiểu hơn cho người dùng.