CodeGym /Blog Java /Ngẫu nhiên /Ví dụ về sự phản chiếu

Ví dụ về sự phản chiếu

Xuất bản trong nhóm
Có thể bạn đã bắt gặp khái niệm “phản chiếu” trong cuộc sống đời thường. Từ này thường chỉ 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ế để phân tích dữ liệu về một chương trình và thậm chí thay đổi cấu trúc và hành vi của chương trình trong khi chương trình đang chạy. Ví dụ về phản ánh - 1 Điều quan trọng ở đây là chúng tôi đang làm điều này trong thời gian chạy, không phải lúc biên dịch. Nhưng tại sao lại kiểm tra mã trong thời gian chạy? Rốt cuộc, bạn đã có thể đọc mã rồi :/ Có một lý do tại sao ý tưởng phản chiếu có thể không rõ ràng ngay lập tức: cho đến thời điểm này, bạn luôn biết mình đang làm việc với những lớp nào. Ví dụ: bạn có thể viết một Catlớp:

package learn.codegym;

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ó và bạn có thể thấy các trường và phương thức mà nó có. Giả sử bạn đột nhiên cần giới thiệu các lớp động vật khác vào chương trình. Bạn có thể tạo cấu trúc kế thừa lớp với Animallớp cha để thuận tiện. Trước đó, chúng tôi thậm chí đã tạo một lớp đại diện cho một phòng khám thú y mà chúng tôi có thể chuyển một Animalđối tượng (ví dụ lớp cha) và chương trình đối xử với con vật một cách thích hợp dựa trên việc đó là chó hay mèo. Mặc dù đây không phải là những nhiệm vụ đơn giản nhất, nhưng chương trình có thể tìm hiểu 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. Theo đó, khi bạn truyền một Catđối tượng cho các phương thức của lớp phòng khám thú y trongmain()phương pháp, chương trình đã biết rằng đó là một con mèo, không phải một con chó. Bây giờ hãy tưởng tượng rằng chúng ta đang đố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 CodeAnalyzerlớp với một phương thức duy nhất: void analyzeObject(Object o). Phương pháp này nên:
  • xác định lớp của đối tượng được truyền cho nó và hiển thị tên lớp trên bàn điều khiển;
  • xác định tên của tất cả các trường của lớp đã thông qua, bao gồm cả các trường riêng tư và hiển thị chúng trên 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 đã truyền, bao gồm cả các phương thức riêng tư và hiển thị chúng trên 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) {
      
       // Print the name of the class of object o
       // Print the names of all variables of this class
       // Print the names of all methods of this class
   }
  
}
Bây giờ chúng ta có thể thấy rõ nhiệm vụ này khác với các nhiệm vụ khác mà bạn đã giải quyết trước đó như thế nào. Với mục tiêu hiện tại của chúng tôi, khó khăn nằm ở chỗ cả chúng tôi và chương trình đều không biết chính xác những gì sẽ được chuyển đếnanalyzeClass()phương pháp. Nếu bạn viết một chương trình như vậy, các lập trình viên khác sẽ bắt đầu sử dụng nó và họ có thể chuyển bất kỳ thứ gì cho 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 khác mà họ viết. Lớp được truyền có thể có bất kỳ số lượng biến và phương thức nào. Nói cách khác, 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 vẫn cần phải hoàn thành nhiệm vụ này. Và đây là nơi API phản chiếu Java tiêu chuẩn hỗ trợ chúng tôi. Reflection API là một công cụ mạnh mẽ của ngôn ngữ. Tài liệu chính thức của Oracle khuyến nghị rằng cơ chế này chỉ nên được sử dụng bởi những lập trình viên có kinh nghiệm, những người biết họ đang làm gì. Bạn sẽ sớm hiểu tại sao chúng tôi lại đưa ra loại cảnh báo này trước :) Sau đây là danh sách những việc bạn có thể làm với Reflection API:
  1. Xác định/xác định lớp của một đối tượng.
  2. Nhận thông tin về công cụ sửa đổi lớp, trường, phương thức, hằng số, hàm tạo và siêu lớp.
  3. Tìm ra phương thức nào thuộc về (các) giao diện đã triển khai.
  4. Tạo một thể hiện của một lớp mà tên lớp không được biết 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 một phương thức cá thể theo tên.
Danh sách ấn tượng nhỉ? :) Ghi chú:cơ chế phản chiếu có thể thực hiện tất cả những thứ này một cách "nhanh chóng", bất kể loại đối tượng nào chúng ta chuyển đến bộ phân tích mã của mình! Hãy khám phá các khả năng của Reflection API bằng cách xem xét một số ví dụ.

Cách xác định/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ông cụ phản chiếu Java là lớp Class. Vâng, nó trông thực sự buồn cười, nhưng đó là sự phản chiếu :) Sử dụng lớp Class, trước tiên chúng tôi xác định lớp của bất kỳ đối tượng nào được truyền cho phương thức của chúng tôi. Hãy thử làm điều này:

import learn.codegym.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("Fluffy", 6));
   }
}
Đầu ra bảng điều khiển:

class learn.codegym.Cat
Hãy chú ý đến hai điều. Đầu tiên, chúng tôi cố tình đặt Catlớp trong một learn.codegymgói riêng biệt. Bây giờ bạn có thể thấy rằng getClass()phương thức 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. Điều đó có vẻ hơi lạ. Sẽ hợp lý khi gọi nó là "lớp", nhưng "lớp" là một từ dành riêng trong Java. Trình biên dịch sẽ không cho phép các biến được gọi như vậy. Chúng tôi phải giải quyết vấn đề đó bằng cách nào đó :) Khởi đầu không tệ! Chúng ta có gì khác trong danh sách các khả năng đó?

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

Bây giờ mọi thứ đang trở nên thú vị hơn! Trong lớp hiện tại, chúng tôi không có bất kỳ hằng số hoặc lớp cha nào. Hãy thêm chúng để tạo ra một bức tranh hoàn chỉnh. Tạo Animallớp cha đơn giản nhất:

package learn.codegym;
public class Animal {

   private String name;
   private int age;
}
Và chúng tôi sẽ làm cho Catlớp kế thừa của chúng tôi Animalvà thêm một hằng số:

package learn.codegym;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Feline family";

   private String name;
   private int age;

   // ...the rest of the class
}
Bây giờ chúng ta có bức tranh hoàn chỉnh! Hãy xem những gì sự phản chiếu có khả năng :)

import learn.codegym.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Class name: " + clazz);
       System.out.println("Class fields: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Parent class: " + clazz.getSuperclass());
       System.out.println("Class methods: " + Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Class constructors: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Fluffy", 6));
   }
}
Đây là những gì chúng ta thấy trên bảng điều khiển:

Class name:  class learn.codegym.Cat 
Class fields: [private static final java.lang.String learn.codegym.Cat.ANIMAL_FAMILY, private java.lang.String learn.codegym.Cat.name, private int learn.codegym.Cat.age] 
Parent class: class learn.codegym.Animal 
Class methods: [public java.lang.String learn.codegym.Cat.getName(), public void learn.codegym.Cat.setName(java.lang.String), public void learn.codegym.Cat.sayMeow(), public void learn.codegym.Cat.setAge(int), public void learn.codegym.Cat.jump(), public int learn.codegym.Cat.getAge()] 
Class constructors: [public learn.codegym.Cat(java.lang.String, int)]
Hãy xem tất cả thông tin lớp học chi tiết mà chúng tôi có thể nhận được! Và không chỉ thông tin công khai, mà cả thông tin cá nhân! Ghi chú: privatecác biến cũng được hiển thị trong danh sách. "Phân tích" của chúng tôi về lớp có thể được coi là hoàn chỉnh về cơ bản: chúng tôi đang sử dụng analyzeObject()phương pháp này để tìm hiểu mọi thứ có thể. Nhưng đây không phải là tất cả những gì chúng ta có thể làm với sự phản chiếu. Chúng tôi không bị giới hạn trong việc quan sát đơn giản — chúng tôi sẽ chuyển sang hành động! :)

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

