编译时依赖与运行时依赖 - Java

126

Java中编译时依赖和运行时依赖有什么区别?它们与类路径有关,但又有何不同之处?

8个回答

108
  • 编译时依赖: 你需要在 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 才会尝试解决 BC 的依赖关系。该功能使您能够仅在代码路径中使用的类的运行时提供其依赖项,并忽略工件中其他类的依赖关系。

2
我知道这是一个非常老的答案,但JVM如何从一开始就没有C作为运行时依赖关系呢?如果它能够识别“这里有一个对C的引用,是时候将其添加为依赖项”,那么C不是已经成为依赖项了吗?因为JVM已经识别它并知道它在哪里。 - wearebob
@wearebob 我猜本来可以这样规定的,但他们决定采用懒惰链接,就个人而言,我同意上述原因:它允许你在必要时使用一些代码,但如果不需要,它不会强制你将其包含在部署中。当处理第三方代码时,这非常方便。 - gpeche
如果我已经在某个地方部署了一个 JAR 文件,它就必须包含所有的依赖项。它不知道是否将使用参数运行它(所以它不知道是否会使用 C),因此无论如何都必须有 C 可用。我只是不明白为什么从一开始就没有将 C 放在类路径上可以节省任何内存/时间。 - wearebob
1
@wearebob 一个JAR文件不需要包含所有的依赖项。这就是为什么几乎每个非平凡应用程序都有一个/lib目录或类似的目录,其中包含多个JAR文件的原因。 - gpeche
1
@wearebob。这个问题涉及到软件架构和应用程序生命周期。请考虑公共API和服务实现。编译/运行时的概念也反映在像Gradle这样的构建工具中。将“实现”视为可交换的服务代码。在简单的应用程序中,编译和运行时代码库通常来自于一个超级JAR文件。对于可能经历多次发布的企业应用程序而言,情况更加复杂,因为您必须升级依赖项。编译/运行时有助于维护向后兼容性。希望这可以帮助到您。 - Vortex

39

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

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
谢谢,但运行时类加载是如何工作的?在编译时很容易理解。但是在运行时,如果我有两个不同版本的 Jars,它会如何表现?它会选择哪一个? - Kunal
1
我非常确定默认的类加载器会获取类路径并按顺序遍历它,因此如果类路径中有两个包含相同类(例如com.example.fooutils.Foo)的JAR文件,则会使用在类路径中排名较高的那个。否则,您将收到一个声明歧义的错误。但是,如果您想要更多关于类加载器的特定信息,您应该提出一个单独的问题。 - Jason S
我认为在第一种情况下,编译时依赖项也应该存在于L2上,即句子应该是:1)如果您的类C1调用库类L1,并且L1调用库类L2,则C1对L1和L2具有运行时依赖性,但仅对L1和L2具有编译时依赖性。这是因为在编译时,当Java编译器验证L1时,它还会验证L1引用的所有其他类(不包括像Class.forName(“myclassname”)这样的动态依赖项)...否则它如何验证编译是否正常工作。如果您有不同看法,请解释一下。 - Rajesh Goel
2
不行,你需要了解Java中编译和链接的工作原理。当编译器引用外部类时,它只关心如何使用该类,例如它的方法和字段是什么。它并不关心外部类的方法实际上发生了什么。如果L1调用L2,那是L1的实现细节,并且L1已经在其他地方编译过了。 - Jason S
在这个答案的第二段中,它是加载时间依赖性,而不是运行时依赖性。运行时依赖性纯粹通过反射。 - overexchange

38

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


1
为了澄清一下(这让我感到困惑),如果您正在使用Maven构建WAR文件,"servlet-api"通常是一个"provided"依赖项而不是"runtime"依赖项,否则它将被包含在WAR文件中,如果我没记错的话。 - xdhmoore
3
“provided”指编译时包含,但不将其捆绑在WAR或其他依赖项集合中。“runtime”则相反(在编译时不可用,但打包在WAR中)。 - KC Baltz

14

Java在编译时实际上并不会链接任何内容,它只使用CLASSPATH中找到的匹配类来验证语法。直到运行时,才会根据当时的CLASSPATH将所有内容组合并执行。


直到加载时间才会出现...运行时与加载时间不同。 - overexchange

13

编译时依赖只包括在编译当前类时直接使用的依赖(其他类)。运行时依赖则同时覆盖了你运行的类直接和间接使用的依赖。因此,运行时依赖包含了依赖的依赖以及所有反射依赖(例如在String中使用的类名,但是在Class#forName()中被使用)。


谢谢,但是类加载在运行时如何工作呢?在编译时很容易理解。但是在运行时,如果我有两个不同版本的Jars,它会产生什么影响呢?如果类路径中有多个不同类的类,在Class.forName()方法中会选用哪个类呢? - Kunal
匹配课程名称的那个。如果你真的意思是“同一类的多个版本”,那么这取决于类加载器。将会加载“最接近”的那一个。 - BalusC
我认为,如果你有带有A的A.jar,带有B extends A的B.jar和带有C extends B的C.jar,那么即使C对A的依赖是间接的,C.jar在编译时仍然依赖于A.jar。 - gpeche
1
所有编译时依赖项的问题都在于接口依赖性(无论接口是通过类的方法、接口的方法还是包含类或接口参数的方法来实现的)。 - Jason S

4
对于Java来说,编译时依赖是指源代码的依赖关系。例如,如果A类调用B类的方法,则A在编译时依赖于B,因为A必须知道B的类型才能进行编译。这里的诀窍应该是:编译后的代码还不是完整且可执行的代码。它包括未编译或存在于外部jar中的源代码的可替换地址(符号、元数据)。在链接期间,这些地址必须被实际内存中的地址所替换。要正确地执行此操作,需要创建正确的符号/地址。这可以通过类(B)的类型来完成。我认为这是编译时的主要依赖关系。
运行时依赖关系更多地涉及实际的控制流程。它涉及实际的内存地址。这是程序运行时所具有的依赖关系。在这里,您需要B类的详细信息,如实现方式,而不仅仅是类型信息。如果该类不存在,则会出现RuntimeException,并且JVM将退出。
通常情况下,这两种依赖关系应该流向相同方向。这是OO设计的问题。
在C++中,编译过程略有不同(不是即时编译),但也有一个链接器。因此,这个过程可能与Java类似。

0

从@Jason S的答案中,我用其他话语得出了我的答案,以防有所帮助:

应用程序的运行时依赖项实际上是该应用程序的编译时依赖项(称为L1)的依赖项(我们称其为L2)。如果不会被应用程序使用,则可能不会声明它作为依赖项。

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

  • 如果将L2声明为应用程序的编译时依赖项,并且在运行时未使用,则它会使jar文件变得更大,编译时间比需要的时间长。

将L2声明为运行时依赖项允许JVM在需要时才进行惰性加载。


0

编译时

编译时是Java源代码被编译的阶段。在这个阶段,Java源代码被编写并由Java编译器(javac)进行编译。在编译过程中,会检查你的代码并检测错误的存在。如果你的代码有编译错误,编译器会通知你这些错误,并需要在运行代码之前修复这些错误。

编译过程中的错误可以帮助你更好地理解你的代码。因为这些错误指示了变量的错误使用、缺失或多余的括号、不正确的类型转换以及其他潜在的编码错误。这使得你的代码更易于文档化和编辑。

编译时也是将你的代码转换为字节码的阶段。字节码被组装成可以由Java虚拟机(JVM)执行的格式。一旦编译过程完成,你的代码就准备好运行了。

从某种程度上说,这个阶段表明Java是一种编译语言,因为在这个阶段所做的就是将代码转换为字节码的过程。

运行时

运行时指的是你的Java程序运行的时刻。当你运行Java程序时,你的代码由JVM执行。在这个阶段,你的代码被实现并产生结果。

在运行时也有可能发生错误。然而,这些类型的错误与编译时的错误不同。编译时错误是在编译代码时检测到的,而运行时错误仅在程序执行时发生。运行时错误可能是由于用户输入不正确或超出内存限制而发生。
在运行时阶段,我们运行字节码。我们的运行代码表明Java是一种解释型语言。在解释型语言中,代码是逐行读取或根据特殊表达式执行的,无需预先编译。
Java是一种既编译又解释的编程语言。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接