CodeGym /Java 博客 /随机的 /反射API:反射。Java的阴暗面
John Squirrels
第 41 级
San Francisco

反射API:反射。Java的阴暗面

已在 随机的 群组中发布
你好,年轻的 Padawan。在本文中,我将向您介绍原力,这是 Java 程序员仅在看似不可能的情况下才会使用的一种力量。Java 的阴暗面是反射 API。在 Java 中,反射是使用 Java Reflection API 实现的。

什么是Java反射?

互联网上有一个简短、准确且流行的定义。反射(来自晚期拉丁语 reflexio - 回头)是一种在程序运行时探索有关数据的机制。反射让您探索有关字段、方法和类构造函数的信息。反射使您可以使用在编译时不存在但在运行时可用的类型。反射和用于发布错误信息的逻辑一致模型使得创建正确的动态代码成为可能。换句话说,了解反射在 Java 中的工作原理将为您打开许多惊人的机会。您实际上可以兼顾类及其组件。这是反射允许的基本列表:
  • 学习/确定对象的类别;
  • 获取有关类的修饰符、字段、方法、常量、构造函数和超类的信息;
  • 找出哪些方法属于已实现的接口;
  • 创建一个类的实例,其类名在运行时之前是未知的;
  • 按名称获取和设置对象字段的值;
  • 按名称调用对象的方法。
反射几乎用在所有现代 Java 技术中。很难想象,作为一个平台,Java 能够在不经反思的情况下获得如此广泛的采用。最有可能的是,它不会。既然您已经大致了解了作为理论概念的反射,那么让我们继续讨论它的实际应用吧!我们不会学习反射 API 的所有方法——只学习您在实践中实际遇到的方法。由于反射涉及使用类,我们将从一个简单的类开始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);
   }
}
如您所见,这是一个非常基础的类。带参数的构造函数被特意注释掉了。我们稍后再谈。如果您仔细查看类的内容,您可能会注意到name字段缺少getter。名称字段本身标有私有访问修饰符:我们无法类本身之外访问它,这意味着我们无法检索它的值。“那有什么问题吗?” 你说。“添加getter或更改访问修饰符”。你是对的,除非MyClass位于已编译的 AAR 库或另一个无法进行更改的私有模块中。实际上,这种情况经常发生。而一些粗心的程序员根本就忘了写一个getter。这是记住反省的好时机!让我们尝试进入类的私有名称字段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
}
我们来分析一下刚刚发生的事情。在 Java 中,有一个很棒的类叫做Class. 它表示可执行 Java 应用程序中的类和接口。我们不会讨论Class和之间的关系ClassLoader,因为这不是本文的主题。接下来,要检索此类的字段,您需要调用该getFields()方法。此方法将返回此类的所有可访问字段。这对我们不起作用,因为我们的字段是private,所以我们使用该getDeclaredFields()方法。此方法还返回一个类字段数组,但现在它包括私有字段受保护字段。在这种情况下,我们知道我们感兴趣的字段的名称,因此我们可以使用该getDeclaredField(String)方法,其中String是所需字段的名称。 笔记: getFields()并且getDeclaredFields()不要返回父类的字段!伟大的。我们得到了一个引用我们名字Field的对象。由于该字段不是public,我们必须授予访问权限才能使用它。该方法让我们进一步进行。现在名称字段在我们的完全控制之下!您可以通过调用对象的方法来检索它的值,其中是我们类的一个实例。我们将类型转换为并将值分配给我们的名称变量。如果我们找不到一个setter来给 name 字段设置一个新值,可以使用set方法: setAccessible(true)Fieldget(Object)ObjectMyClassString

field.set(myClass, (String) "new value");
恭喜!您刚刚掌握了反射的基础知识并访问了私有字段! 注意块try/catch,以及正在处理的异常类型。IDE 会告诉您它们本身是必需的,但您可以通过它们的名称清楚地告诉您它们为什么会出现在这里。继续!您可能已经注意到,我们的MyClass类已经有一个方法来显示有关类数据的信息:

private void printData(){
       System.out.println(number + name);
   }
但是这个程序员也在这里留下了他的指纹。方法有私有访问修饰符,每次显示数据都得自己写代码。真是一团糟。我们的反思去哪儿了?编写以下函数:

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();
   }
}
这里的过程与用于检索字段的过程大致相同。我们按名称访问所需的方法并授予对它的访问权限。在Method对象上我们调用invoke(Object, Args)方法,其中Object也是类的一个实例MyClassArgs是方法的参数,尽管我们的没有。现在我们使用printData函数来显示信息:

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
}
欢呼!现在我们可以访问该类的私有方法。但是,如果该方法确实有参数怎么办,为什么构造函数被注释掉了呢?一切都在适当的时候。从一开始的定义就清楚了,反射让你可以在运行时(程序运行时)创建一个类的实例!我们可以使用类的全名创建一个对象。类的全称是类名,包括其包的路径。
反射API:反射。 Java 的阴暗面 - 2
在我的包层次结构中, MyClass的全名是“reflection.MyClass”。还有一种学习类名的简单方法(将类名作为字符串返回):

MyClass.class.getName()
让我们使用 Java 反射来创建类的实例:

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
}
当 Java 应用程序启动时,并非所有类都加载到 JVM 中。如果您的代码没有引用该类MyClass,那么ClassLoader负责将类加载到 JVM 中的 将永远不会加载该类。这意味着您必须强制ClassLoader加载它并以变量的形式获取类描述Class。这就是我们拥有该forName(String)方法的原因,其中String是我们需要其描述的类的名称。获取Сlass对象后,调用该方法newInstance()将返回Object使用该描述创建的对象。剩下的就是将这个对象提供给我们MyClass班级。凉爽的!这很难,但我希望可以理解。现在我们可以在一行中创建一个类的实例!不幸的是,所描述的方法仅适用于默认构造函数(不带参数)。如何调用带有参数的方法和构造函数?是时候取消注释我们的构造函数了。正如预期的那样,newInstance()找不到默认构造函数,并且不再有效。让我们重写类实例化:

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
}
应在类定义上调用该getConstructors()方法以获取类构造函数,然后getParameterTypes()调用该方法以获取构造函数的参数:

Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
这让我们得到了所有的构造函数和它们的参数。在我的示例中,我引用了一个特定的构造函数,该构造函数具有特定的、以前已知的参数。为了调用这个构造函数,我们使用方法newInstance,我们将这些参数的值传递给该方法。invoke调用方法时也是一样的。这就引出了一个问题:什么时候通过反射调用构造函数派上用场?如开头所述,现代 Java 技术离不开 Java Reflection API。例如依赖注入(Dependency Injection,DI),将注解与方法和构造函数的反射相结合,形成了流行的DarerAndroid 开发库。阅读本文后,您可以自信地认为自己已掌握 Java 反射 API 的方法。他们不会无缘无故地将反射称为 Java 的阴暗面。它完全打破了 OOP 范式。在 Java 中,封装隐藏并限制了其他人对某些程序组件的访问。当我们使用 private 修饰符时,我们希望该字段只能从它所在的类中访问。并且我们基于这个原则构建程序的后续架构。在本文中,我们了解了如何使用反射来强制执行任何操作。 创建型设计模式Singleton是一个很好的例子,作为一个架构解决方案。基本思想是实现此模式的类在整个程序执行期间将只有一个实例。这是通过将私有访问修饰符添加到默认构造函数来实现的。如果程序员使用反射来创建此类类的更多实例,那将是非常糟糕的。对了,最近听同事问了一个很有意思的问题:实现了单例模式的类可以继承吗?难道,在这种情况下,连反思都无能为力了?在下面的评论中留下您对本文的反馈和答案,并在那里提出您自己的问题!

更多阅读:

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION