具有没有任何构造函数的 JVM 字节码类是否有效?

2022-09-03 14:05:33

AFAIK,在Java中,隐式构造函数总是为没有构造函数[1][2]的类生成。

但是在字节码中,我无法在JVMS上找到这样的限制。

所以:

  • 根据JVMS定义一个没有构造函数的类是否有效,只是为了使用它的静态方法,就像下面的jasmin hello world一样?

  • 除了无法创建它的实例之外,它还会产生任何进一步的后果吗?我将无法用于初始化实例,根据 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.10.2.4(不能使用未初始化的对象),这会变得无用。invokespecialnew

茉莉代码:

.class public Main
.super java/lang/Object
.method public static main([Ljava/lang/String;)V
    .limit stack 2
    getstatic java/lang/System/out Ljava/io/PrintStream;
    ldc "Hello World!"
    invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
    return
.end method

也就是说,没有构造函数:

.method public <init>()V
    aload_0
    invokenonvirtual java/lang/Object/<init>()V
    return
.end method

?

运行 with 会得到预期的输出。java MainHello World!

我已经检查了输出,与Java不同,我没有生成默认构造函数。javap -vjasmin

无论如何,我也试图打电话看看会发生什么:new Main();

public class TestMain {
    public static void main(String[] args) {
        Main m = new Main();
    }
}

并且如预期的那样,它给出了编译错误。如果我将构造函数添加到茉莉花中,那么就可以工作了。cannot find symbolTestMain

的输出完整性:javap -v

public class Main
  minor version: 0
  major version: 46
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Utf8               Main.j
   #2 = Class              #17            // Main
   #3 = NameAndType        #21:#23        // out:Ljava/io/PrintStream;
   #4 = Utf8               ([Ljava/lang/String;)V
   #5 = Utf8               java/lang/Object
   #6 = Class              #5             // java/lang/Object
   #7 = Utf8               Hello World!
   #8 = Class              #16            // java/io/PrintStream
   #9 = String             #7             // Hello World!
  #10 = Class              #19            // java/lang/System
  #11 = Utf8               Code
  #12 = Utf8               main
  #13 = Fieldref           #10.#3         // java/lang/System.out:Ljava/io/PrintStream;
  #14 = Utf8               SourceFile
  #15 = NameAndType        #18:#22        // println:(Ljava/lang/String;)V
  #16 = Utf8               java/io/PrintStream
  #17 = Utf8               Main
  #18 = Utf8               println
  #19 = Utf8               java/lang/System
  #20 = Methodref          #8.#15         // java/io/PrintStream.println:(Ljava/lang/String;)V
  #21 = Utf8               out
  #22 = Utf8               (Ljava/lang/String;)V
  #23 = Utf8               Ljava/io/PrintStream;
{
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #9                  // String Hello World!
         5: invokevirtual #20                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}
SourceFile: "Main.j"

如果有人可以用javac生成它(特别是no nor ),那将是有效性的一个很好的论据。ACC_INTERFACEACC_SYNTHETIC


答案 1

这是合法的。JVMS没有说不是这样。

有时,Java 编译器甚至会创建这样的类,以便为内部类创建访问器构造函数

class Foo {
  { new Bar(); }
  class Bar() {
    private Bar() { }
  }
}

为了使外部 clasd 可以访问此私有构造函数,Java 编译器向内部类添加了一个包私有构造函数,该构造函数将随机创建的无构造函数类的实例作为其单个参数。此实例始终为 null,并且访问器仅调用无参数构造函数而不使用参数。但是由于无法命名构造函数,因此这是避免与其他构造函数发生碰撞的唯一方法。为了保持类文件最小,不添加构造函数。

附带说明:始终可以在没有构造函数的情况下创建类的实例。例如,这可以通过放弃反序列化来实现。如果使用 Jasmin 定义一个没有实现接口的构造函数的类,则可以手动创建一个字节流,该字节流类似于该类(如果已序列化)。您可以反序列化此类并接收它的实例。Serializable

在 Java 中,调用对象分配的构造函数是两个独立的步骤。这甚至通过创建实例的字节码来暴露。类似的东西由两个直觉表示new Object()

NEW java/lang/Object
INVOKESPECIAL java/lang/Object <init> ()V

第一个是分配,第二个是构造函数的调用。JVM的验证器总是在使用实例之前检查是否调用了构造函数,但从理论上讲,JVM完全能够分离两者,正如反序列化(或内部调用VM,如果序列化不是一个选项)所证明的那样。


答案 2

你自己已经回答了这个问题:根据JVMS,没有构造函数的类是绝对有效的。你不能用纯Java编写这样的类,但它可以使用字节码生成来构造。

想想接口:从JVM的角度来看,它们也是没有构造函数的类。它们还可以具有静态成员(您甚至可以从命令行调用接口的方法)。main