Enum disassembled with javap doesn't show constructor arguments
Asked Answered
V

1

7

When I disassemble an enum with javap, the enum's implicit constructor arguments seem to be missing, and I can't figure out why.

Here's an enum:

enum Foo { X }

I compile and disassemble this (on Java 8u60) with this command:

javac Foo.java && javap -c -p Foo

And here is the output I get:

final class Foo extends java.lang.Enum<Foo> {
  public static final Foo X;

  private static final Foo[] $VALUES;

  public static Foo[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[LFoo;
       3: invokevirtual #2                  // Method "[LFoo;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[LFoo;"
       9: areturn

  public static Foo valueOf(java.lang.String);
    Code:
       0: ldc           #4                  // class Foo
       2: aload_0
       3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #4                  // class Foo
       9: areturn

  private Foo(); // <--- here
    Code:
       0: aload_0
       1: aload_1
       2: iload_2
       3: invokespecial #6                  // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V
       6: return

  static {};
    Code:
       0: new           #4                  // class Foo
       3: dup
       4: ldc           #7                  // String X
       6: iconst_0
       7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #9                  // Field X:LFoo;
      13: iconst_1
      14: anewarray     #4                  // class Foo
      17: dup
      18: iconst_0
      19: getstatic     #9                  // Field X:LFoo;
      22: aastore
      23: putstatic     #1                  // Field $VALUES:[LFoo;
      26: return
}

My confusion is with the private constructor used to instantiate each enum constant. The disassembly shows that it takes no arguments (private Foo();), but it surely does take arguments. For example, you can see the load instructions reading the passed enum constant name and ordinal, as well as the this pointer, and passing them on to the superclass constructor, which requires them. The code in the static initializer block also shows that it pushes those arguments onto the stack before calling the constructor.

Now I would have assumed this was just an obscure bug in javap, but when I compile exactly the same enum with Eclipse's compiler and disassemble that using javap, the constructor is exactly the same except the arguments are shown:

final class Foo extends java.lang.Enum<Foo> {
  public static final Foo X;

  private static final Foo[] ENUM$VALUES;

  static {};
    Code:
       0: new           #1                  // class Foo
       3: dup
       4: ldc           #12                 // String X
       6: iconst_0
       7: invokespecial #13                 // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #17                 // Field X:LFoo;
      13: iconst_1
      14: anewarray     #1                  // class Foo
      17: dup
      18: iconst_0
      19: getstatic     #17                 // Field X:LFoo;
      22: aastore
      23: putstatic     #19                 // Field ENUM$VALUES:[LFoo;
      26: return

  private Foo(java.lang.String, int); // <--- here
    Code:
       0: aload_0
       1: aload_1
       2: iload_2
       3: invokespecial #23                 // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V
       6: return

  public static Foo[] values();
    Code:
       0: getstatic     #19                 // Field ENUM$VALUES:[LFoo;
       3: dup
       4: astore_0
       5: iconst_0
       6: aload_0
       7: arraylength
       8: dup
       9: istore_1
      10: anewarray     #1                  // class Foo
      13: dup
      14: astore_2
      15: iconst_0
      16: iload_1
      17: invokestatic  #27                 // Method java/lang/System.arraycopy:(Ljava/lang/Object;ILjava/lang/Object;II)V
      20: aload_2
      21: areturn

  public static Foo valueOf(java.lang.String);
    Code:
       0: ldc           #1                  // class Foo
       2: aload_0
       3: invokestatic  #35                 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #1                  // class Foo
       9: areturn
}

My question is: what physically is different between a javac-compiled enum and an Eclipse-compiled enum that causes javap to not show the constructor arguments for the javac-compiled enum? And is this difference a bug (in javap, in javac, or Eclipse)?

Valais answered 28/9, 2015 at 18:3 Comment(6)
At a guess, the setting of the -g flag (controls generation of debugging info).Hyperplasia
@Hyperplasia Good hypothesis but I compared -g (generate all debugging info) and -g:none (generate no debugging info) and it seems to make no difference.Valais
Did you try the -v (verbose) flag? It should at least show you the descriptor of the constructor. My guess is that javac marks the first parameters MANDATED, which might make javap omit them.Launch
@Launch Interesting. The "descriptor" does show the arguments (descriptor: (Ljava/lang/String;I)V), but I still don't understand what's causing the difference between the javac and Eclipse enums.Valais
As I said, javap probably ignores MANDATED parameters. I suppose Eclipse doesn't generate the modifier, while javac does.Launch
@Launch when I try that, the only flag shown is ACC_PRIVATE.Hyperplasia
M
3

The parameters and return type of a method inside a class file are described by a method descriptor.

With the introduction of generics in 1.5. additional information was introduced into the class file format, the method signature.

The "method descriptor" is used to describe the method after type erasure, the "method signature" additionally contains the generic type information.

Now javap prints out the method signature (which contains more information), and when the -v flag is set, it also prints the descriptor.

This reveals that also the constructor of the javac generated enum class has a method descriptor with parameter types String and int. Now it is also clear why both the Elipse and javac generated code work. Both call the private constructor with arguments String and int.

What still needs to be explained: Why does javac create a signature which differs from the descriptor at all - no generics are involved?

Anyway, the behaviour of javac regarding the enum constructor has caused other troubles and an bug report for javac was filed:

There is no need for an enum declaration's constructor to have a Signature attribute storing a method signature if 1) the constructor isn't generic and 2) its formal parameter types are neither parameterized types nor type variables. It's a bug if javac expects a Signature attribute for the constructor written above.

The following comments and the classification of the case suggest that this is a real bug in javac.

Mcvey answered 28/9, 2015 at 19:32 Comment(2)
Thanks, this explains the difference. It turns out that javac does include explicitly defined enum constructor parameters in the signature, just not the two implicit ones for the constant name and ordinal. I'm not convinced it's a bug in javac though, but more of a deficiency in the VM spec. The VM spec's rules for the signature attribute, for when it is supposed to be present and what it is supposed to contain, seem to be hazily defined.Valais
A similar issue is with inner classes' constructors, which have a hidden parameter used to pass the reference to the outer class. For example: class Outer { class Inner { Inner(){} } } – the constructor appears in the disassembly as: Outer$Inner(Outer);. But if the constructor is made generic: class Outer { class Inner { <T>Inner(){} } }, it gets a signature attribute in the classfile and javap disassembles it as <T> Outer$Inner();the parameter is suddenly invisible, and this is true with both javac and ECJ. Weird stuff! At least the -v option reveals the truth!Valais

© 2022 - 2024 — McMap. All rights reserved.