Почему класс, содержащий вызов метода для отсутствующего интерфейса в неиспользуемом коде, вызывает ошибку загрузки класса Java?
Я вижу поведение загрузки класса, которое, по-видимому, несовместимо с спецификацией JVM, и я задаюсь вопросом, является ли это ошибкой. Или если нет, надеясь, что кто-то сможет объяснить, почему.
Приведенный ниже примерный код просто печатает привет из основного метода. У него есть неиспользуемый метод, который содержит вызов метода для метода, который объявляет, что он принимает в качестве аргумента "C" (который является интерфейсом).
Когда main выполняется (без A, B и C в пути к классу) для интерфейса C. для класса C выбрана ошибка ClassNotFound. (Примечание C никогда не требуется на время выполнения, поскольку оно ссылается на метод, который никогда не выполняется).
Это, по-видимому, является нарушением спецификации JVM
Раздел 2.17.1 Java VM Spec, второе издание гласит:
Единственное требование, касающееся того, когда выполняется разрешение, заключается в том, что любые ошибки, обнаруженные во время разрешения, должны быть выброшены в точку в программе, где программа выполняет какое-либо действие, которое может прямо или косвенно требовать привязки к классу или интерфейс, участвующий в ошибке
Раздел 2.17.3 Java VM Spec, второе издание гласит:
Язык программирования 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 {}
Ed,
Я не намеренно пытался вывести цитату из контекста, я вытащил то, что я считал подходящей частью. Спасибо, что помогли мне понять это.
Во всяком случае, спецификация кажется мне совершенно понятной. В нем говорится, что ошибки должны быть выбраны в, а не точкой. Конечно, я прочитал спецификацию VM после прочтения следующего в главе 8 Inside Virtual Java, поэтому, возможно, это окрасило мою интерпретацию.
От, http://www.artima.com/insidejvm/ed2/linkmod.html
Как описано в главе 7 "Время жизни класса", различным реализациям виртуальной машины Java разрешено выполнять разрешение в разное время во время выполнения программы. Реализация может выбрать, чтобы связать все по фронту, следуя всем символическим ссылкам из исходного класса, затем все символические ссылки из последующих классов, пока каждая символическая ссылка не будет разрешена. В этом случае приложение будет полностью связано до того, как будет вызван метод main(). Такой подход называется ранним разрешением. В качестве альтернативы, реализация может решить подождать до последней минуты, чтобы разрешить каждую символическую ссылку. В этом случае виртуальная машина Java разрешит символическую ссылку только тогда, когда она будет сначала использоваться запущенной программой. Этот подход называется поздним разрешением. Реализации могут также использовать стратегию разрешения между этими двумя крайностями.
Несмотря на то, что реализация виртуальной машины Java имеет определенную свободу выбора, когда нужно разрешать символические ссылки, каждая виртуальная машина Java должна давать внешнее впечатление о том, что использует позднее разрешение. Независимо от того, когда конкретная виртуальная машина Java выполняет свое разрешение, , она всегда будет вызывать любую ошибку, которая возникает при попытке разрешить символическую ссылку в точке выполнения программы, где символическая ссылка была фактически использована в первый раз. Таким образом, пользователь всегда будет выглядеть так, как будто разрешение было запоздалым. Если виртуальная машина Java делает раннее разрешение, и во время раннего разрешения обнаруживает, что файл класса отсутствует, он не будет сообщать о файле класса, отсутствующем, бросая соответствующую ошибку до тех пор, пока в программе не будет использоваться что-то в этом файле класса. Если класс никогда не используется программой, ошибка никогда не будет выбрана.
Ответы
Ответ 1
Вот более простой пример, который также терпит неудачу.
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
Ваша цитата из раздел 2.17.1 была массово из контекста. Он выделен жирным шрифтом. При чтении в контексте ясно, что "ошибки... должны быть брошены в точку в программе..." означает, что "ошибки... должны быть выброшены к тому времени, когда программа достигнет точки...". Предложение само по себе может быть сформулировано лучше, но оно не само по себе.
Шаг разрешения является необязательным во время начальной привязки. реализация может разрешить символическую ссылку из класса или интерфейс, который связан очень рано, даже до точки разрешая все символические ссылки из классов и интерфейсов, которые далее ссылаются, рекурсивно. (Это разрешение может привести к ошибки от дальнейших действий по загрузке и связыванию.) Эта реализация выбор представляет собой одну крайность и похож на тип статического которые были сделаны в течение многих лет в простых реализациях язык C.
Реализация может вместо этого решить разрешить символическую ссылку только когда он фактически используется; последовательное использование этой стратегии для всех символические ссылки будут представлять собой "самую ленивую" форму разрешения. В этом случае, если у Терминатора было несколько символических ссылок на другое класс, ссылки могут быть разрешены по одному или, возможно, не вообще, если эти ссылки никогда не использовались во время выполнения Программа.
Единственное требование, касающееся выполнения разрешения, заключается в том, что любые ошибки, обнаруженные во время разрешения, должны быть выброшены в точку в программа, в которой некоторые действия принимаются программой, которая может, непосредственно или косвенно, требуют привязки к классу или интерфейсу, участвующим в ошибка. В примере "статический" пример реализации ранее ошибки загрузки и связывания могли возникнуть до того, как программа будет выполняются, если они связаны с классом или интерфейсом, упомянутым в классе Терминатор или любой из других, рекурсивно связанных классов и интерфейсы. В системе, которая реализовала "самую ленивую" резолюцию, эти ошибки будут выбрасываться только при использовании символической ссылки.
Два последующих предложения делают смысл очень понятным.