JavaRush /Java 博客 /Random-ZH /Java 中的反射 - 使用示例

Java 中的反射 - 使用示例

已在 Random-ZH 群组中发布
您可能在日常生活中遇到过“反思”的概念。通常这个词指的是研究自己的过程。在编程中,它具有类似的含义 - 它是一种检查程序数据以及在程序执行期间更改程序的结构和行为的机制。这里重要的是它是在运行时完成的,而不是在编译时完成的。但为什么要在运行时检查代码呢?你已经看到了:/使用反射的示例 - 1反思的想法可能不会立即清晰,原因有一个:直到这一刻,你一直知道你正在使用的类。好吧,例如,您可以编写一个类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 +
           '}';
}

}
你知道它的一切,你知道它有哪些字段和方法。Animal当然,如果程序突然需要其他类别的动物,为了方便起见,您可以创建一个具有公共类的继承系统。以前,我们甚至创建了一个兽医诊所类,您可以在其中传递父对象Animal,并且程序将根据动物是狗还是猫来对待它。尽管这些任务不是很简单,但程序会在编译时学习所需的有关类的所有信息。因此,当您将方法中的main()对象传递Cat给兽医诊所类的方法时,程序已经知道这是一只猫,而不是狗。现在让我们想象一下我们面临着另一项任务。我们的目标是编写一个代码分析器。我们需要创建一个CodeAnalyzer具有单一方法的类 - void analyzeClass(Object o)。该方法应该:
  • 确定对象传递给它的类并在控制台中显示类名;
  • 确定该类的所有字段的名称,包括私有字段,并将其显示在控制台中;
  • 确定该类的所有方法的名称,包括私有方法,并将它们显示在控制台中。
它看起来像这样:
public class CodeAnalyzer {

   public static void analyzeClass(Object o) {

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

}
现在这个问题和你之前解决的其他问题之间的区别是显而易见的。在这种情况下,困难在于您和程序都不知道到底将传递给该方法的内容是什么analyzeClass()。您编写一个程序,其他程序员将开始使用它,他们可以将任何内容传递到此方法中 - 任何标准 Java 类或他们编写的任何类。该类可以有任意数量的变量和方法。换句话说,在这种情况下,我们(和我们的程序)不知道我们将使用哪些类。然而,我们必须解决这个问题。标准 Java 库可以为我们提供帮助——Java Reflection API。Reflection API 是一项强大的语言功能。Oracle 官方文档指出,建议仅由非常了解自己在做什么的经验丰富的程序员使用此机制。您很快就会明白为什么我们会突然提前收到此类警告:) 以下是使用 Reflection API 可以完成的操作的列表:
  1. 找出/确定对象的类别。
  2. 获取有关类修饰符、字段、方法、常量、构造函数和超类的信息。
  3. 找出哪些方法属于已实现的接口。
  4. 当程序执行之前类名未知时,创建类的实例。
  5. 按名称获取和设置对象字段的值。
  6. 按名称调用对象的方法。
令人印象深刻的清单,是吧?:) 注意:无论我们将哪个类对象传递给代码分析器,反射机制都能够“即时”执行所有这些操作!让我们通过示例来了解 Reflection API 的功能。

如何找出/确定对象的类

让我们从基础开始。Java 反射机制的入口点是Class. 是的,它看起来真的很有趣,但这就是反射的用途:)使用类Class,我们首先确定传递给我们方法的任何对象的类。让我们试试这个:
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));
   }
}
控制台输出:

class learn.javarush.Cat
注意两件事。首先,我们故意将类放在Cat一个单独的包中,learn.javarush;现在你可以看到它getClass()返回了类的全名。其次,我们将变量命名为clazz。看起来有点奇怪。当然应该叫“class”,但是“class”是Java语言中的保留字,编译器不会允许这样调用变量。我必须摆脱它:) 好吧,这是一个不错的开始!我们还有什么可能性?

如何获取有关类修饰符、字段、方法、常量、构造函数和超类的信息

这已经更有趣了!在当前类中,我们没有常量,也没有父类。为了完整起见,我们添加它们。让我们创建最简单的父类Animal
package learn.javarush;
public class Animal {

   private String name;
   private int age;
}
让我们向我们的类添加一个Cat继承Animal和一个常量:
package learn.javarush;

public class Cat extends Animal {

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

   private String name;
   private int age;

   //...остальная часть класса
}
现在我们已经有一整套了!让我们尝试一下反思的可能性:)
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));
   }
}
这是我们在控制台中得到的结果:
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)]
我们收到了有关课​​程的非常详细的信息!不仅涉及公共部分,还涉及私人部分。 注意: private- 变量也显示在列表中。实际上,到此为止,类的“分析”就可以被认为完成了:现在,使用该方法,analyzeClass()我们将学习所有可能的内容。但这些并不是我们在进行反思时所拥有的全部可能性。让我们不要局限于简单的观察,而要积极行动!:)

如果在程序执行之前类名未知,如何创建类的实例

让我们从默认构造函数开始。它不在我们的类中Cat,所以让我们添加它:
public Cat() {

}
以下是使用反射创建对象的代码Cat(方法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());
   }
}
进入控制台:

learn.javarush.Cat
控制台输出:

Cat{name='null', age=0}
这不是错误:值nameage显示在控制台中,因为我们在toString()类方法中编写了它们的输出Cat。在这里,我们读取将从控制台创建其对象的类的名称。正在运行的程序了解它将创建其对象的类的名称。 使用反射的示例 - 3为了简洁起见,我们省略了正确的异常处理代码,这样它就不会比示例本身占用更多的空间。当然,在真实的程序中,绝对值得处理输入错误名称等情况。默认构造函数是一个相当简单的事情,因此如您所见,使用它创建类的实例并不困难:)并且使用该方法,newInstance()我们创建了此类的一个新对象。Cat如果类构造函数将参数作为输入,则是另一回事。让我们从类中删除默认构造函数并尝试再次运行我们的代码。

null
java.lang.InstantiationException: learn.javarush.Cat
  at java.lang.Class.newInstance(Class.java:427)
出了些问题!我们收到错误,因为我们调用了通过默认构造函数创建对象的方法。但现在我们没有这样的设计师。这意味着当该方法起作用时,newInstance()反射机制将使用带有两个参数的旧构造函数:
public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
但我们没有对这些参数做任何事情,就好像我们完全忘记了它们一样!要使用反射将它们传递给构造函数,您必须稍微调整一下:
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());
   }
}
控制台输出:

Cat{name='Barsik', age=6}
让我们仔细看看我们的程序中发生了什么。我们创建了一个对象数组Class
Class[] catClassParams = {String.class, int.class};
它们对应于我们构造函数的参数(我们只有参数Stringint)。我们将它们传递给方法clazz.getConstructor()并访问所需的构造函数。之后,剩下的就是使用newInstance()必要的参数调用该方法,并且不要忘记将对象显式转换为我们需要的类 - Cat
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
这样,我们的对象就创建成功了!控制台输出:

Cat{name='Barsik', age=6}
让我们继续 :)

如何通过名称获取和设置对象字段的值

想象一下您正在使用另一个程序员编写的类。但是,您没有机会对其进行编辑。例如,打包在 JAR 中的现成的类库。您可以阅读类代码,但无法更改它。在这个库中创建类的程序员(让它成为我们的旧类Cat)在最终设计之前没有得到足够的睡眠,并删除了该字段的 getter 和 setter age。现在这堂课已经来到你身边了。它完全满足您的需求,因为您只需要程序中的对象Cat。但你需要他们在同一领域age!这是一个问题:我们无法到达该字段,因为它有一个修饰符private,并且 getter 和 setter 被此类的潜在开发人员删除了:/好吧,反射在这种情况下也可以帮助我们!Cat我们可以访问类代码:我们至少可以找出它有哪些字段以及它们的名称。有了这些信息,我们就可以解决我们的问题:
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());
   }
}
正如评论中所述,name该字段的一切都很简单:类开发人员为其提供了一个设置器。您还已经知道如何从默认构造函数创建对象:有一个 this 方法newInstance()。但你必须修改第二个字段。让我们弄清楚这里发生了什么:)
Field age = clazz.getDeclaredField("age");
在这里,我们使用我们的对象Class clazz,使用 访问age该字段getDeclaredField()。它使我们能够将年龄字段作为对象获取Field age。但这还不够,因为private字段不能简单地赋值。为此,您需要使用以下方法使该字段“可用” setAccessible()
age.setAccessible(true);
可以为完成此操作的那些字段分配值:
age.set(cat, 6);
正如您所看到的,我们有一种颠倒的设置器:我们为字段分配Field age其值,并将该字段应分配到的对象传递给它。让我们运行我们的方法main()看看:

Cat{name='Barsik', age=6}
太棒了,我们都做到了!:) 让我们看看还有什么其他的可能性......

如何通过名称调用对象的方法

让我们稍微改变一下前面例子的情况。假设类开发人员Cat在字段上犯了一个错误 - 两者都可用,有它们的 getter 和 setter,一切都很好。问题是不同的:他将我们肯定需要的方法设为私有:
private void sayMeow() {

   System.out.println("Meow!");
}
结果,我们将Cat在程序中创建对象,但无法调用它们的方法sayMeow()。我们会有不会喵喵叫的猫吗?很奇怪:/我该如何解决这个问题?Reflection API 再次拯救您!我们知道所需方法的名称。剩下的就是技术问题了:
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();
   }
}
在这里,我们的行为方式与访问私有字段的情况大致相同。首先我们得到我们需要的方法,它被封装在一个类对象中Method
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
在帮助下,getDeclaredMethod()您可以“接触”私有方法。接下来我们使该方法可调用:
sayMeow.setAccessible(true);
最后,我们调用所需对象的方法:
sayMeow.invoke(cat);
调用方法也看起来像“反向调用”:我们习惯于使用点 ( cat.sayMeow()) 将对象指向所需的方法,而在使用反射时,我们将需要调用该方法的对象传递给该方法。我们在控制台中有什么?

Meow!
一切顺利!:) 现在您可以看到 Java 中的反射机制为我们提供了多么广泛的可能性。在困难和意外的情况下(例如在封闭库中的类的示例中),它确实可以帮助我们很多。然而,就像任何大国一样,它也意味着巨大的责任。Oracle 网站上有专门的章节介绍了反射的缺点。主要有以下三个缺点:
  1. 生产力下降。使用反射调用的方法的性能低于通常调用的方法。

  2. 有安全限制。反射机制允许您在运行时更改程序的行为。但在实际项目的工作环境中,可能存在一些限制,不允许您执行此操作。

  3. 内幕信息泄露的风险。重要的是要理解,使用反射直接违反了封装原则:它允许我们访问私有字段、方法等。我认为没有必要解释,只有在最极端的情况下,当由于你无法控制的原因而没有其他方法来解决问题时,才应该诉诸直接和粗暴地违反 OOP 原则。

明智地使用反射机制,并且仅在无法避免的情况下使用,并且不要忘记它的缺点。我们的讲座到此结束!结果相当大,但今天你学到了很多新东西:)
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION