编译Java类时禁用编译时依赖性检查

时间:2009-10-08 13:11:59

标签: java jvm bytecode javac

考虑以下两个Java类:

a.) class Test { void foo(Object foobar) { } }

b.) class Test { void foo(pkg.not.in.classpath.FooBar foobar) { } }

此外,假设在类路径中找不到pkg.not.in.classpath.FooBar

使用标准的javac编译第一个类。

但是,第二个类不会编译,javac会给你错误消息"package pkg.not.in.classpath does not exist"

错误消息在一般情况下很好,因为检查依赖项允许编译器告诉您是否有一些方法参数错误等等。

虽然很好,也很有帮助,但在编译时检查依赖项是AFAIK,而不是严格,需要在上面的例子中生成Java类文件。

  1. 您是否可以举例说明在没有执行编译时依赖性检查的情况下生成有效Java类文件在技术上是不可能的?

  2. 您是否知道有任何方法可以指示javac或任何其他Java编译器跳过编译时依赖性检查?

  3. 请确保您的答案解决了这两个问题。

8 个答案:

答案 0 :(得分:19)

  

你能举出一个例子,在没有执行编译时依赖性检查的情况下,在技术上不可能生成有效的Java类文件吗?

考虑以下代码:

public class GotDeps {
  public static void main(String[] args) {
    int i = 1;
    Dep.foo(i);
  }
}

如果目标方法具有签名public static void foo(int n),则将生成以下指令:

public static void main(java.lang.String[]);
  Code:
   0:   iconst_1
   1:   istore_1
   2:   iload_1
   3:   invokestatic    #16; //Method Dep.foo:(I)V
   6:   return

如果目标方法具有签名public static void foo(long n),则在方法调用之前int将被提升为long

public static void main(java.lang.String[]);
  Code:
   0:   iconst_1
   1:   istore_1
   2:   iload_1
   3:   i2l
   4:   invokestatic    #16; //Method Dep.foo:(J)V
   7:   return

在这种情况下,将无法生成调用指令或如何使用数字16填充类常量池中引用的CONSTANT_Methodref_info结构。请参阅VM中的class file format规范更多细节。

答案 1 :(得分:5)

我认为没有这样的方法 - 编译器需要知道参数的类,以便创建适当的字节码。如果找不到Foobar类,则无法编译Test类。

请注意,虽然您的两个类是功能等效的,因为您实际上并未使用该参数,但它们并不相同,并且在编译时会产生不同的字节码。

所以你的前提 - 编译器在这种情况下不需要找到要编译的类 - 是不正确的。

编辑 - 您的评论似乎在问“编译器不能忽略这一事实并生成 适合的字节码吗?”

答案是不 - 它不能。 According to the Java Language Specification,方法签名必须采用elsewhere defined类型才能在编译时解析。

这意味着虽然创建一个可以满足您要求的编译器在机械上非常简单,但它会违反JLS,因此在技术上不会是Java编译器。此外,绕过编译时安全对我来说听起来不是一个很好的卖点......: - )

答案 2 :(得分:2)

我看不出如何在不破坏java类型检查的情况下允许这样做。您将如何在方法中使用引用的对象?为了扩展你的例子,

class test {
   void foo (pkg.not.in.classpath.FooBar foobar) { 
       foobar.foobarMethod(); //what does the compiler do here?
  } 
}

如果你在某些情况下你需要编译(并调用一个方法)在一个可以在库上工作的东西你无法访问最接近你可以通过反射来获取方法,类似的东西(来自内存的方法调用,可能不准确)

 void foo(Object suspectedFoobar)
     {
       try{
        Method m = suspectedFoobar.getClass().getMethod("foobarMethod");
        m.invoke(suspectedFoobar);
       }
       ...
     }
但是,我无法真正看到这样做的重点。您能否提供有关您要解决的问题的更多信息?

答案 3 :(得分:2)

编译一个类而不查看它所依赖的类的类型签名是违反JLS的。没有符合要求的Java编译器允许你这样做。

然而......有可能做一些相似的事情。具体来说,如果我们有A类和B类依赖于A,则可以执行以下操作:

  1. 编译A.java
  2. 针对A.class编译B.java。
  3. 编辑A.java以不兼容的方式更改它。
  4. 编译A.java,替换旧的A.class。
  5. 使用B.class和新的(不兼容的)A.class运行Java应用程序。
  6. 如果执行此操作,当类加载器注意到签名不兼容时,应用程序将失败并显示IncompatibleClassChangeError

    实际上,这说明了为什么编译忽略依赖关系会是一个坏主意。如果运行具有不一致字节码文件的应用程序(仅),将报告检测到的第一个不一致。因此,如果你有很多不一致的地方,你需要多次运行你的应用程序来“检测”它们。实际上,如果在应用程序或其任何依赖项中存在任何类的动态加载(例如,使用Class.forName()),则这些问题中的一些可能不会立即显示。

    总之,在编译时忽略依赖关系的成本将是较慢的Java开发和较不可靠的Java应用程序。

答案 4 :(得分:1)

Java by design进行编译时依赖性检查,不仅用于确定类型,还用于在重载时确定方法调用。我知道无处可去。

可以做什么(并且为JDBC驱动程序完成)是通过使用反射来延迟依赖性检查。您可以从Class.forName获取类,而编译器在编译时不知道该类。但是,一般来说,这意味着代码被写入接口,并且在运行时加载了一个实现接口的类。

答案 5 :(得分:0)

提取界面

pkg.in.classpath.IFooBar

制作FooBar implements IFooBar

class Test { void foo(pkg.in.classpath.IFooBar foobar) {} }

您的Test类将被编译。只需使用工厂和配置在运行时中插入正确的实现即FooBar即可。寻找一些 IOC容器

答案 6 :(得分:0)

关于你唯一能做的就是使用一些字节码操作将其转换为更具体的类型。

Java语法中没有任何内容可以使用pkg.not.in.classpath.FooBar来区分它:

 package pkg.not.in.classpath;
 public class FooBar { }

来自:

 package pkg.not.in.classpath;
 class FooBar { }

所以只有你的话,在那里使用FooBar是合法的。

源包中的包范围类和内部类之间也存在歧义:

class pkg {
    static class not {
        static class in {
            static class classpath {
                static class FooBar {}
            }
        }
    }
}

内部类在源代码中也称为pkg.not.in.classpath.FooBar,但在类文件中将被称为pkg$not$in$classpath$FooBar而不是pkg/not/in/classpath/FooBar。如果没有在类路径中查找它,javac就无法判断你的意思。

答案 7 :(得分:0)

我创建了两个类:CallerCallee

public class Caller {
    public void doSomething( Callee callee) {
        callee.doSomething();
    }

    public void doSame(Callee callee) {
        callee.doSomething();
    }

    public void doSomethingElse(Callee callee) {
        callee.doSomethingElse();
    }
}

public class Callee {
    public void doSomething() {
    }
    public void doSomethingElse() {
    }
}

我编译了这些类,然后用javap -c Callee > Callee.bcjavap -c Caller > Caller.bc对它们进行了反汇编。这产生了以下结果:

Compiled from "Caller.java"
public class Caller extends java.lang.Object{
public Caller();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return

public void doSomething(Callee);
Code:
0: aload_1
1: invokevirtual #2; //Method Callee.doSomething:()V
4: return

public void doSame(Callee);
Code:
0: aload_1
1: invokevirtual #2; //Method Callee.doSomething:()V
4: return

public void doSomethingElse(Callee);
Code:
0: aload_1
1: invokevirtual #3; //Method Callee.doSomethingElse:()V
4: return

}

Compiled from "Callee.java"
public class Callee extends java.lang.Object{
public Callee();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return

public void doSomething();
Code:
0: return

public void doSomethingElse();
Code:
0: return

}

编译器为方法调用'callee'生成了一个方法签名和一个类型安全invokevirtual调用 - 它知道在这里调用什么类和方法。如果该类不可用,编译器将如何生成方法签名或“invokevirtual”?

有一个JSR(JSR 292)来添加一个支持动态调用的'invokedynamic'操作码,但JVM目前不支持这种操作。