13

考虑以下两个 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"

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

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

  1. 你能举出任何例子,如果不执行编译时依赖检查,在技术上就不可能生成有效的 Java 类文件吗?

  2. 你知道有什么方法可以指示 javac 或任何其他 Java 编译器跳过编译时依赖检查吗?

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

4

8 回答 8

20

你能举出任何例子,如果不执行编译时依赖检查,在技术上就不可能生成有效的 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

在这种情况下,将无法生成调用指令或如何填充CONSTANT_Methodref_info类常量池中引用的数字 16 的结构。有关更多详细信息,请参阅 VM 规范中的类文件格式

于 2009-10-08T13:40:24.823 回答
6

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

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

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

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

答案是不——它不能。 根据 Java Language Specification,方法签名必须采用类型,这些类型在其他地方定义为在编译时可解析。

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

于 2009-10-08T13:18:00.313 回答
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);
       }
       ...
     }

不过,我真的看不出这样做的意义。您能否提供有关您要解决的问题的更多信息?

于 2009-10-08T13:16:49.007 回答
2

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

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

  1. 编译A.java
  2. 针对 A.class 编译 B.java。
  3. 编辑 A.java 以不兼容的方式对其进行更改。
  4. 编译 A.java,替换旧的 A.class。
  5. 使用 B.class 和新的(不兼容的)A.class 运行 Java 应用程序。

如果你这样做,IncompatibleClassChangeError当类加载器注意到签名不兼容时,应用程序将失败。

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

总之,在编译时忽略依赖项的代价是 Java 开发速度变慢,Java 应用程序可靠性降低。

于 2009-10-08T14:51:48.033 回答
1

Java 通过设计进行编译时依赖性检查,并且不仅使用它来确定类型,而且在重载时确定方法调用。我不知道有什么办法。

可以做的事情(例如 JDBC 驱动程序)是通过使用反射来延迟依赖性检查。您可以Class.forName在编译器不知道该类的情况下从编译时获取该类。然而,一般来说,这意味着将代码写入接口,并在运行时加载实现该接口的类。

于 2009-10-08T13:58:46.830 回答
0

提取界面

pkg.in.classpath.IFooBar

使FooBar implements IFooBar

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

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

于 2009-10-08T13:19:53.663 回答
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 无法分辨出您的意思。

于 2009-10-08T13:40:55.417 回答
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.bc和反汇编它们javap -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

}

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

有一个 JSR ( JSR 292 ) 可以添加一个支持动态调用的“invokedynamic”操作码,但是 JVM 目前不支持。

于 2009-10-08T14:53:46.173 回答