为什么要使用反射?


29

我是Java的新手。通过学习,我读到反射被用来调用类和方法,并知道哪些方法没有实现。

什么时候应该使用反射,使用反射和实例化对象以及以传统方式调用方法之间有什么区别?



10
发布前,请先完成您的研究工作。StackExchange(如@Jalayn所述)和网络上有很多有关反射的材料。我建议您阅读例如《反射Java教程》,如果还有其他具体问题,请回来阅读。
彼得Török

1
必须有一百万个骗子。
DeadMG

3
超过几个专业的程序员会回答“尽可能少,甚至永远不会”。
罗斯·帕特森

Answers:


38
  • 反射比仅按其名称调用方法要慢得多,因为反射必须检查字节码中的元数据,而不是仅使用预编译的地址和常量。

  • 反射功能也更强大:您可以检索a protectedfinalmember 的定义,删除保护并对其进行操作,就好像它已声明为可变的一样!显然,这破坏了该语言通常为您的程序提供的许多保证,并且可能非常非常危险。

这几乎解释了何时使用它。通常不。如果要调用方法,只需调用它即可。如果要变异成员,只需将其声明为可变成员即可,而不要落后于编译后台。

反射在现实世界中的一种有用用法是在编写必须与用户定义的类进行互操作的框架时,框架作者不知道成员(甚至是类)将是什么。反思使他们无需事先知道就可以处理任何课程。例如,我认为没有反射就不可能编写一个复杂的面向方面的库。

再举一个例子,JUnit过去使用了一些琐碎的反思:它枚举了类中的所有方法,假定所有被调用testXXX的方法都是测试方法,并且仅执行那些方法。但这现在可以通过注释来更好地完成,实际上,JUnit 4基本上已经转移到了注释。


6
“更强大”需要注意。您不需要进行反射即可获得图灵的完整性,因此,任何计算都不需要进行反射。当然,Turing complete对其他功能(如I / O功能以及反射)一无所知。
Steve314,2011年

1
反思并不一定要“慢得多”。您可以一次使用反射来生成直接调用包装器字节码。
SK-logic

2
您会在需要时知道它。我经常想知道为什么(在语言生成之外)需要它。然后,突然间,我做到了……当我从其他开发人员那里获得面板以进入我维护的系统之一时,不得不在父母/子女链上走来走去/ oke取数据。
Brian Knoblauch

@ SK-logic:实际上,生成字节码根本不需要反射(实际上反射根本不包含用于字节码操作的API!)。
约阿希姆·绍尔

1
@JoachimSauer,当然,但是您需要一个反射API来加载此生成的字节码。
SK-logic

15

我曾经像你一样,对反射了解不多-仍然不了解-但我确实使用过一次。

我有一个带有两个内部类的类,每个类都有很多方法。

我需要调用内部类中的所有方法,而手动调用它们将耗费大量精力。

使用反射,我可以仅用2-3行代码来调用所有这些方法,而不用调用方法本身的数量。


4
为什么要下票?
Mahmoud Hossam

1

1
@MahmoudHossam也许不是最佳实践,但是您的回答说明了可以部署的一项重要策略。
ankush981

13

我将反射的使用分为三组:

  1. 实例化任意类。例如,在依赖项注入框架中,您可能声明接口ThingDoer由类NetworkThingDoer实现。然后,框架将找到NetworkThingDoer的构造函数并将其实例化。
  2. 编组和解组为其他格式。例如,将具有遵循bean约定的吸气剂和设置的对象映射到JSON,然后再次返回。该代码实际上并不知道这些字段或方法的名称,而只是检查该类。
  3. 将类包装在重定向层中(也许实际上并未加载List,而只是指向知道如何从数据库中获取信息的指针)或完全伪造类(jMock将创建一个实现接口的综合类)用于测试)。

这是我在StackExchange上发现的最佳反思。大多数答案都重复Java Trail所说的(“您可以访问所有这些属性”,而不是为什么),提供了一些使用反射做事的示例,这些事情在没有反射的情况下更容易完成,或者给出了一些关于Spring的模糊回答。使用它。这个答案实际上给出了三个有效的示例,JVM无法轻松地解决这些示例而无需进行反思。谢谢!
ndm13'1

3

反射允许程序处理可能不存在的代码,并以可靠的方式进行处理。

“普通代码”具有一些片段,例如片段URLConnection c = null,由于它们的绝对存在,它们导致类加载器加载URLConnection类,作为加载此类的一部分,并引发ClassNotFound异常并退出。

通过反射,您可以在启动依赖于它们的实际类之前,根据它们的名称以字符串形式加载类,并测试它们的各种属性(可用于控件外部的多个版本)。一个典型的示例是用于使Java程序在OS X下看起来本机的OS X特定代码,该代码在其他平台上不存在。


2

从根本上讲,反射意味着将程序的代码用作数据。

因此,当程序代码是有用的数据源时,使用反射可能是一个好主意。(但是需要权衡取舍,因此可能并不总是一个好主意。)

例如,考虑一个简单的类:

public class Foo {
  public int value;
  public string anotherValue;
}

并且您想从中生成XML。您可以编写代码以生成XML:

public XmlNode generateXml(Foo foo) {
  XmlElement root = new XmlElement("Foo");
  XmlElement valueElement = new XmlElement("value");
  valueElement.add(new XmlText(Integer.toString(foo.value)));
  root.add(valueElement);
  XmlElement anotherValueElement = new XmlElement("anotherValue");
  anotherValueElement.add(new XmlText(foo.anotherValue));
  root.add(anotherValueElement);
  return root;
}

但这是很多样板代码,每次更改类时,都必须更新代码。真的,您可以描述这段代码的作用

  • 用类名创建一个XML元素
  • 对于课程的每个属性
    • 用属性名称创建一个XML元素
    • 将属性的值放入XML元素
    • 将XML元素添加到根目录

这是一种算法,算法的输入是类:我们需要它的名称,以及它的属性的名称,类型和值。这就是反射的来源:它使您可以访问此信息。Java允许您使用Class类的方法检查类型。

其他一些用例:

  • 根据类的方法名称在Web服务器中定义URL,并根据方法参数定义URL参数
  • 将类的结构转换为GraphQL类型定义
  • 调用名称以“ test”开头的类的每个方法作为单元测试用例

但是,全反射不仅意味着查看现有代码(其本身被称为“自省”),而且还意味着修改或生成代码。Java中有两个主要的用例:代理和模拟。

假设您有一个接口:

public interface Froobnicator {
  void froobnicateFruits(List<Fruit> fruits);
  void froobnicateFuel(Fuel fuel);
  // lots of other things to froobnicate
}

并且您有一个实现一些有趣的实现:

public class PowerFroobnicator implements Froobnicator {
  // awesome implementations
}

实际上,您还有第二种实现:

public class EnergySaverFroobnicator implements Froobnicator {
  // efficient implementations
}

现在,您还需要一些日志输出;只要调用方法,您只需要一条日志消息。您可以将日志输出显式地添加到每个方法中,但这会很烦人,并且您必须执行两次。每个实现一次。(添加更多实现时,更多。)

相反,您可以编写代理:

public class LoggingFroobnicator implements Froobnicator {
  private Logger logger;
  private Froobnicator inner;

  // constructor that sets those two

  public void froobnicateFruits(List<Fruit> fruits) {
    logger.logDebug("froobnicateFruits called");
    inner.froobnicateFruits(fruits);
  }

  public void froobnicateFuel(Fuel fuel) {
    logger.logDebug("froobnicateFuel( called");
    inner.froobnicateFuel(fuel);
  }
  // lots of other things to froobnicate
}

同样,有一种重复的模式可以用算法描述:

  • 记录器代理是实现接口的类
  • 它有一个采用接口的另一个实现的构造函数和一个记录器
  • 界面中的每种方法
    • 该实现记录一条消息“已调用$ methodname”
    • 然后在内部接口上调用相同的方法,并传递所有参数

该算法的输入是接口定义。

反射允许您使用此算法定义一个新类。Java允许您使用java.lang.reflect.Proxy类的方法来执行此操作,并且有些库为您提供了更多功能。

那么反思的不利之处是什么?

  • 您的代码变得更难以理解。您是一种抽象级别,已从代码的具体效果中进一步删除。
  • 您的代码变得更难调试。特别是使用代码生成库,执行的代码可能不是您编写的代码,而是您生成的代码,并且调试器可能无法向您显示该代码(或让您放置断点)。
  • 您的代码变慢了。动态读取类型信息并通过其运行时句柄访问字段比硬编码访问要慢。动态代码生成可以减轻这种影响,但代价是更难以调试。
  • 您的代码可能变得更加脆弱。动态反射访问不由编译器进行类型检查,但会在运行时引发错误。

1

反射可以自动使程序的各个部分保持同步,而在以前,您必须手动更新程序才能使用新界面。


5
在这种情况下,您要付出的代价是丢失了编译器进行的类型检查和IDE中的重构安全性。我不愿意做出这样的权衡。
巴伦德
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.