也许你在日常生活中遇到过“反射”的概念。这个词通常指的是学习自己的过程。在编程中,它具有类似的含义——它是一种机制,用于分析有关程序的数据,甚至可以在程序运行时更改程序的结构和行为。 这里重要的是我们在运行时而不是编译时执行此操作。但是为什么要在运行时检查代码呢?毕竟,您已经可以阅读代码了:/ 反射的概念可能不会立即清晰,这是有原因的:到目前为止,您始终知道自己在使用哪些类。例如,您可以编写一个
Cat
类:
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 +
'}';
}
}
你知道关于它的一切,你可以看到它的字段和方法。假设你突然需要向程序中引入其他动物类。为了方便起见,您可能会创建一个带有Animal
父类的类继承结构。早些时候,我们甚至创建了一个代表兽医诊所的类,我们可以将一个Animal
对象(父类的实例)传递给它,程序根据动物是狗还是猫来适当地对待它。尽管这些不是最简单的任务,但程序能够在编译时了解有关类的所有必要信息。因此,当您将Cat
对象传递给main()
方法,程序已经知道它是一只猫,而不是一只狗。现在让我们假设我们面临着不同的任务。我们的目标是编写一个代码分析器。我们需要CodeAnalyzer
用一个方法创建一个类:void analyzeObject(Object o)
。这个方法应该:
- 确定传递给它的对象的类,并在控制台上显示类名;
- 确定传递类的所有字段的名称,包括私有字段,并将它们显示在控制台上;
- 确定传递类的所有方法的名称,包括私有方法,并将它们显示在控制台上。
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
}
}
现在我们可以清楚地看到这个任务与您之前解决的其他任务有何不同。对于我们当前的目标,困难在于我们和程序都不知道将传递给analyzeClass()
方法。如果您编写这样的程序,其他程序员将开始使用它,并且他们可能将任何东西传递给此方法——任何标准 Java 类或他们编写的任何其他类。传递的类可以有任意数量的变量和方法。换句话说,我们(和我们的程序)不知道我们将使用哪些类。但是,我们仍然需要完成这项任务。这就是标准 Java Reflection API 为我们提供帮助的地方。Reflection API 是该语言的一个强大工具。Oracle 的官方文档建议这种机制应该只由有经验的程序员使用,他们知道自己在做什么。您很快就会明白为什么我们要提前发出此类警告 :) 以下是您可以使用 Reflection API 执行的操作的列表:
- 识别/确定对象的类别。
- 获取有关类修饰符、字段、方法、常量、构造函数和超类的信息。
- 找出哪些方法属于已实现的接口。
- 创建一个类的实例,其类名在程序执行之前是未知的。
- 按名称获取和设置实例字段的值。
- 按名称调用实例方法。
如何识别/确定对象的类别
让我们从基础开始。Java 反射引擎的入口点是类Class
。是的,它看起来很有趣,但这就是反射 :) 使用类Class
,我们首先确定传递给我们方法的任何对象的类。让我们尝试这样做:
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));
}
}
控制台输出:
class learn.codegym.Cat
注意两件事。首先,我们特意将Cat
类放在一个单独的learn.codegym
包中。现在您可以看到该getClass()
方法返回类的全名。其次,我们将变量命名为clazz
。这看起来有点奇怪。称它为“类”是有意义的,但“类”是 Java 中的保留字。编译器不允许这样调用变量。我们必须以某种方式解决这个问题 :) 一开始还不错!我们在该功能列表中还有什么?
如何获取有关类修饰符、字段、方法、常量、构造函数和超类的信息。
现在事情变得越来越有趣了!在当前类中,我们没有任何常量或父类。让我们添加它们以创建完整的图片。创建最简单的Animal
父类:
package learn.codegym;
public class Animal {
private String name;
private int age;
}
我们将使我们的Cat
类继承Animal
并添加一个常量:
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
}
现在我们有了完整的画面!让我们看看反射的能力:)
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));
}
}
这是我们在控制台上看到的:
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)]
看看我们能够获得的所有详细的课程信息!不仅是公共信息,还有私人信息! 笔记: private
变量也显示在列表中。我们对课程的“分析”可以认为基本上是完整的:我们正在使用该analyzeObject()
方法来学习我们能学到的一切。但这并不是我们可以用反射做的一切。我们不局限于简单的观察——我们将继续采取行动!:)
如何创建一个类的实例,其类名在程序执行之前是未知的。
让我们从默认构造函数开始。我们Cat
班还没有,所以让我们添加它:
public Cat() {
}
Cat
下面是使用反射(createCat()
方法) 创建对象的代码:
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());
}
}
控制台输入:
learn.codegym.Cat
控制台输出:
Cat{name='null', age=0}
这不是错误:name
和的值age
显示在控制台上,因为我们编写了代码将它们输出到类toString()
的方法中Cat
。在这里,我们读取了一个类的名称,我们将从控制台创建其对象。该程序识别要创建其对象的类的名称。 为了简洁起见,我们省略了适当的异常处理代码,这将占用比示例本身更多的空间。在实际程序中,当然,您应该处理涉及输入错误名称等情况。默认构造函数非常简单,所以如您所见,使用它很容易创建类的实例:) 使用newInstance()
方法,我们创建了这个类的一个新对象。这是另一回事,如果Cat
构造函数将参数作为输入。让我们删除该类的默认构造函数并尝试再次运行我们的代码。
null
java.lang.InstantiationException: learn.codegym.Cat
at java.lang.Class.newInstance(Class.java:427)
出了些问题!我们得到了一个错误,因为我们调用了一个使用默认构造函数创建对象的方法。但是我们现在没有这样的构造函数。所以当newInstance()
方法运行时,反射机制使用我们带有两个参数的旧构造函数:
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
但是我们没有对参数做任何事情,就好像我们完全忘记了它们一样!使用反射将参数传递给构造函数需要一点“创造力”:
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());
}
}
控制台输出:
Cat{name='Fluffy', age=6}
让我们仔细看看我们的程序中发生了什么。我们创建了一个对象数组Class
。
Class[] catClassParams = {String.class, int.class};
它们对应于我们构造函数的参数(只有String
和int
参数)。我们将它们传递给clazz.getConstructor()
方法并获得对所需构造函数的访问权限。之后,我们需要做的就是使用newInstance()
必要的参数调用该方法,并且不要忘记将对象显式转换为所需的类型:Cat
。
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
现在我们的对象创建成功了!控制台输出:
Cat{name='Fluffy', age=6}
向右移动:)
如何通过名称获取和设置实例字段的值。
想象一下,您正在使用另一个程序员编写的类。此外,您无权对其进行编辑。比如打包成JAR的现成类库。您可以阅读类的代码,但不能更改它。假设在这个库中创建其中一个类(让它成为我们的旧Cat
类)的程序员,在设计定稿的前一晚没有睡够,删除了该age
字段的 getter 和 setter。现在这堂课已经来到你身边。它满足您的所有需求,因为您只需要Cat
程序中的对象。但是你需要他们有一个age
领域!这是一个问题:我们无法到达该字段,因为它有private
修饰符,getter 和 setter 被创建类的睡眠不足的开发人员删除了:/ 好吧,反射可以在这种情况下帮助我们!我们可以访问该类的代码Cat
,因此我们至少可以找出它有哪些字段以及它们的名称。有了这些信息,我们就可以解决我们的问题:
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());
}
}
如评论中所述,该name
字段的所有内容都很简单,因为类开发人员提供了一个设置器。您已经知道如何从默认构造函数创建对象:我们有newInstance()
为此。但是我们必须对第二个字段进行一些修补。让我们弄清楚这里发生了什么:)
Field age = clazz.getDeclaredField("age");
在这里,使用我们的Class clazz
对象,我们age
通过getDeclaredField()
方法访问该字段。它让我们将年龄字段作为Field age
对象。但这还不够,因为我们不能简单地为private
字段赋值。为此,我们需要使用以下setAccessible()
方法使该字段可访问:
age.setAccessible(true);
一旦我们对一个字段执行此操作,我们就可以分配一个值:
age.set(cat, 6);
如您所见,我们的Field age
对象有一种由内而外的 setter,我们向其传递一个 int 值和要分配其字段的对象。我们运行我们的main()
方法并看到:
Cat{name='Fluffy', age=6}
出色的!我们做到了!:) 让我们看看我们还能做些什么......
如何通过名称调用实例方法。
让我们稍微改变一下前面示例中的情况。假设Cat
类开发人员没有在 getter 和 setter 上出错。在这方面一切都很好。现在问题不同了:有一个我们确实需要的方法,但开发人员将其设为私有:
private void sayMeow() {
System.out.println("Meow!");
}
这意味着如果我们Cat
在我们的程序中创建对象,那么我们将无法调用sayMeow()
它们的方法。我们会有不会喵喵叫的猫吗?这很奇怪:/我们将如何解决这个问题?反射 API 再一次帮助了我们!我们知道我们需要的方法的名称。其他一切都是技术问题:
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();
}
}
在这里,我们做了很多与访问私有字段时相同的事情。首先,我们得到我们需要的方法。它被封装在一个Method
对象中:
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
该getDeclaredMethod()
方法让我们可以访问私有方法。接下来,我们使方法可调用:
sayMeow.setAccessible(true);
最后,我们调用所需对象的方法:
sayMeow.invoke(cat);
在这里,我们的方法调用看起来像一个“回调”:我们习惯于使用句点将对象指向所需的方法 ( cat.sayMeow()
),但是在使用反射时,我们将要调用的对象传递给方法那个方法。我们的控制台上有什么?
Meow!
一切正常!:) 现在您可以看到 Java 的反射机制为我们提供的巨大可能性。在困难和意想不到的情况下(比如我们的例子中的类来自一个封闭的库),它确实可以帮助我们很多。但是,与任何大国一样,它也带来了巨大的责任。Oracle 网站上的一个特殊部分描述了反射的缺点。主要有以下三个缺点:
-
性能更差。使用反射调用的方法比以正常方式调用的方法性能更差。
-
有安全限制。反射机制让我们可以在运行时改变程序的行为。但是在你的工作场所,当你在做一个真实的项目时,你可能会面临不允许这样做的限制。
-
内部信息泄露风险。理解反射是对封装原则的直接违反很重要:它让我们访问私有字段、方法等。我认为我不需要提及应该采用直接和公然违反 OOP 原则的行为仅在最极端的情况下,即由于您无法控制的原因而没有其他方法可以解决问题时。
GO TO FULL VERSION