Java中编译时依赖和运行时依赖有什么区别?它们与类路径有关,但又有何不同之处?
Java中编译时依赖和运行时依赖有什么区别?它们与类路径有关,但又有何不同之处?
编译时依赖: 你需要在 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
通过 B
编译时依赖于 C
,但只有在执行 java dependencies.A
时,如果传递了一些参数,它才会在运行时依赖于C。因为当执行 B b = new B()
时,JVM 才会尝试解决 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类的对象,并且该类只在一个特定场景下被实例化,则除了在该场景下没有依赖。
一个简单的例子是看一个像servlet api这样的api。为了使您的servlet编译,您需要servlet-api.jar,但在运行时,servlet容器提供servlet api实现,因此您不需要将servlet-api.jar添加到运行时类路径中。
Java在编译时实际上并不会链接任何内容,它只使用CLASSPATH中找到的匹配类来验证语法。直到运行时,才会根据当时的CLASSPATH将所有内容组合并执行。
编译时依赖只包括在编译当前类时直接使用的依赖(其他类)。运行时依赖则同时覆盖了你运行的类直接和间接使用的依赖。因此,运行时依赖包含了依赖的依赖以及所有反射依赖(例如在String
中使用的类名,但是在Class#forName()
中被使用)。
A
的A.jar,带有B extends A
的B.jar和带有C extends B
的C.jar,那么即使C对A的依赖是间接的,C.jar在编译时仍然依赖于A.jar。 - gpeche从@Jason S的答案中,我用其他话语得出了我的答案,以防有所帮助:
应用程序的运行时依赖项实际上是该应用程序的编译时依赖项(称为L1)的依赖项(我们称其为L2)。如果不会被应用程序使用,则可能不会声明它作为依赖项。
如果L2恰好被应用程序(通过L1)使用,而未声明为依赖项,则会出现NoClassDefFoundError。
如果将L2声明为应用程序的编译时依赖项,并且在运行时未使用,则它会使jar文件变得更大,编译时间比需要的时间长。
将L2声明为运行时依赖项允许JVM在需要时才进行惰性加载。
编译时
编译时是Java源代码被编译的阶段。在这个阶段,Java源代码被编写并由Java编译器(javac)进行编译。在编译过程中,会检查你的代码并检测错误的存在。如果你的代码有编译错误,编译器会通知你这些错误,并需要在运行代码之前修复这些错误。
编译过程中的错误可以帮助你更好地理解你的代码。因为这些错误指示了变量的错误使用、缺失或多余的括号、不正确的类型转换以及其他潜在的编码错误。这使得你的代码更易于文档化和编辑。
编译时也是将你的代码转换为字节码的阶段。字节码被组装成可以由Java虚拟机(JVM)执行的格式。一旦编译过程完成,你的代码就准备好运行了。
从某种程度上说,这个阶段表明Java是一种编译语言,因为在这个阶段所做的就是将代码转换为字节码的过程。
运行时
运行时指的是你的Java程序运行的时刻。当你运行Java程序时,你的代码由JVM执行。在这个阶段,你的代码被实现并产生结果。
在运行时也有可能发生错误。然而,这些类型的错误与编译时的错误不同。编译时错误是在编译代码时检测到的,而运行时错误仅在程序执行时发生。运行时错误可能是由于用户输入不正确或超出内存限制而发生。