编译时间与运行时相关性-Java


Answers:


77
  • 编译时依赖性:需要依赖项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对一个编译时依赖C通过B,但只会对下的运行时间依赖性,如果你通过执行时一些参数java dependencies.A,因为JVM将只尝试解决B的依赖上C时,它得到执行B b = new B()。此功能允许您在运行时仅提供在代码路径中使用的类的依赖关系,而忽略工件中其余类的依赖关系。


1
我知道这是一个很老的答案,但是JVM如何从一开始就不具备C作为运行时依赖项?如果它能够识别“这是对C的引用,是时候将其添加为依赖项了”,那么C实质上就不是依赖项,因为JVM可以识别并知道它在哪里?
wearebob

@wearebob我猜可能是用这种方式指定的,但是他们认为延迟链接更好,并且我个人出于上述原因也同意:它允许您在必要时使用一些代码,但并不强迫您将其包括在其中。您不需要的部署。在处理第三方代码时,这非常方便。
gpeche

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

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

33

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


为了澄清(这使我感到困惑),如果您使用的是maven并进行战争,则“ servlet-api”通常是“提供”的依赖关系,而不是“运行时”的依赖关系,如果这样,它将被包含在战争中我是对的
xdhmoore

2
“提供”是指在编译时包括在内,但不要将其捆绑在WAR或其他依赖项集合中。“运行时”的作用与此相反(在编译时不可用,但与WAR打包在一起)。
KC Baltz

29

编译器需要正确的类路径才能编译对库的调用(编译时间相关性)

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
示例1中的编译时依赖关系不应该是L1吗?
BalusC 2010年

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

1
我很确定默认的类加载器会采用类路径并按顺序进行操作,因此,如果类路径中有两个jar都包含相同的类(例如com.example.fooutils.Foo),它将使用一个在类路径中是第一个。要么那样,要么您会得到一个错误,指出歧义。但是,如果您想要更多有关类加载器的信息,则应该提出一个单独的问题。
杰森S

我认为在第一种情况下,编译时间相关性也应该存在于L2上,即句子应为:1)如果您的类C1调用库类L1,而L1调用库类L2,则C1对L1具有运行时依赖性,并且L2,但仅依赖于L1和L2的编译时间。就是这样,就像在编译时java编译器验证L1一样,它还会验证L1引用的所有其他类(不包括诸如Class.forName(“ myclassname)之类的动态依赖项)...否则它将如何验证编译工作正常,请解释一下是否
同意

1
不需要。您需要阅读有关Java中编译和链接如何工作的信息。当编译器引用一个外部类时,关心的只是如何使用该类,例如其方法和字段是什么。无关紧要在该外部类的方法中实际发生什么。如果L1调用L2,则这是L1的实现细节,并且L1已在其他地方编译。
詹森·S

12

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


直到加载时间...运行时才不同于加载时间。
兑换

10

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


谢谢,但是类加载在运行时如何工作?在编译时很容易理解。但是在运行时,如果我有两个不同版本的Jar,它如何工作?如果一个类路径中有多个不同类的类,则Class.forName()将使用哪个类?
Kunal

匹配名称的课程。如果您实际上是指“同一类的多个版本”,则取决于类加载器。将加载“最近”的一个。
BalusC 2010年

好吧,我想如果您将A.jar与 A,B.jar与B extends AC.jar一起使用,C extends B则C.jar取决于A.jar的编译时间,即使C对A的依赖是间接的。
gpeche

1
所有编译时相关性的问题都是接口相关性(接口是通过类的方法,接口的方法,还是通过包含类或接口参数的方法)
Jason S,2010年

2

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

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

两种依赖关系(通常也不应)遵循相同的方向。不过,这是面向对象设计的问题。

在C ++中,编译有些不同(不是实时的),但是它也有一个链接器。因此,我认为该过程可能与Java类似。

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.