Почему класс, содержащий вызов метода для отсутствующего интерфейса в неиспользуемом коде, вызывает ошибку загрузки класса Java? - PullRequest
15 голосов
/ 18 ноября 2011

Я вижу поведение загрузки классов, которое кажется несовместимым со спецификацией JVM, и мне интересно, если это ошибка.Или, если нет, надеясь, что кто-то может объяснить, почему.

Пример кода, приведенный ниже, просто выводит привет из основного метода.У него есть неиспользуемый метод, который содержит вызов метода для метода, который объявляет, что в качестве аргумента он принимает «C» (который является интерфейсом).

Когда выполняется основное (без A, Bи C в пути к классам) выдается ошибка ClassNotFound для интерфейса C. (Примечание C фактически никогда не требуется во время выполнения, поскольку на него ссылаются только в методе, который никогда не выполняется).

Похоже, что это нарушение спецификации JVM

Раздел 2.17.1 спецификации Java VM, 2-е издание, гласит:

Единственное требование относительно того, когдаРазрешение выполняется следующим образом: любые ошибки, обнаруженные во время разрешения, должны возникать в той точке программы, в которой программа предпринимает какие-либо действия, которые могут прямо или косвенно потребовать связи с классом или интерфейсом, участвующим в ошибке * 1015.*

Раздел 2.17.3 спецификации Java VM, 2-е издание, гласит:

Язык программирования Java обеспечивает гибкость реализации при связывании действий (и, посколькурекурсии, загрузки), при условии, что семантика языка соблюдается, что класс или интерфейс полностью проверены и подготовлены до их инициализации, и что ошибок, обнаруженных во время связывания, генерируются в точке программыгде программа предпринимает определенные действия, которые могут потребоватьlass или интерфейс, связанный с ошибкой .

Примечание. Если я изменю тип параметра в определении на класс вместо интерфейса, то код загружается и выполняется правильно.

/**
 * 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 {}

Ed,

Я не пытался умышленно вырвать цитату из контекста, я вытащил то, что мне показалось релевантным.Спасибо, что помогли мне попытаться понять это.

Во всяком случае, спецификация кажется мне достаточно ясной.Он говорит, что ошибки должны быть выброшены в точку, а не в точку .Конечно, я прочитал спецификацию VM после прочтения следующего в Главе 8 «Внутри виртуальной машины Java», так что, возможно, это изменило мою интерпретацию.

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

Как описано в главе 7 «Время жизни класса», различные реализации виртуальной машины Java могут выполнять разрешение в разное время в течениевыполнение программы.Реализация может решить связать все заранее, следуя всем символическим ссылкам из начального класса, затем всем символическим ссылкам из последующих классов, пока каждая символическая ссылка не будет разрешена.В этом случае приложение будет полностью связано до того, как будет вызван его метод main ().Этот подход называется ранним разрешением.Альтернативно, реализация может выбрать ожидание до самой последней минуты, чтобы разрешить каждую символическую ссылку.В этом случае виртуальная машина Java будет разрешать символическую ссылку только тогда, когда она впервые используется запущенной программой.Этот подход называется поздним разрешением.Реализации могут также использовать стратегию разрешения между этими двумя крайностями.

Хотя реализация виртуальной машины Java имеет некоторую свободу выбора, когда разрешать символьные ссылки, каждая виртуальная машина Java должна создавать внешнее впечатление, что она использует позднее разрешение .Независимо от того, когда конкретная виртуальная машина Java выполняет свое разрешение, она всегда будет выдавать любую ошибку, которая возникает в результате попытки разрешить символическую ссылку в точке выполнения программы, где символическая ссылка фактически использовалась впервые.Таким образом, пользователю всегда будет казаться, что разрешение было поздно.Если виртуальная машина Java выполняет раннее разрешение и во время раннего разрешения обнаруживает, что файл класса отсутствует, она не сообщит об отсутствии файла класса, выдав соответствующую ошибку в программу до тех пор, пока что-то в этом файле класса фактически не используется.Если класс никогда не используется программой, ошибка никогда не будет выдана.

Ответы [ 2 ]

6 голосов
/ 18 ноября 2011

Вот более простой пример, который также дает сбой.

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
5 голосов
/ 19 ноября 2011

Ваша цитата из Раздел 2.17.1 была массово вне контекста. Это выделено жирным шрифтом ниже. При чтении в контексте становится ясно, что « ошибки ... должны быть выданы в точке программы ... » означает, что « ошибки ... должны быть выданы к моменту достижения программы». точка ...". Само предложение может быть сформулировано лучше - но оно не само по себе.

Шаг разрешения необязателен во время начальной привязки. реализация может разрешить символическую ссылку из класса или интерфейс, который подключается очень рано, даже до точки разрешение всех символических ссылок из классов и интерфейсов, которые далее рекурсивно (Это разрешение может привести к ошибки при дальнейшей загрузке и связывании шагов.) Эта реализация выбор представляет собой одну крайность и похож на статический связь, которая была сделана в течение многих лет в простых реализациях язык C.

Вместо этого реализация может решить разрешить символическую ссылку только когда это действительно используется; последовательное использование этой стратегии для всех символические ссылки будут представлять «самую ленивую» форму разрешения. В этом случае, если Терминатор имел несколько символических ссылок на другой класс, ссылки могут быть решены по одному или, возможно, нет вообще, если эти ссылки никогда не использовались во время выполнения программа.

Единственное требование относительно того, когда выполняется разрешение, это любые ошибки, обнаруженные во время разрешения, должны быть выброшены в точку программа, в которой программа выполняет определенные действия, которые могут непосредственно или косвенно, требуется связь с классом или интерфейсом, участвующим в ошибка. В «статическом» примере реализации описан выбор ранее ошибки загрузки и компоновки могли произойти до того, как программа выполняется, если они задействовали класс или интерфейс, упомянутый в классе Терминатор или любой из следующих, рекурсивно ссылающихся классов и интерфейсы. В системе, которая реализовала «самое ленивое» разрешение, эти ошибки будут выдаваться только при использовании символической ссылки.

Два последующих предложения дают очень ясное значение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...