【发布时间】:2011-05-15 07:57:33
【问题描述】:
Java 中的编译时依赖和运行时依赖有什么区别? 和类路径有关,但是它们有什么区别呢?
【问题讨论】:
标签: java runtime classpath compile-time
Java 中的编译时依赖和运行时依赖有什么区别? 和类路径有关,但是它们有什么区别呢?
【问题讨论】:
标签: java runtime classpath compile-time
从@Jason S 的回答中,我用其他词得出我的答案,以防万一:
应用的运行时依赖实际上是该应用的编译时依赖(L1)的依赖(我们称之为L2)。如果应用程序不使用它,则它可能不会被声明为依赖项。
如果 L2 恰好被应用程序(通过 L1)使用但未声明为依赖项,则会出现 NoClassDefFoundError。
如果 L2 被声明为应用程序的编译时依赖项,而不是在运行时使用,它会毫无用处地使 jar 更大并且编译时间比需要的长。
将 L2 声明为运行时依赖项允许 JVM 仅在需要时延迟加载它。
【讨论】:
对于 Java,编译时依赖是源代码的依赖。例如,如果 A 类从 B 类调用一个方法,那么 A 在编译时依赖于 B,因为 A 必须知道要编译的 B(B 的类型)。这里的诀窍应该是:编译后的代码还不是完整的可执行代码。它包括尚未编译或存在于外部 jar 中的源的可替换地址(符号、元数据)。在链接期间,这些地址必须替换为内存中的实际地址。要正确执行此操作,应创建正确的符号/地址。这可以通过类 (B) 的类型来完成。我相信这是编译时的主要依赖。
运行时依赖与实际的控制流更相关。它涉及实际的内存地址。这是您在程序运行时所拥有的依赖项。您需要此处的 B 类详细信息,例如实现,而不仅仅是类型信息。如果该类不存在,那么您将得到 RuntimeException 并且 JVM 将退出。
这两个依赖项通常和不应该流向相同的方向。这是面向对象设计的问题。
在 C++ 中,编译有点不同(不仅仅是即时编译),但它也有一个链接器。所以我猜这个过程可能被认为类似于 Java。
【讨论】:
一个简单的例子是查看像 servlet api 这样的 api。要编译 servlet,您需要 servlet-api.jar,但在运行时 servlet 容器提供了 servlet api 实现,因此您无需将 servlet-api.jar 添加到运行时类路径中。
【讨论】:
编译时依赖项:您需要 CLASSPATH 中的依赖项来编译您的工件。它们的产生是因为您对代码中硬编码的依赖项有某种“引用”,例如为某个类调用 new,扩展或实现某些东西(直接或间接),或者使用直接 @987654323 的方法调用@ 表示法。
运行时依赖项:您需要 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 对 C 到 B 具有编译时依赖关系,但如果在执行 java dependencies.A 时传递一些参数,它只会对 C 有运行时依赖关系,因为JVM 只会在执行B b = new B() 时尝试解决B 对C 的依赖。此功能允许您在运行时仅提供您在代码路径中使用的类的依赖关系,而忽略工件中其余类的依赖关系。
【讨论】:
编译器需要正确的类路径才能编译对库的调用(编译时依赖)
JVM 需要正确的类路径才能加载您正在调用的库中的类(运行时依赖项)。
它们可能在以下几个方面有所不同:
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 类的对象,并且该类仅在一个特定场景中被实例化,那么除了该场景之外没有依赖关系。
【讨论】:
编译时依赖项只是您在正在编译的类中直接使用的依赖项(其他类)。运行时依赖项涵盖了您正在运行的类的直接和间接依赖项。因此,运行时依赖项包括依赖项的依赖项和任何反射依赖项,例如您在 String 中拥有但在 Class#forName() 中使用的类名。
【讨论】:
A 的A.jar、带有B extends A 的B.jar 和带有C extends B 的C.jar 那么C.jar 甚至取决于A.jar 上的编译时间虽然 C 对 A 的依赖是间接的。
Java 在编译时实际上并没有链接任何东西。它仅使用在 CLASSPATH 中找到的匹配类来验证语法。直到运行时,所有东西才被放在一起并基于当时的 CLASSPATH 执行。
【讨论】: