JavaRush /Blog Java /Random-VI /Phản ánh trong Java - Ví dụ sử dụng

Phản ánh trong Java - Ví dụ sử dụng

Xuất bản trong nhóm
Có thể bạn đã từng gặp khái niệm “phản ánh” trong cuộc sống hàng ngày. Thông thường từ này đề cập đến quá trình nghiên cứu bản thân. Trong lập trình, nó có ý nghĩa tương tự - đó là một cơ chế kiểm tra dữ liệu về chương trình, cũng như thay đổi cấu trúc và hành vi của chương trình trong quá trình thực thi. Điều quan trọng ở đây là nó được thực hiện trong thời gian chạy chứ không phải lúc biên dịch. Nhưng tại sao phải kiểm tra mã khi chạy? Bạn đã thấy rồi :/ Ví dụ sử dụng Reflection - 1Ý tưởng phản ánh có thể không rõ ràng ngay lập tức vì một lý do: cho đến thời điểm này, bạn luôn biết các lớp bạn đang làm việc cùng. Chà, ví dụ, bạn có thể viết một lớp Cat:
package learn.javarush;

public class Cat {

   private String name;
   private int age;

   public Cat(String name, int age) {
       this.name = name;
       this.age = age;
   }

   public void sayMeow() {

       System.out.println("Meow!");
   }

   public void jump() {

       System.out.println("Jump!");
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public int getAge() {
       return age;
   }

   public void setAge(int age) {
       this.age = age;
   }

@Override
public String toString() {
   return "Cat{" +
           "name='" + name + '\'' +
           ", age=" + age +
           '}';
}

}
Bạn biết mọi thứ về nó, bạn thấy nó có những lĩnh vực và phương thức nào. Chắc chắn bạn có thể tạo một hệ thống kế thừa với một lớp chung để thuận tiện Animal, nếu đột nhiên chương trình cần các lớp động vật khác. Trước đây, chúng tôi thậm chí còn tạo ra một lớp phòng khám thú y trong đó bạn có thể chuyển đối tượng cha mẹ Animalvà chương trình sẽ điều trị cho con vật tùy thuộc vào việc đó là chó hay mèo. Mặc dù các tác vụ này không đơn giản lắm nhưng chương trình sẽ học tất cả thông tin cần thiết về các lớp tại thời điểm biên dịch. Do đó, khi bạn main()chuyển một đối tượng trong một phương thức Catsang các phương thức của lớp phòng khám thú y, chương trình đã biết rằng đây là một con mèo chứ không phải một con chó. Bây giờ hãy tưởng tượng rằng chúng ta đang phải đối mặt với một nhiệm vụ khác. Mục tiêu của chúng tôi là viết một bộ phân tích mã. Chúng ta cần tạo một lớp CodeAnalyzervới một phương thức duy nhất - void analyzeClass(Object o). Phương pháp này nên:
  • xác định lớp mà đối tượng được truyền cho nó và hiển thị tên lớp trong bảng điều khiển;
  • xác định tên của tất cả các trường thuộc lớp này, bao gồm cả trường riêng tư và hiển thị chúng trong bảng điều khiển;
  • xác định tên của tất cả các phương thức của lớp này, bao gồm cả các phương thức riêng tư và hiển thị chúng trong bảng điều khiển.
Nó sẽ trông giống như thế này:
public class CodeAnalyzer {

   public static void analyzeClass(Object o) {

       //Вывести название класса, к которому принадлежит an object o
       //Вывести названия всех переменных этого класса
       //Вывести названия всех методов этого класса
   }

}
Bây giờ sự khác biệt giữa vấn đề này và các vấn đề khác mà bạn đã giải quyết trước đây đã rõ ràng. Trong trường hợp này, khó khăn nằm ở chỗ cả bạn và chương trình đều không biết chính xác những gì sẽ được truyền vào phương thức này analyzeClass(). Bạn viết một chương trình, các lập trình viên khác sẽ bắt đầu sử dụng nó, họ có thể chuyển bất kỳ thứ gì vào phương thức này - bất kỳ lớp Java tiêu chuẩn nào hoặc bất kỳ lớp nào họ đã viết. Lớp này có thể có bất kỳ số lượng biến và phương thức nào. Nói cách khác, trong trường hợp này, chúng tôi (và chương trình của chúng tôi) không biết chúng tôi sẽ làm việc với những lớp nào. Tuy nhiên, chúng ta phải giải quyết vấn đề này. Và ở đây thư viện Java tiêu chuẩn đã hỗ trợ chúng tôi - Java Reflection API. API Reflection là một tính năng ngôn ngữ mạnh mẽ. Tài liệu chính thức của Oracle nêu rõ rằng cơ chế này chỉ được khuyến nghị sử dụng bởi những lập trình viên có kinh nghiệm, những người hiểu rất rõ những gì họ đang làm. Bạn sẽ sớm hiểu lý do tại sao chúng tôi đột nhiên nhận được những cảnh báo trước như vậy :) Dưới đây là danh sách những gì có thể thực hiện được khi sử dụng API Reflection:
  1. Tìm hiểu/xác định lớp của một đối tượng.
  2. Nhận thông tin về các công cụ sửa đổi lớp, trường, phương thức, hằng, hàm tạo và siêu lớp.
  3. Tìm hiểu những phương thức nào thuộc về giao diện/giao diện đã triển khai.
  4. Tạo một thể hiện của một lớp khi không biết tên lớp cho đến khi chương trình được thực thi.
  5. Nhận và đặt giá trị của trường đối tượng theo tên.
  6. Gọi phương thức của đối tượng theo tên.
Danh sách ấn tượng nhỉ? :) Chú ý:Cơ chế phản chiếu có thể thực hiện tất cả điều này một cách “nhanh chóng” bất kể đối tượng lớp nào chúng ta chuyển tới bộ phân tích mã của mình! Hãy xem xét các khả năng của API Reflection bằng các ví dụ.

Cách tìm hiểu/xác định lớp của một đối tượng

Hãy bắt đầu với những điều cơ bản. Điểm vào cơ chế phản chiếu của Java là tệp Class. Vâng, nó trông thực sự buồn cười, nhưng đó chính là mục đích của sự phản chiếu :) Bằng cách sử dụng một class Class, trước hết chúng ta xác định lớp của bất kỳ đối tượng nào được truyền vào phương thức của chúng ta. Chúng ta hãy cố gắng này:
import learn.javarush.Cat;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println(clazz);
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
Đầu ra của bảng điều khiển:

class learn.javarush.Cat
Hãy chú ý đến hai điều. Đầu tiên, chúng tôi cố tình đặt lớp này Catvào một gói riêng, learn.javarush;bây giờ bạn có thể thấy rằng nó getClass()trả về tên đầy đủ của lớp. Thứ hai, chúng tôi đặt tên cho biến của mình clazz. Có vẻ hơi lạ. Tất nhiên, nó phải được gọi là “class”, nhưng “class” là một từ dành riêng trong ngôn ngữ Java và trình biên dịch sẽ không cho phép các biến được gọi theo cách đó. Tôi phải thoát ra khỏi nó :) Chà, một khởi đầu không tệ! Chúng ta còn có gì nữa trong danh sách các khả năng?

Cách nhận thông tin về bộ sửa đổi lớp, trường, phương thức, hằng, hàm tạo và siêu lớp

Điều này đã thú vị hơn! Trong lớp hiện tại chúng ta không có hằng số và không có lớp cha. Hãy thêm chúng cho đầy đủ. Hãy tạo lớp cha đơn giản nhất Animal:
package learn.javarush;
public class Animal {

   private String name;
   private int age;
}
Và hãy thêm Cattính kế thừa từ Animalvà một hằng số vào lớp của chúng ta:
package learn.javarush;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Семейство кошачьих";

   private String name;
   private int age;

   //...остальная часть класса
}
Bây giờ chúng ta đã có một bộ hoàn chỉnh! Hãy thử khả năng phản ánh :)
import learn.javarush.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Name класса: " + clazz);
       System.out.println("Поля класса: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Родительский класс: " + clazz.getSuperclass());
       System.out.println("Методы класса: " +  Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Конструкторы класса: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
Đây là những gì chúng tôi nhận được trong bảng điều khiển:
Name класса: class learn.javarush.Cat
Поля класса: [private static final java.lang.String learn.javarush.Cat.ANIMAL_FAMILY, private java.lang.String learn.javarush.Cat.name, private int learn.javarush.Cat.age]
Родительский класс: class learn.javarush.Animal
Методы класса: [public java.lang.String learn.javarush.Cat.getName(), public void learn.javarush.Cat.setName(java.lang.String), public void learn.javarush.Cat.sayMeow(), public void learn.javarush.Cat.setAge(int), public void learn.javarush.Cat.jump(), public int learn.javarush.Cat.getAge()]
Конструкторы класса: [public learn.javarush.Cat(java.lang.String,int)]
Chúng tôi đã nhận được rất nhiều thông tin chi tiết về lớp học! Và không chỉ về phần công cộng, mà còn về phần riêng tư. Chú ý: private-các biến cũng được hiển thị trong danh sách. Trên thực tế, việc “phân tích” lớp học có thể được coi là hoàn thành vào thời điểm này: bây giờ, bằng cách sử dụng phương pháp này, analyzeClass()chúng ta sẽ học mọi thứ có thể. Nhưng đây không phải là tất cả những khả năng mà chúng ta có được khi làm việc với sự phản chiếu. Chúng ta đừng giới hạn bản thân trong việc quan sát đơn giản mà hãy chuyển sang hành động tích cực! :)

Cách tạo một thể hiện của một lớp nếu không biết tên lớp trước khi chương trình được thực thi

Hãy bắt đầu với hàm tạo mặc định. Nó chưa có trong lớp của chúng ta Cat, vì vậy hãy thêm nó:
public Cat() {

}
Đây là mã sẽ trông như thế nào khi tạo một đối tượng Catbằng cách sử dụng sự phản chiếu (phương thức createCat()):
import learn.javarush.Cat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

   public static Cat createCat() throws IOException, IllegalAccessException, InstantiationException, ClassNotFoundException {

       BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
       String className = reader.readLine();

       Class clazz = Class.forName(className);
       Cat cat = (Cat) clazz.newInstance();

       return cat;
   }

public static Object createObject() throws Exception {

   BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
   String className = reader.readLine();

   Class clazz = Class.forName(className);
   Object result = clazz.newInstance();

   return result;
}

   public static void main(String[] args) throws IOException, IllegalAccessException, ClassNotFoundException, InstantiationException {
       System.out.println(createCat());
   }
}
Nhập vào bảng điều khiển:

learn.javarush.Cat
Đầu ra của bảng điều khiển:

Cat{name='null', age=0}
Đây không phải là lỗi: các giá trị nameageđược hiển thị trong bảng điều khiển vì chúng tôi đã lập trình đầu ra của chúng trong phương thức toString()lớp Cat. Ở đây chúng ta đọc tên của lớp có đối tượng mà chúng ta sẽ tạo từ bảng điều khiển. Chương trình đang chạy sẽ học tên của lớp mà đối tượng mà nó sẽ tạo. Ví dụ sử dụng Reflection - 3Để cho ngắn gọn, chúng tôi đã bỏ qua đoạn mã xử lý ngoại lệ thích hợp để nó không chiếm nhiều dung lượng hơn chính ví dụ đó. Tất nhiên, trong một chương trình thực tế, chắc chắn cần phải xử lý các tình huống nhập sai tên, v.v. Hàm tạo mặc định là một thứ khá đơn giản, vì vậy việc tạo một thể hiện của một lớp bằng cách sử dụng nó, như bạn có thể thấy, không khó :) Và bằng cách sử dụng phương thức này, newInstance()chúng ta tạo một đối tượng mới của lớp này. Sẽ là một vấn đề khác nếu hàm tạo của lớp Catlấy tham số làm đầu vào. Hãy loại bỏ hàm tạo mặc định khỏi lớp và thử chạy lại mã của chúng ta.

null
java.lang.InstantiationException: learn.javarush.Cat
  at java.lang.Class.newInstance(Class.java:427)
Đã xảy ra lỗi! Chúng tôi đã gặp lỗi vì đã gọi một phương thức để tạo đối tượng thông qua hàm tạo mặc định. Nhưng bây giờ chúng tôi không có nhà thiết kế như vậy. Điều này có nghĩa là khi phương thức hoạt động, newInstance()cơ chế phản chiếu sẽ sử dụng hàm tạo cũ của chúng ta với hai tham số:
public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
Nhưng chúng tôi đã không làm gì với các thông số, như thể chúng tôi đã hoàn toàn quên mất chúng! Để chuyển chúng cho hàm tạo bằng cách sử dụng sự phản chiếu, bạn sẽ phải điều chỉnh nó một chút:
import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.javarush.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
Đầu ra của bảng điều khiển:

Cat{name='Barsik', age=6}
Chúng ta hãy xem xét kỹ hơn những gì đang xảy ra trong chương trình của chúng tôi. Chúng tôi đã tạo ra một loạt các đối tượng Class.
Class[] catClassParams = {String.class, int.class};
Chúng tương ứng với các tham số của hàm tạo của chúng tôi (chúng tôi chỉ có các tham số Stringint). Chúng ta chuyển chúng vào phương thức clazz.getConstructor()và có quyền truy cập vào hàm tạo được yêu cầu. Sau này, tất cả những gì còn lại là gọi phương thức newInstance()với các tham số cần thiết và đừng quên truyền đối tượng một cách rõ ràng đến lớp chúng ta cần - Cat.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
Kết quả là đối tượng của chúng ta sẽ được tạo thành công! Đầu ra của bảng điều khiển:

Cat{name='Barsik', age=6}
Tiếp tục nào :)

Cách nhận và đặt giá trị của trường đối tượng theo tên

Hãy tưởng tượng rằng bạn đang sử dụng một lớp được viết bởi một lập trình viên khác. Tuy nhiên, bạn không có cơ hội để chỉnh sửa nó. Ví dụ: thư viện lớp làm sẵn được đóng gói trong JAR. Bạn có thể đọc mã lớp nhưng không thể thay đổi nó. Lập trình viên đã tạo lớp trong thư viện này (hãy coi đó là lớp cũ của chúng tôi Cat) đã không ngủ đủ giấc trước thiết kế cuối cùng và đã loại bỏ các getters và setters cho trường age. Bây giờ lớp học này đã đến với bạn. Nó đáp ứng đầy đủ nhu cầu của bạn, vì bạn chỉ cần các đối tượng trong chương trình Cat. Nhưng bạn cần chúng với cùng lĩnh vực đó age! Đây là một vấn đề: chúng ta không thể tiếp cận trường này vì nó có một modifier privatevà các getters và setters đã bị nhà phát triển tương lai của lớp này xóa :/ Chà, sự phản chiếu cũng có thể giúp chúng ta trong tình huống này! CatChúng tôi có quyền truy cập vào mã lớp : ít nhất chúng tôi có thể tìm ra nó có những trường nào và chúng được gọi là gì. Được trang bị thông tin này, chúng tôi giải quyết vấn đề của mình:
import learn.javarush.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;
       try {
           clazz = Class.forName("learn.javarush.Cat");
           cat = (Cat) clazz.newInstance();

           //с полем name нам повезло - для него в классе есть setter
           cat.setName("Barsik");

           Field age = clazz.getDeclaredField("age");

           age.setAccessible(true);

           age.set(cat, 6);

       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchFieldException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
Như đã nêu trong nhận xét, namemọi thứ đều đơn giản với trường này: các nhà phát triển lớp đã cung cấp một trình thiết lập cho nó. Bạn cũng đã biết cách tạo đối tượng từ hàm tạo mặc định: có một phương thức cho việc này newInstance(). Nhưng bạn sẽ phải mày mò với lĩnh vực thứ hai. Hãy cùng tìm hiểu chuyện gì đang xảy ra ở đây :)
Field age = clazz.getDeclaredField("age");
Ở đây, chúng tôi sử dụng đối tượng của mình Class clazz, truy cập vào trường agebằng phương thức getDeclaredField(). Nó cho chúng ta khả năng lấy trường tuổi làm đối tượng Field age. Nhưng điều này vẫn chưa đủ vì privatecác trường không thể được gán giá trị một cách đơn giản. Để thực hiện việc này, bạn cần làm cho trường này “có sẵn” bằng phương thức setAccessible():
age.setAccessible(true);
Những trường được thực hiện việc này có thể được gán giá trị:
age.set(cat, 6);
Như bạn có thể thấy, chúng ta có một loại setter bị đảo lộn: chúng ta gán Field agegiá trị cho trường và cũng chuyển cho nó đối tượng mà trường này sẽ được gán. Hãy chạy phương pháp của chúng tôi main()và xem:

Cat{name='Barsik', age=6}
Tuyệt vời, chúng tôi đã làm được tất cả! :) Hãy xem chúng ta có những khả năng nào khác...

Cách gọi phương thức của đối tượng theo tên

Hãy thay đổi tình huống một chút so với ví dụ trước. Giả sử nhà phát triển lớp Catđã mắc lỗi với các trường - cả hai đều có sẵn, có getters và setters cho chúng, mọi thứ đều ổn. Vấn đề lại khác: anh ấy đã đặt một phương pháp ở chế độ riêng tư mà chúng tôi chắc chắn cần:
private void sayMeow() {

   System.out.println("Meow!");
}
Kết quả là chúng ta sẽ tạo các đối tượng Cattrong chương trình của mình nhưng sẽ không thể gọi phương thức của chúng sayMeow(). Liệu chúng ta có những con mèo không kêu meo meo không? Khá lạ:/Làm cách nào để khắc phục điều này? Một lần nữa, API Reflection lại ra tay giải cứu! Chúng tôi biết tên của phương pháp được yêu cầu. Phần còn lại là vấn đề kỹ thuật:
import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {

   public static void invokeSayMeowMethod()  {

       Class clazz = null;
       Cat cat = null;
       try {

           cat = new Cat("Barsik", 6);

           clazz = Class.forName(Cat.class.getName());

           Method sayMeow = clazz.getDeclaredMethod("sayMeow");

           sayMeow.setAccessible(true);

           sayMeow.invoke(cat);

       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }
   }

   public static void main(String[] args) {
       invokeSayMeowMethod();
   }
}
Ở đây, chúng tôi hành động theo cách tương tự như trong trường hợp có quyền truy cập vào một trường riêng tư. Đầu tiên chúng ta lấy phương thức chúng ta cần, được gói gọn trong một đối tượng lớp Method:
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Với sự trợ giúp, getDeclaredMethod()bạn có thể “tiếp cận” với các phương pháp riêng tư. Tiếp theo chúng ta làm cho phương thức có thể gọi được:
sayMeow.setAccessible(true);
Và cuối cùng, chúng ta gọi phương thức trên đối tượng mong muốn:
sayMeow.invoke(cat);
Việc gọi một phương thức cũng giống như một "cuộc gọi ngược": chúng ta quen trỏ một đối tượng đến phương thức được yêu cầu bằng cách sử dụng dấu chấm ( cat.sayMeow()) và khi làm việc với sự phản chiếu, chúng ta truyền cho phương thức đối tượng mà nó cần được gọi . Chúng ta có gì trong bảng điều khiển?

Meow!
Mọi thứ đã làm ra! :) Bây giờ bạn đã thấy cơ chế phản chiếu trong Java mang lại cho chúng ta những khả năng mở rộng nào. Trong những tình huống khó khăn và bất ngờ (như trong ví dụ với một lớp học từ thư viện đóng), nó thực sự có thể giúp chúng ta rất nhiều. Tuy nhiên, giống như bất kỳ cường quốc nào, nó cũng hàm chứa trách nhiệm to lớn. Những nhược điểm của sự phản ánh được viết trong một phần đặc biệt trên trang web của Oracle. Có ba nhược điểm chính:
  1. Năng suất giảm. Các phương thức được gọi bằng cách sử dụng sự phản chiếu có hiệu suất thấp hơn các phương thức được gọi thông thường.

  2. Có những hạn chế về an toàn. Cơ chế phản chiếu cho phép bạn thay đổi hành vi của chương trình trong thời gian chạy. Nhưng trong môi trường làm việc của bạn trong một dự án thực tế, có thể có những hạn chế không cho phép bạn thực hiện điều này.

  3. Rủi ro tiết lộ thông tin nội bộ. Điều quan trọng là phải hiểu rằng việc sử dụng sự phản chiếu vi phạm trực tiếp nguyên tắc đóng gói: nó cho phép chúng ta truy cập vào các trường, phương thức riêng tư, v.v. Tôi nghĩ không cần thiết phải giải thích rằng chỉ nên sử dụng hành vi vi phạm trực tiếp và thô bạo các nguyên tắc OOP trong những trường hợp cực đoan nhất, khi không còn cách nào khác để giải quyết vấn đề vì những lý do ngoài tầm kiểm soát của bạn.

Hãy sử dụng cơ chế phản ánh một cách khôn ngoan và chỉ trong những tình huống không thể tránh khỏi, đồng thời đừng quên những khuyết điểm của nó. Điều này kết thúc bài giảng của chúng tôi! Hóa ra nó khá lớn nhưng hôm nay bạn đã học được rất nhiều điều mới :)
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION