JavaRush /Java 博客 /Random-ZH /反射 API。反射。Java 的黑暗面
Oleksandr Klymenko
第 13 级
Харків

反射 API。反射。Java 的黑暗面

已在 Random-ZH 群组中发布
你好,年轻的学徒。在这篇文章中,我将向您介绍原力,Java 程序员仅在看似绝望的情况下使用它的力量。所以,Java 的阴暗面是 -Reflection API
反射 API。 反射。 Java 的黑暗面 - 1
Java 中的反射是使用 Java Reflection API 完成的。这个反射是什么?有一个简短而精确的定义在互联网上也很流行。 反射 (来自后期拉丁语 reflexio - 回溯)是一种研究程序执行期间数据的机制。反射允许您检查有关字段、方法和类构造函数的信息。反射机制本身允许您处理在编译期间丢失但在程序执行期间出现的类型。反射和用于报告错误的逻辑一致模型的存在使得创建正确的动态代码成为可能。换句话说,了解反射在 Java 中的工作原理将为您带来许多惊人的机会。您实际上可以同时使用类及其组件。
反射 API。 反射。 Java 的黑暗面 - 2
以下是反射允许的基本列表:
  • 找出/确定对象的类别;
  • 获取有关类修饰符、字段、方法、常量、构造函数和超类的信息;
  • 找出哪些方法属于已实现的接口;
  • 创建类的实例,类的名称在程序执行之前是未知的;
  • 通过名称获取和设置对象字段的值;
  • 按名称调用对象的方法。
几乎所有现代 Java 技术都使用了反射。很难想象 Java 作为一个平台是否能够在不经过反思的情况下获得如此广泛的采用。我很可能不能。你已经熟悉了反射的一般理论思想,现在让我们开始实际应用吧!我们不会研究Reflection 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);
   }
}
正如我们所看到的,这是最常见的类。带参数的构造函数被注释掉是有原因的,我们稍后会再讨论这一点。如果您仔细查看课程内容,您可能会发现getter'a的缺失name。该字段本身name标有访问修饰符private;我们无法在类本身之外访问它;=>我们无法获取它的值。“所以有什么问题?- 你说。“添加getter或更改访问修饰符。” 你是对的,但如果MyClass它位于已编译的 aar 库或另一个没有编辑访问权限的封闭模块中,该怎么办,而实际上这种情况经常发生。而一些不专心的程序员干脆忘记了写getter。是时候记住反思了!让我们尝试进入类private字段: nameMyClass
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().这个方法也返回一个类字段数组,但是现在privateprotected。在我们的情况下,我们知道我们感兴趣的字段的名称,我们可以使用方法getDeclaredField(String),其中String是所需字段的名称。 笔记: getFields()并且getDeclaredFields()不返回父类的字段!太棒了,我们收到了一个Field 带有我们的链接的对象name。因为 该领域不是публичным(公共)的,因此应给予其使用的权限。该方法setAccessible(true)使我们能够继续工作。现在战场name已经完全在我们的掌控之中了!get(Object)您可以通过调用该对象来获取它的值Field,其中Object是我们类的一个实例MyClass。我们将其转换String并将其分配给我们的变量name。如果我们突然没有setter'a,我们可以使用该方法为 name 字段设置一个新值set
field.set(myClass, (String) "new value");
恭喜!您刚刚掌握了反射的基本机制,并且能够进入该private领域了! 注意try/catch处理的异常块和类型。IDE 本身会指示它们的强制存在,但它们的名称清楚地表明了它们存在的原因。前进!正如您可能已经注意到的,我们MyClass已经有一个用于显示有关类数据的信息的方法:
private void printData(){
       System.out.println(number + name);
   }
但这位程序员也在这里留下了遗产。该方法位于访问修饰符下private,每次我们都必须自己编写输出代码。不按顺序,我们的反射在哪里?...我们来写下面的函数:
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(Оbject, Args),其中Оbject也是类的实例MyClass Args- 方法参数 - 我们的没有。现在我们使用该函数来显示信息printData
public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // outout 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
}
万岁,现在我们可以访问类的私有方法了。但是如果该方法仍然有参数怎么办,为什么会有一个被注释掉的构造函数呢?每个事物都有它的时代。runtime从一开始的定义就可以清楚地看出,反射允许您以某种模式(在程序运行时)创建类的实例!我们可以通过该类的完全限定名称来创建该类的对象。完全限定类名是类的名称,在package.
反射 API。 反射。 Java 的黑暗面 - 3
在我的层次结构中,package全名MyClass是“ reflection.MyClass”。您还可以通过简单的方式找出类名(它将以字符串形式返回类名):
MyClass.class.getName()
让我们使用反射创建该类的实例:
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,那么负责将类加载到 JVM 中的人,即ClassLoader永远不会将其加载到那里。因此,我们需要强制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 技术离不开 Reflection API。例如,DI(依赖注入),其中注释与方法和构造函数的反射相结合形成了 Android 开发中流行的 Dagger 库。读完本文后,您可以自信地认为自己对 Reflection API 的机制有所了解。反射被称为java的黑暗面并不是没有道理的。它完全打破了OOP范式。在java中,封装用于隐藏和限制某些程序组件对其他程序组件的访问。通过使用 private 修饰符,我们意味着只能在该字段存在的类中访问该字段,基于此我们构建了程序的进一步架构。在本文中,我们了解了如何使用反射来实现目标。架构解决方案形式的一个很好的例子是生成设计模式 - Singleton。它的主要思想是,在程序的整个运行过程中,实现这个模板的类应该只有一份。这是通过将构造函数的默认访问修饰符设置为 private 来完成的。如果某个程序员用自己的反思创建了这样的类,那将是非常糟糕的。顺便说一句,我最近从我的员工那里听到一个非常有趣的问题:实现模板的类可以有Singleton继承人吗?难道在这种情况下连反思都无能为力了吗?在评论中写下您对文章的反馈和答案,也可以提出您的问题!Reflection API 的真正威力来自于与运行时注释的结合,我们可能会在以后关于 Java 阴暗面的文章中讨论这一点。感谢您的关注!
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION