CodeGym /Blog Java /Ngẫu nhiên /API phản chiếu: Phản chiếu. Mặt tối của Java

API phản chiếu: Phản chiếu. Mặt tối của Java

Xuất bản trong nhóm
Xin chào, Padawan trẻ tuổi. Trong bài viết này, tôi sẽ kể cho bạn nghe về Thần lực, một sức mạnh mà các lập trình viên Java chỉ sử dụng trong những tình huống dường như không thể. Mặt tối của Java là Reflection API. Trong Java, sự phản chiếu được triển khai bằng cách sử dụng Java Reflection API.

Phản ánh Java là gì?

Có một định nghĩa ngắn gọn, chính xác và phổ biến trên Internet. Reflection ( từ tiếng Latin có nghĩa là reflexio - quay ngược lại ) là một cơ chế để khám phá dữ liệu về một chương trình khi nó đang chạy. Sự phản chiếu cho phép bạn khám phá thông tin về các trường, phương thức và hàm tạo của lớp. Phản chiếu cho phép bạn làm việc với các loại không có trong thời gian biên dịch, nhưng có sẵn trong thời gian chạy. Phản ánh và một mô hình nhất quán hợp lý để đưa ra thông tin lỗi giúp tạo mã động chính xác. Nói cách khác, việc hiểu cách hoạt động của sự phản chiếu trong Java sẽ mở ra một số cơ hội tuyệt vời cho bạn. Bạn thực sự có thể sắp xếp các lớp và các thành phần của chúng. Dưới đây là danh sách cơ bản về những gì sự phản chiếu cho phép:
  • Tìm hiểu/xác định lớp của đối tượng;
  • Nhận thông tin về các công cụ sửa đổi, trường, phương thức, hằng số, hàm tạo và siêu lớp của một lớp;
  • Tìm ra phương thức nào thuộc về (các) giao diện đã triển khai;
  • Tạo một thể hiện của một lớp có tên lớp không xác định cho đến khi chạy;
  • Nhận và đặt giá trị cho các trường của đối tượng theo tên;
  • Gọi một phương thức của đối tượng theo tên.
Sự phản chiếu được sử dụng trong hầu hết các công nghệ Java hiện đại. Thật khó để tưởng tượng rằng Java, với tư cách là một nền tảng, có thể đạt được sự chấp nhận rộng rãi như vậy mà không cần phản ánh. Nhiều khả năng, nó sẽ không có. Bây giờ bạn đã quen với sự phản chiếu như một khái niệm lý thuyết, hãy chuyển sang ứng dụng thực tế của nó! Chúng ta sẽ không tìm hiểu tất cả các phương pháp của Reflection API—chỉ những phương pháp mà bạn sẽ thực sự gặp phải trong thực tế. Vì phản xạ liên quan đến làm việc với các lớp, nên chúng ta sẽ bắt đầu với một lớp đơn giản gọi là MyClass:

public class MyClass {
   private int number;
   private String name = "default";
//    public MyClass(int number, String name) {
//        this.number = number;
//        this.name = name;
//    }
   public int getNumber() {
       return number;
   }
   public void setNumber(int number) {
       this.number = number;
   }
   public void setName(String name) {
       this.name = name;
   }
   private void printData(){
       System.out.println(number + name);
   }
}
Như bạn có thể thấy, đây là một lớp rất cơ bản. Hàm tạo với các tham số được nhận xét có chủ ý. Chúng ta sẽ quay lại vấn đề đó sau. Nếu bạn xem kỹ nội dung của lớp, bạn có thể nhận thấy sự vắng mặt của getter cho trường tên . Bản thân trường tên được đánh dấu bằng công cụ sửa đổi truy cập riêng tư : chúng ta không thể truy cập nó bên ngoài chính lớp đó, điều đó có nghĩa là chúng ta không thể truy xuất giá trị của nó. " Vậy vấn đề là gì ?" bạn nói. "Thêm trình thu thập hoặc thay đổi công cụ sửa đổi quyền truy cập". Và bạn sẽ đúng, trừ khiMyClassnằm trong thư viện AAR đã biên dịch hoặc trong một mô-đun riêng khác không có khả năng thực hiện thay đổi. Trong thực tế, điều này xảy ra mọi lúc. Và một số lập trình viên bất cẩn chỉ đơn giản là quên viết getter . Đây là thời gian để nhớ phản ánh! Hãy thử truy cập vào trường tên riêng của lớp MyClass:

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; // No getter =(
   System.out.println(number + name); // Output: 0null
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(number + name); // Output: 0default
}
Hãy phân tích những gì vừa xảy ra. Trong Java, có một lớp tuyệt vời được gọi là Class. Nó đại diện cho các lớp và giao diện trong một ứng dụng Java có thể thực thi được. Chúng tôi sẽ không đề cập đến mối quan hệ giữa ClassClassLoader, vì đó không phải là chủ đề của bài viết này. Tiếp theo, để lấy các trường của lớp này, bạn cần gọi getFields()phương thức. Phương thức này sẽ trả về tất cả các trường có thể truy cập của lớp này. Điều này không hiệu quả với chúng tôi, vì trường của chúng tôi là riêng tư , vì vậy chúng tôi sử dụng getDeclaredFields()phương pháp này. Phương thức này cũng trả về một mảng các trường lớp, nhưng hiện tại nó bao gồm các trường riêng tưđược bảo vệ . Trong trường hợp này, chúng tôi biết tên của trường mà chúng tôi quan tâm, vì vậy chúng tôi có thể sử dụng phương getDeclaredField(String)thức, trong đóStringlà tên trường mong muốn. Ghi chú: getFields()getDeclaredFields()không trả về các trường của lớp cha! Tuyệt vời. Chúng tôi có một Fieldđối tượng đề cập đến tên của chúng tôi . Vì trường này không công khai nên chúng tôi phải cấp quyền truy cập để làm việc với nó. Phương pháp này setAccessible(true)cho phép chúng tôi tiến xa hơn. Bây giờ trường tên nằm dưới sự kiểm soát hoàn toàn của chúng tôi! Bạn có thể truy xuất giá trị của nó bằng cách gọi phương thức Fieldcủa đối tượng get(Object), trong đó Objectlà một thể hiện của MyClasslớp chúng ta. Ta chuyển kiểu sang Stringvà gán giá trị cho biến name của mình . Nếu chúng tôi không thể tìm thấy trình thiết lập để đặt giá trị mới cho trường tên, bạn có thể sử dụng phương thức đặt :

field.set(myClass, (String) "new value");
Chúc mừng! Bạn vừa nắm vững kiến ​​thức cơ bản về phản chiếu và truy cập vào một trường riêng tư ! Hãy chú ý đến try/catchkhối và các loại ngoại lệ đang được xử lý. IDE sẽ tự cho bạn biết sự hiện diện của họ là bắt buộc, nhưng bạn có thể nói rõ ràng bằng tên của họ tại sao họ ở đây. Tiến lên! Như bạn có thể nhận thấy, MyClasslớp của chúng ta đã có một phương thức để hiển thị thông tin về dữ liệu của lớp:

private void printData(){
       System.out.println(number + name);
   }
Nhưng lập trình viên này cũng để lại dấu vân tay của mình ở đây. Phương thức này có một công cụ sửa đổi truy cập riêng và chúng tôi phải viết mã của riêng mình để hiển thị dữ liệu mỗi lần. Thật là một mớ hỗn độn. Hình ảnh phản chiếu của chúng ta đã đi đâu? Viết hàm sau:

public static void printData(Object myClass){
   try {
       Method method = myClass.getClass().getDeclaredMethod("printData");
       method.setAccessible(true);
       method.invoke(myClass);
   } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
       e.printStackTrace();
   }
}
Quy trình ở đây gần giống với quy trình được sử dụng để truy xuất một trường. Chúng tôi truy cập phương thức mong muốn theo tên và cấp quyền truy cập cho nó. Và trên Methodđối tượng mà chúng ta gọi là invoke(Object, Args)phương thức, đâu Objectcũng là một thể hiện của MyClasslớp. Argslà các đối số của phương thức, mặc dù đối số của chúng tôi không có bất kỳ đối số nào. Bây giờ chúng ta sử dụng printDatahàm để hiển thị thông tin:

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // Output: 0default
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       field.set(myClass, (String) "new value");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// Output: 0new value
}
Tiếng hoan hô! Bây giờ chúng ta có quyền truy cập vào phương thức riêng tư của lớp. Nhưng điều gì sẽ xảy ra nếu phương thức này có đối số và tại sao hàm tạo lại nhận xét? Tất cả mọi thứ trong thời gian của riêng mình. Rõ ràng từ định nghĩa ở phần đầu, sự phản chiếu cho phép bạn tạo các thể hiện của một lớp trong thời gian chạy (trong khi chương trình đang chạy)! Chúng ta có thể tạo một đối tượng bằng cách sử dụng tên đầy đủ của lớp. Tên đầy đủ của lớp là tên lớp, bao gồm cả đường dẫn của gói của nó .
API phản chiếu: Phản chiếu.  Mặt tối của Java - 2
Trong hệ thống phân cấp gói của tôi , tên đầy đủ của MyClass sẽ là "Reflection.MyClass". Ngoài ra còn có một cách đơn giản để tìm hiểu tên của lớp (trả về tên của lớp dưới dạng Chuỗi):

MyClass.class.getName()
Hãy sử dụng phản xạ Java để tạo một thể hiện của lớp:

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       myClass = (MyClass) clazz.newInstance();
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(myClass); // Output: created object reflection.MyClass@60e53b93
}
Khi một ứng dụng Java khởi động, không phải tất cả các lớp đều được tải vào JVM. Nếu mã của bạn không tham chiếu đến MyClasslớp, thì ClassLoader, chịu trách nhiệm tải các lớp vào JVM, sẽ không bao giờ tải lớp đó. Điều đó có nghĩa là bạn phải buộc ClassLoadertải nó và nhận được mô tả lớp dưới dạng một Classbiến. Đây là lý do tại sao chúng ta có forName(String)phương thức, đâu Stringlà tên của lớp mà chúng ta cần mô tả. Sau khi nhận được Сlassđối tượng, việc gọi phương thức newInstance()sẽ trả về một Objectđối tượng được tạo bằng mô tả đó. Tất cả những gì còn lại là cung cấp đối tượng này cho chúng taMyClasslớp học. Mát mẻ! Điều đó thật khó, nhưng tôi hy vọng có thể hiểu được. Bây giờ chúng ta có thể tạo một thể hiện của một lớp theo đúng nghĩa đen một dòng! Thật không may, cách tiếp cận được mô tả sẽ chỉ hoạt động với hàm tạo mặc định (không có tham số). Làm thế nào để bạn gọi các phương thức và hàm tạo có tham số? Đã đến lúc bỏ ghi chú hàm tạo của chúng ta. Như mong đợi, newInstance()không thể tìm thấy hàm tạo mặc định và không còn hoạt động nữa. Hãy viết lại phần khởi tạo lớp:

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       Class[] params = {int.class, String.class};
       myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);// Output: created object reflection.MyClass@60e53b93
}
Phương getConstructors()thức nên được gọi trên định nghĩa lớp để lấy các hàm tạo của lớp, và sau đó getParameterTypes()nên được gọi để lấy các tham số của hàm tạo:

Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
Điều đó giúp chúng ta có tất cả các hàm tạo và tham số của chúng. Trong ví dụ của tôi, tôi đề cập đến một hàm tạo cụ thể với các tham số cụ thể, đã biết trước đó. Và để gọi hàm tạo này, chúng tôi sử dụng newInstancephương thức mà chúng tôi chuyển các giá trị của các tham số này. Nó sẽ giống nhau khi sử dụng invokeđể gọi các phương thức. Điều này đặt ra câu hỏi: khi nào việc gọi hàm tạo thông qua sự phản chiếu có ích? Như đã đề cập ở phần đầu, các công nghệ Java hiện đại không thể tồn tại nếu không có Java Reflection API. Ví dụ: Dependency Injection (DI), kết hợp các chú thích với sự phản ánh của các phương thức và hàm tạo để tạo thành Darer phổ biếnthư viện để phát triển Android. Sau khi đọc bài viết này, bạn có thể tự tin cho rằng mình đã được đào tạo theo cách của Java Reflection API. Họ không vô cớ gọi sự phản chiếu là mặt tối của Java. Nó hoàn toàn phá vỡ mô hình OOP. Trong Java, đóng gói ẩn và hạn chế quyền truy cập của người khác vào các thành phần chương trình nhất định. Khi chúng tôi sử dụng công cụ sửa đổi riêng tư, chúng tôi dự định rằng trường đó chỉ được truy cập từ bên trong lớp nơi nó tồn tại. Và chúng tôi xây dựng kiến ​​trúc tiếp theo của chương trình dựa trên nguyên tắc này. Trong bài viết này, chúng ta đã thấy cách bạn có thể sử dụng sự phản chiếu để định hướng theo cách của bạn ở bất kỳ đâu. Mẫu thiết kế sáng tạo Singletonlà một ví dụ tốt về điều này như một giải pháp kiến ​​trúc. Ý tưởng cơ bản là một lớp triển khai mẫu này sẽ chỉ có một thể hiện trong quá trình thực hiện toàn bộ chương trình. Điều này được thực hiện bằng cách thêm công cụ sửa đổi truy cập riêng vào hàm tạo mặc định. Và sẽ rất tệ nếu một lập trình viên sử dụng sự phản chiếu để tạo ra nhiều thể hiện của các lớp như vậy. Nhân tiện, gần đây tôi có nghe một đồng nghiệp hỏi một câu hỏi rất thú vị: liệu một lớp triển khai mẫu Singleton có thể được kế thừa không? Có thể nào, trong trường hợp này, ngay cả phản xạ cũng bất lực? Để lại phản hồi của bạn về bài viết và câu trả lời của bạn trong các bình luận bên dưới và đặt câu hỏi của riêng bạn ở đó!
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION