Java中的编译时间和运行时依赖性有什么区别?

它与类路径有关,但它们有何不同?

编译时依赖:您需要CLASSPATH中的依赖项来编译工件。它们的产生是因为你对代码中的硬编码有一些"引用",例如为某些类调用new,扩展或实现某些东西(直接或间接),或者使用direct 符号。

运行时依赖性:您需要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 b = new B()时对C的依赖性。此功能允许您在运行时仅提供在代码路径中使用的类的依赖项,并忽略工件中其余类的依赖项。

我知道这是一个非常古老的答案,但是JVM怎么能从一开始就没有C作为运行时依赖?如果它能够识别"继承人对C的引用,时间将其添加为依赖关系",那么由于JVM识别它并且知道它在哪里,因此C本质上已经是一个依赖关系了吗?

@wearebob我可能已经指定了这种方式,但他们认为延迟链接更好,并且我个人同意上述原因:它允许您在必要时使用某些代码,但不会强制您将其包括在内如果您不需要它,请进行部署。在处理第三方代码时,这非常方便。

如果我在某处部署了jar,它已经必须包含所有依赖项。它不知道它是否将使用参数运行(因此它不知道是否将使用C),因此它必须以任何方式使用C。我只是看不到从一开始就没有C在类路径上节省了内存/时间。

@wearebob JAR不需要包含其所有依赖项。 这就是为什么几乎每个非平凡的应用程序都有一个/ lib目录或类似的包含多个JAR的原因。

一个简单的例子就是看一下像servlet api这样的api。要使servlet编译,需要servlet-api.jar,但在运行时servlet容器提供了一个servlet api实现,因此您不需要将servlet-api.jar添加到运行时类路径中。

为了澄清(这让我很困惑),如果你正在使用maven并建立一个战争,"servlet-api"通常是一个"提供"依赖,而不是"运行时"依赖,这将导致它被包含在战争中,如果我是对的。

提供的方法,包括在编译时,但不要将它捆绑在WAR或其他依赖集合中。运行时相反(在编译时不可用,但与WAR打包在一起)。

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

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的对象,并且该类仅在一个特定场景中实例化,那么除了在那个场景。

例1中的编译时依赖项不应该是L1吗?

呀,修了几个错别字。

谢谢,但是如何在运行时加载类?在编译时,它很容易理解。但是在运行时,如果我有两个不同版本的Jars,它是如何起作用的?它会挑选哪一个?

我非常确定默认的类加载器采用类路径并按顺序逐步执行它,因此如果类路径中有两个包含相同类的jar(例如com.example.fooutils.Foo),它将使用第一个在类路径中。无论是那个还是你都会得到一个错误陈述歧义。但是如果你想要更多关于类加载器的信息,你应该问一个单独的问题。

我认为在第一种情况下,编译时依赖性也应该存在于L2上,即句子应该是:1)如果你的类C1调用库类L1,而L1调用库类L2,那么C1对L1有一个运行时依赖性L2,但只对L1和L2的编译时依赖性。就是这样,在编译时也是当java编译器验证L1时,它也会验证L1引用的所有其他类(不包括动态依赖项,如Class.forName("myclassname))...否则它如何验证汇编工作正常。如果您不这么认真,请解释一下

不需要。您需要了解Java中的编译和链接是如何工作的。所有编译器在引用外部类时都关心的是如何使用该类,例如:它的方法和领域是什么。它并不关心外部类方法中实际发生的事情。如果L1调用L2,那就是L1的实现细节,而L1已经在其他地方编译过了。

在这个答案的第二段中,它是加载时间依赖性,而不是运行时依赖性。运行时依赖性纯粹是通过反射

Java在编译时实际上并没有链接任何内容。它仅使用它在CLASSPATH中找到的匹配类来验证语法。直到运行时才将所有内容组合在一起并在当时基于CLASSPATH执行。

它直到加载时...运行时与加载时不同。

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

谢谢,但是如何在运行时加载类?在编译时,它很容易理解。但是在运行时,如果我有两个不同版本的Jars,它是如何起作用的?如果类路径中有多个不同类的类,哪个类会被Class.forName()拾取?

与课程名称相匹配的那个。如果你的意思是"同一类的多个版本",那么它取决于类加载器。将加载"最接近的"。

好吧,我想如果你有A.jar与A,B.jar与B extends A和C.jar与C extends B然后C.jar依赖于A.jar上的编译时间,即使C对A的依赖是间接的。

@gpeche:是的。

所有编译时依赖项中的问题是接口依赖性(接口是通过类方法,还是通过接口方法,还是通过包含类或接口的参数的方法)

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

运行时依赖性与实际控制流更相关。它会侵入实际的内存地址。这是程序运行时的依赖关系。您需要B类详细信息,例如实现,而不仅仅是类型信息。如果该类不存在,那么您将获得RuntimeException并且JVM将退出。

这两种依赖关系,通常和不应该,流向相同的方向。这是OO设计的问题。

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