在本文中,我将向您介绍一种Java程序员只在看似不可能的情况下使用的能力。Java的黑暗面是反射API
在Java中,反射是使用java reflection api实现的。
什么是Java反射?
互联网上有一个简短、准确和流行的定义。反射(从拉丁语reflexio晚期到turn-back)是一种在程序运行时探索有关它的数据的机制。反射允许您探索有关字段、方法和类构造函数的信息。
反射允许您处理编译时不存在但在运行时可用的类型。反射和发布错误信息的逻辑一致模型使创建正确的动态代码成为可能。
换句话说,了解反射在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
。name字段本身用private access
修饰符标记:我们不能在类本身之外访问它,这意味着我们无法检索它的值。
“那有什么问题吗?”你说。”添加一个getter
或更改access
修饰符”。你是对的,除非MyClass在一个编译的AAR库中,或者在另一个不能进行更改的私有模块中。实际上,这种情况经常发生。一些粗心的程序员只是忘了写getter
。这正是回忆反射的时候!
让我们尝试获取MyClass类的private name
字段:
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应用程序中的类和接口。我们将不讨论类和类加载器之间的关系,因为这不是本文的主题。(关于类加载引起的问题感兴趣的可以参考之前的一篇文章:https://javakk.com/466.html)
接下来,要检索这个类的字段,需要调用getFields()
方法。此方法将返回此类的所有可访问字段。这对我们不起作用,因为我们的字段是私有的,所以我们使用getDeclaredFields()
方法。此方法还返回一个类字段数组,但现在它包含私有字段和受保护字段。在本例中,我们知道我们感兴趣的字段的名称,因此可以使用getDeclaredField(String)
方法,其中String
是所需字段的名称。
注意:getFields()
和getDeclaredFields()
不返回父类的字段!
我们有一个引用我们名字的Field对象。因为这个领域不是公开的,我们必须允许使用它。setAccessible(true)
方法允许我们进一步进行。现在名称字段完全在我们的控制之下!您可以通过调用Field
对象的get(object)
方法来检索它的值,其中object是MyClass
类的一个实例。我们将类型转换为String并将值赋给name变量。
如果找不到setter
来为name字段设置新值,可以使用set
方法:
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也是MyClass
类的一个实例。arg是该方法的参数,但我们没有参数。现在我们使用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
}
现在我们可以访问类的私有方法了。但是如果方法确实有参数,为什么构造函数被注释掉了呢?一切都在适当的时候。
从一开始的定义中可以清楚地看到,反射允许您在运行时(在程序运行时)创建类的实例!我们可以使用类的全名创建一个对象。类的全名是类名,包括其包的路径。
在我的包层次结构中,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
类,那么负责将类加载到JVM中的ClassLoader永远不会加载该类。这意味着您必须强制类加载器加载它并以类变量的形式获取类描述。这就是为什么我们有forName(String)
方法,其中String是我们需要其描述的类的名称。
获取Сlass对象后,调用newInstance()
方法将返回使用该描述创建的对象对象。剩下的就是把这个对象提供给我们的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技术就不能没有反射。例如,依赖注入(Dependency Injection,DI),它将注释与方法和构造函数的反射结合起来,形成了Android开发中流行的Darer库。
反射是Java的黑暗面。它完全打破了OOP范式。在Java中,封装隐藏并限制其他人对某些程序组件的访问。当我们使用private
修饰符时,我们希望只从该字段存在的类中访问该字段。我们基于这个原则构建了程序的后续架构。在本文中,我们了解了如何使用反射在任何地方强制执行。
独创性设计模式Singleton就是一个很好的例子,它是一个架构解决方案。基本思想是,在整个程序的执行过程中,实现此模式的类只有一个实例。这是通过将私有访问修饰符添加到默认构造函数来实现的。如果程序员使用反射来创建更多此类类的实例,那将是非常糟糕的。
这篇文章讲解了反射的利弊:https://javakk.com/728.html
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/755.html
暂无评论