Hãy bắt đầu với hàm tạo mặc định. Lớp của chúng tôi Catchưa có, vì vậy hãy thêm nó:

public Cat() {
  
}
Đây là mã để tạo một Catđối tượng bằng cách sử dụng phương thức ( createCat()phương thức ):

import learn.codegym.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());
   }
}
Đầu vào bảng điều khiển:

learn.codegym.Cat
Đầu ra bảng điều khiển:

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

null
java.lang.InstantiationException: learn.codegym.Cat 
at java.lang.Class.newInstance(Class.java:427)
Đã xảy ra sự cố! Chúng tôi đã gặp lỗi vì chúng tôi đã gọi một phương thức để tạo một đối tượng bằng cách sử dụng hàm tạo mặc định. Nhưng chúng tôi không có một nhà xây dựng như vậy bây giờ. Vì vậy, khi newInstance()phương thức chạy, cơ chế phản chiếu 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 bất cứ điều gì với các tham số, như thể chúng tôi đã hoàn toàn quên mất chúng! Sử dụng sự phản chiếu để truyền đối số cho hàm tạo đòi hỏi một chút "sáng tạo":

import learn.codegym.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.codegym.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 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 bảng điều khiển:

Cat{name='Fluffy', age=6}
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 mảng Classcác đối tượng.

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 ta (chỉ có Stringinttham số). Chúng tôi chuyển chúng đến clazz.getConstructor()phương thức và có quyền truy cập vào hàm tạo mong muốn. Sau đó, tất cả những gì chúng ta cần làm là gọi newInstance()phương thức với các đối số cần thiết và đừng quên chuyển đối tượng sang kiểu mong muốn một cách rõ ràng: Cat.

cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
Bây giờ đối tượng của chúng ta đã được tạo thành công! Đầu ra bảng điều khiển:

Cat{name='Fluffy', age=6}
Di chuyển ngay dọc theo :)

Cách nhận và đặt giá trị của trường mẫu 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. Ngoài ra, bạn không có khả năng chỉnh sửa nó. Ví dụ: một thư viện lớp làm sẵn được đóng gói trong JAR. Bạn có thể đọc mã của các lớp, nhưng bạn không thể thay đổi nó. Giả sử rằng lập trình viên đã tạo một trong các lớp trong thư viện này (hãy để nó là Catlớp cũ của chúng ta), không ngủ đủ giấc vào đêm trước khi thiết kế được hoàn thiện, đã xóa bộ thu thập và thiết lập cho trường age. Bây giờ lớp học này đã đến với bạn. Nó đáp ứng mọi nhu cầu của bạn, vì bạn chỉ cần Catcác đối tượng trong chương trình của mình. Nhưng bạn cần chúng để có một agelĩnh vực! Đây là một vấn đề: chúng tôi không thể tiếp cận lĩnh vực này, bởi vì nó cóprivatecông cụ sửa đổi và getter và setter đã bị xóa bởi nhà phát triển thiếu ngủ, người đã tạo ra lớp :/ Chà, sự phản chiếu có thể giúp chúng ta trong tình huống này! Chúng tôi có quyền truy cập vào mã của Catlớp, vì vậy ít nhất chúng tôi có thể tìm hiểu xem 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 có thể giải quyết vấn đề của mình:

import learn.codegym.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

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

           // We got lucky with the name field, since it has a setter
           cat.setName("Fluffy");

           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 các nhận xét, mọi thứ với nametrường đều đơn giản, vì các nhà phát triển lớp đã cung cấp một trình thiết lập. Bạn đã biết cách tạo các đối tượng từ các hàm tạo mặc định: chúng ta có công newInstance()cụ này. Nhưng chúng ta sẽ phải mày mò với trường thứ hai. Hãy tìm hiểu những gì đang xảy ra ở đây :)

Field age = clazz.getDeclaredField("age");
Ở đây, sử dụng Class clazzđối tượng của chúng tôi, chúng tôi truy cập agetrường thông qua getDeclaredField()phương thức. Nó cho phép chúng ta lấy trường tuổi làm Field ageđối tượng. Nhưng điều này là không đủ, bởi vì chúng ta không thể gán giá trị cho privatecác trường một cách đơn giản. Để làm điều này, chúng ta cần làm cho trường có thể truy cập được bằng cách sử dụng setAccessible()phương thức:

age.setAccessible(true);
Khi chúng tôi thực hiện điều này với một trường, chúng tôi có thể gán một giá trị:

age.set(cat, 6);
Như bạn có thể thấy, Field ageđối tượng của chúng ta có một loại trình thiết lập từ trong ra ngoài mà chúng ta chuyển một giá trị int và đối tượng có trường sẽ được gán. Chúng tôi chạy main()phương pháp của chúng tôi và xem:

Cat{name='Fluffy', age=6}
Xuất sắc! Chúng ta làm được rồi! :) Hãy xem những gì chúng ta có thể làm ...

Cách gọi một phương thức cá thể theo tên.

Hãy thay đổi một chút tình huống trong ví dụ trước. Giả sử rằng Catngười phát triển lớp không mắc lỗi với getters và setters. Mọi thứ đều ổn trong vấn đề đó. Bây giờ vấn đề đã khác: có một phương pháp mà chúng tôi chắc chắn cần, nhưng nhà phát triển đã đặt nó ở chế độ riêng tư:

private void sayMeow() {

   System.out.println("Meow!");
}
Điều này có nghĩa là nếu chúng ta tạo Catcác đối tượng trong chương trình của mình thì chúng ta sẽ không thể gọi sayMeow()phương thức trên chúng. Chúng ta sẽ có những con mèo không kêu meo meo? Thật kỳ quặc :/ Chúng ta sẽ khắc phục điều này như thế nào? Một lần nữa, Reflection API sẽ giúp chúng ta! Chúng tôi biết tên của phương pháp chúng tôi cần. Mọi thứ khác là một kỹ thuật:

import learn.codegym.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("Fluffy", 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 thực hiện nhiều thao tác tương tự như khi truy cập vào một trường riêng tư. Đầu tiên, chúng tôi có được phương pháp chúng tôi cần. Nó được gói gọn trong một Methodđối tượng:

Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Phương thức này getDeclaredMethod()cho phép chúng ta truy cập các phương thức riêng tư. Tiếp theo, chúng tôi 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);
Ở đây, lệnh gọi phương thức của chúng ta trông giống như một "gọi lại": chúng ta đã quen với việc sử dụng dấu chấm để chỉ một đối tượng vào phương thức mong muốn ( cat.sayMeow()), nhưng khi làm việc với sự phản chiếu, chúng ta chuyển đối tượng mà chúng ta muốn gọi đến phương thức đó. phương pháp đó. Có gì trên bảng điều khiển của chúng tôi?

Meow!
Mọi thứ đã hoạt động! :) Bây giờ bạn có thể thấy những khả năng to lớn mà cơ chế phản chiếu của Java mang lại cho chúng ta. Trong những tình huống khó khăn và bất ngờ (chẳng hạn như ví dụ của chúng tôi với một lớp học từ thư viện đã đóng cửa), nó thực sự có thể giúp chúng tôi rất nhiều. Nhưng, như với bất kỳ cường quốc nào, nó mang lại trách nhiệm lớn. Những bất lợi của sự phản ánh được mô 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. Hiệu suất kém hơn. Các phương thức được gọi bằng phản xạ có hiệu suất kém hơn các phương thức được gọi theo cách thông thường.

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

  3. Rủi ro lộ thông tin nội bộ. Điều quan trọng là phải hiểu sự phản chiếu là sự 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 không nghĩ rằng mình cần phải đề cập đến việc vi phạm trực tiếp và trắng trợn các nguyên tắc của OOP. chỉ trong những trường hợp cực đoan nhất, khi không có 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.

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