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

考虑以下两个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编译器跳过编译时依赖性检查?

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

你能举一个例子,在没有执行编译时依赖性检查的情况下,在技术上不可能生成有效的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规范中的类文件格式 。

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

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

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

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

答案是,不 – 它不能。 根据Java语言规范 ,方法签名必须采用类型,这些类型在别处定义为在编译时可解析。

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

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

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

如果你在某些情况下你必须编译(并调用一个方法)在一个适用于库的东西上你无法访问最接近你可以通过reflection得到方法的东西,例如(从内存调用方法,可能不准确)

  void foo(Object suspectedFoobar) { try{ Method m = suspectedFoobar.getClass().getMethod("foobarMethod"); m.invoke(suspectedFoobar); } ... } 

但是,我无法真正看到这样做的重点。 您能否提供有关您要解决的问题的更多信息?

编译一个类而不查看它所依赖的类的类型签名是违反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应用程序。

如果这样做,当类加载器注意到签名不IncompatibleClassChangeError时,应用程序将失败并出现IncompatibleClassChangeError

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

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

Java按设计进行编译时依赖性检查,并使用它不仅可以确定类型,还可以在重载时确定方法调用。 我知道没办法。

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

提取界面

 pkg.in.classpath.IFooBar 

使FooBar implements IFooBar

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

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

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

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就无法判断你的意思。

我创建了两个类: 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."":()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."":()V 4: return public void doSomething(); Code: 0: return public void doSomethingElse(); Code: 0: return } 

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

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