编译时间与运行时间依赖性

Java 中编译时和运行时依赖项之间的区别是什么? 它与类路径有关,但是它们有什么不同呢?

69084 次浏览

编译器需要正确的类路径来编译对库的调用(编译时依赖项)

JVM 需要正确的类路径来加载您调用的库中的类(运行时依赖项)。

They may be different in a couple of ways:

1)如果你的类 C1调用库类 L1,而 L1调用库类 L2,那么 C1在运行时依赖于 L1和 L2,但是只有编译时依赖于 L1。

2)如果类 C1使用 Class.forName ()或其他机制动态实例化接口 I1,并且接口 I1的实现类是类 L1,那么 C1在运行时依赖于 I1和 L1,但是只有编译时依赖于 I1。

编译时和运行时相同的其他“间接”依赖项:

3)您的类 C1扩展库类 L1,而 L1实现接口 I1并扩展库类 L2: C1对 L1、 L2和 I1有编译时依赖关系。

4)你的类 C1有一个方法 foo(I1 i1)和一个方法 bar(L1 l1),其中 I1是一个接口,L1是一个带有一个参数的类,这个参数就是接口 I1: C1在编译时依赖于 I1和 L1。

基本上,要做任何有趣的事情,您的类需要与类路径中的其他类和接口进行接口。由这组库 接口形成的类/接口图生成编译时依赖链。库 实施产生运行时依赖链。注意,运行时依赖链是依赖于运行时的或者失败-缓慢的: 如果 L1的实现有时依赖于实例化类 L2的一个对象,并且该类只在一个特定的场景中被实例化,那么除了这个场景之外就没有依赖了。

Java 实际上在编译时不链接任何东西。它只使用在 CLASSPATH 中找到的匹配类验证语法。直到运行时,所有内容才会在那时基于 CLASSPATH 放在一起并执行。

编译时依赖项只是在编译的类中使用 直接的依赖项(其他类)。运行时依赖关系涵盖正在运行的类的直接依赖关系和间接依赖关系。因此,运行时依赖关系包括依赖关系的依赖关系和任何反射依赖关系,如 String中的类名,但在 Class#forName()中使用。

An easy example is to look at an api like the servlet api. To make your servlets compile, you need the servlet-api.jar, but at runtime the servlet container provides a servlet api implementation so you do not need to add servlet-api.jar to your runtime class path.

  • 编译时依赖项 : 您需要 CLASSPATH中的依赖项来编译工件。它们之所以产生,是因为您在代码中对依赖项有某种“引用”,比如对某个类调用 new,扩展或实现某些东西(直接或间接) ,或者使用直接 reference.method()符号调用方法。

  • 运行时依赖项 : 您需要 CLASSPATH中的依赖项来运行工件。它们的产生是因为您执行了访问依赖项的代码(以硬编码的方式或通过反射或其他方式)。

虽然编译时依赖性通常意味着运行时依赖性,但您可以只有编译时依赖性。这是基于这样一个事实: Java 只在第一次访问该类时链接类依赖项,所以如果在运行时从未访问过特定类,因为从未遍历过代码路径,那么 Java 将忽略该类及其依赖项。

举个例子

在 C.java 中(生成 C.class) :

package dependencies;
public class C { }

在 A.java 中(生成 A.class) :

package dependencies;
public class A {
public static class B {
public String toString() {
C c = new C();
return c.toString();
}
}
public static void main(String[] args) {
if (args.length > 0) {
B b = new B();
System.out.println(b.toString());
}
}
}

在这种情况下,A在编译时依赖于 CB,但是如果在执行 java dependencies.A时传递一些参数,它在运行时依赖于 C,因为 JVM 只会在执行 B b = new B()时尝试解决 BC的依赖。此特性允许您在运行时仅提供在代码路径中使用的类的依赖项,而忽略构件中其余类的依赖项。

对于 Java,编译时依赖性是源代码的依赖性。例如,如果 A 类从 B 类调用一个方法,那么 A 在编译时依赖于 B,因为 A 必须知道要编译的 B (B 类型)。这里的诀窍应该是: 编译的代码还不是一个完整的、可执行的代码。它包括尚未编译或存在于外部 jar 中的源的可替换地址(符号、元数据)。在链接期间,这些地址必须替换为内存中的实际地址。要做到这一点,正确的符号/地址应该创建。这可以通过类(B)的类型来实现。我相信这是编译时的主要依赖项。

运行时依赖性与实际的控制流更为相关。它涉及到实际的内存地址。这是程序运行时的一个依赖项。这里需要类 B 的详细信息,比如实现,而不仅仅是类型信息。如果该类不存在,那么您将得到 RuntimeException,并且 JVM 将退出。

这两种依赖关系通常和不应该流向同一个方向,但这是面向对象设计的问题。

在 C + + 中,编译稍有不同(不仅仅是及时编译) ,但是它也有一个链接器。所以我猜这个过程可能被认为类似于 Java。

从@Jason S 的回答中,我得出了另一个词,以防有帮助:

应用程序的 runtime dependency实际上是这个应用程序的编译时依赖项(L1)的依赖项(我们称之为 L2)。如果应用程序不使用它,它可能不会被声明为依赖项。

  • 如果 L2碰巧被应用程序使用(通过 L1) ,而没有声明为依赖项,则会出现 NoClassDefFoundError。

  • 如果 L2被声明为应用程序的编译时依赖项,而不是在运行时使用,那么它会使 jar 变得更大,编译时间也会超过所需的时间。

将 L2声明为运行时依赖项允许 JVM 只在需要时延迟加载它。