繁体   English   中英

为什么包含方法的类在未使用的代码中调用缺少的接口会导致Java类加载错误?

[英]Why does a class containing a method call to a missing Interface within unused code cause a Java class loading error?

我看到一些类加载行为似乎与JVM规范不一致,我想知道这是否是一个错误。 或者如果没有,希望有人可以解释原因。

下面的示例代码只是从其main方法打印hello。 它有一个未使用的方法,它包含对方法的方法调用,该方法声明它将'C'(作为接口)作为参数。

当执行main时(类路径中没有A,B和C) ,接口C将抛出ClassNotFound错误。(因为它仅在永不执行的方法中引用,因此在运行时实际上不需要注释C)。

这似乎违反了JVM规范

Java VM Spec的第2.17.1节第2版说:

关于何时执行解析的唯一要求是, 在解析期间检测到的任何错误都必须抛到程序中的某个点,程序可能会直接或间接地需要链接到错误中涉及的类或接口。

Java VM Spec的第2.17.3节第2版说:

Java编程语言允许实现灵活性,以便何时发生链接活动(以及由于递归,加载),只要语言的语义得到尊重,类或接口在初始化之前完全验证和准备,并且在链接期间检测到的错误被抛出到程序中的某个点,在该点上程序可能需要链接到错误中涉及的类或接口

注意:如果我将定义中的参数类型更改为类而不是接口,则代码会正确加载和执行。

/**
 * This version fails, the method call in neverCalled() is to a method whose 
 * parameter definition is for an Interface
 */
public class Main {

    public  void neverCalled(){
          A a = new A();
          B b = new B(); // B implements C

          //method takeInter is declared to take paramters of type Interface C
          //This code is causes a ClassNotFound error do be thrown when Main
          //is loaded if A, B, and C is not in the class path
          a.takeInter(b); 
    }

    public static void main(String[] args) {
        System.out.println("Hello...");
    }
}


/**
 * This version runs, the method call in neverCalled() is to a method whose 
 * parameter definition is for a Class
 */
public class Main {

    public  void neverCalled(){
          A a = new A();
          B b = new B(); // B implements C

          //method takeInter is declared to take paramters of type Interface C
          //This code is causes a ClassNotFound error do be thrown when Main
          //is loaded if A, B, and C is not in the class path
          a.takeClass(b); 
    }

    public static void main(String[] args) {
        System.out.println("Hello...");
    }
}


public class A {
    public void takeClass(B in){};
    public void takeInter(C in){}
}

public class B implements C {}

public interface C {}

埃德,

我没有故意试图脱离背景我引出了我认为相关的部分。 感谢您帮助我尝试理解这一点。

无论如何,这个规范对我来说似乎很清楚。 它说的错误必须一个点不点抛出。 当然,我在阅读了Java虚拟机内部的第8章中的以下内容后阅读了VM规范,所以也许这对我的解释有所了解。

来自, http://www.artima.com/insidejvm/ed2/linkmod.html

如第7章“类的生命周期”中所述,允许Java虚拟机的不同实现在程序执行期间的不同时间执行解析。 实现可以选择通过遵循来自初始类的所有符号引用,然后跟随后续类中的所有符号引用来预先链接所有内容,直到每个符号引用都已被解析。 在这种情况下,应用程序将在调用main()方法之前完全链接。 这种方法称为早期解决方案。 或者,实现可以选择等到最后一分钟来解析每个符号引用。 在这种情况下,Java虚拟机仅在运行程序首次使用时才会解析符号引用。 这种方法称为延迟解决方案。 实现也可以在这两个极端之间使用解决策略。

虽然Java虚拟机实现在选择何时解析符号引用方面有一定的自由度,但是每个Java虚拟机都必须给出外部印象,即它使用后期解析 无论特定Java虚拟机何时执行其解析, 它总是会抛出任何错误,这些错误是在第一次实际使用符号引用的程序执行点尝试解析符号引用时产生的 通过这种方式,用户将始终看起来好像分辨率很晚。 如果Java虚拟机提前解析,并且在早期解析期间发现缺少类文件,则在实际使用该类文件中的某些内容时,它将不会通过抛出相应的错误来报告该类文件。 如果程序从不使用该类,则永远不会抛出该错误。

这是一个更简单的例子也失败了。

public class Main {
    public void neverCalled() {
        A a = new A();
        B b = new B();
        a.takeInter(b);
    }

    public static void main(String[] args) {
        System.out.println("Hello...");
    }
}

class A {
    public void takeInter(A in) {
    }
}

class B extends A {
}

class C {
}

在字节码中

public void neverCalled();
 Code:
   0: new           #2                  // class A
   3: dup           
   4: invokespecial #3                  // Method A."<init>":()V
   7: astore_1      
   8: new           #4                  // class B
  11: dup           
  12: invokespecial #5                  // Method B."<init>":()V
  15: astore_2      
  16: aload_1       
  17: aload_2       
  18: invokevirtual #6                  // Method A.takeInter:(LA;)V
  21: return   

b隐式地转换为A ,似乎需要检查它。

如果关闭所有验证,则不会发生错误。

$ rm A.class B.class C.class 
$ java -Xverify:none -cp . Main
Hello...
$ java -cp . Main
Exception in thread "main" java.lang.NoClassDefFoundError: A

您在第2.17.1节中的引用 大量脱离了上下文。 它在下面以粗体显示。 在上下文中阅读时,很明显“ 错误......必须在程序中的某一点抛出...... ”意味着“ 错误......必须在程序达到某一点时抛出...... ”。 这句话本身可能措辞得更好 - 但它本身并不是

在初始链接时,解决步骤是可选的。 实现可以解析来自非常早期链接的类或接口的符号引用,甚至可以递归地解析来自进一步引用的类和接口的所有符号引用。 (此解决方案可能会因进一步加载和链接步骤而导致错误。)此实现选择代表一种极端,类似于在C语言的简单实现中多年来所做的静态链接。

相反,实现可以选择仅在实际使用时解析符号引用; 对所有符号引用一致使用此策略将代表“最懒惰”的分辨率形式。 在这种情况下,如果Terminator具有对另一个类的多个符号引用,则可以一次解析一个引用,或者可能根本不解析引用,如果在执行程序期间从未使用过这些引用。

关于何时执行解析的唯一要求是,在解析期间检测到的任何错误都必须抛到程序中的某个点,程序可能会直接或间接地需要链接到错误中涉及的类或接口。 。 在前面描述的“静态”示例实现选择中,加载和链接错误可能在程序执行之前发生,如果它们涉及类Terminator中提到的类或接口或任何其他递归引用的类和接口。 在实现“最懒惰”分辨率的系统中,仅在使用符号引用时才会抛出这些错误。

后两句话的含义非常清楚。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM