Java inner class inconsistency between descriptor

2020-07-09 06:35发布

问题:

I'm trying to understand if there's a reason in the spec for the discrepany between java descriptors and signatures for inner classes. (I'm looking directly at the content of the class files here, but I use javap to illustrate).

( n.b. I have tried this on JDK 1.6.0_33 and 1.7.0_05, both have the same issue when viewed with javap from Java 7 - java 6's javap doesn't seem to show any generic signature info, as per Sean's answer below. )

Update : Thanks to those discussing - my take is

  • The descriptor (which doesn't contain generic information) is correct.
  • The signature (which is an attribute of the method, and does contain generic information) is incorrect. The relevant ConstPool entry for SIGNATURE of the <init> method is " ConstantUTF8[(Ljava/util/list<TE;>)V] "
  • Javap in Java 6 doesn't LOOK at the signature, just the descriptor. (My guess!)

In case anyone wonders, I hit this without using JAVAP, just looking at the class files myself, I am only using javap to show it. (so it's unlikely to be a javap bug).

Consider:

public class InnerClassTest1 {

  public int getX() {
    return new Inner1(new ArrayList<String>()).getX(4);
  }

  public class Inner1 {
    private final List arg;

    public Inner1(List arg) {
        this.arg = arg;
    }....

vs

public class InnerClassTest2 {

   public int getX() {
      return new Inner1(new ArrayList<String>()).getX(4);
   }

   public class Inner1<E> {
    private final List<E> arg;

    public Inner1(List<E> arg) {
        this.arg = arg;
    }.....

If you look at the output of javap -cs on the inner classes, they're surprisingly different!

public org.benf.cfr.tests.InnerClassTest1$Inner1(org.benf.cfr.tests.InnerClassTest1, java.util.List); Signature: (Lorg/benf/cfr/tests/InnerClassTest1;Ljava/util/List;)V

vs

public org.benf.cfr.tests.InnerClassTest2$Inner1(java.util.List<E>); Signature: (Lorg/benf/cfr/tests/InnerClassTest2;Ljava/util/List;)V

... the one which uses generics is missing the implicit parameter for the outer class! (it's correctly present in InnerClassTest1).

I can't find anything in the class file documentation to explain this - does anyone know why this might be?

Thanks!

Lee.


Update -

I've placed example files at http://www.benf.org/files/innerClassTest.tgz

Given Sean's answer below, I tried using javap on java 6, and I saw identical output for both, with no generic information - this leads me to believe that java 6's javap is not displaying full signature information?

The exact output I get using javap on 1.7.0_05-b06 is

public class org.benf.cfr.tests.InnerClassTest2$Inner1<E> {
 final org.benf.cfr.tests.InnerClassTest2 this$0;
 Signature: Lorg/benf/cfr/tests/InnerClassTest2;

public org.benf.cfr.tests.InnerClassTest2$Inner1(java.util.List<E>);
 Signature: (Lorg/benf/cfr/tests/InnerClassTest2;Ljava/util/List;)V
 Code:
   0: aload_0       
   1: aload_1       
   2: putfield      #1                  // Field this$0:Lorg/benf/cfr/tests/InnerClassTest2;
   5: aload_0       
   6: invokespecial #2                  // Method java/lang/Object."<init>":()V
   9: aload_0       
  10: aload_2       
  11: putfield      #3                  // Field arg:Ljava/util/List;
  14: return        

public int getX(int);
  Signature: (I)I
  Code:
   0: iconst_2      
   1: ireturn       
}

回答1:

Using the code above and using JDK 1.6.0_33 I get the following output:

src\test>javap -c -s InnerClassTest1$Inner1
Compiled from "InnerClassTest1.java"
public class test.InnerClassTest1$Inner1 extends java.lang.Object{
final test.InnerClassTest1 this$0;
  Signature: Ltest/InnerClassTest1;

public test.InnerClassTest1$Inner1(test.InnerClassTest1, java.util.List);
  Signature: (Ltest/InnerClassTest1;Ljava/util/List;)V
  Code:
   0:   aload_0
   1:   aload_1
   2:   putfield        #1; //Field this$0:Ltest/InnerClassTest1;
   5:   aload_0
   6:   invokespecial   #2; //Method java/lang/Object."<init>":()V
   9:   aload_0
   10:  aload_2
   11:  putfield        #3; //Field arg:Ljava/util/List;
   14:  return

public int getX(int);
  Signature: (I)I
  Code:
   0:   iload_1
   1:   ireturn
}

src\test>javap -c -s InnerClassTest2$Inner1
Compiled from "InnerClassTest2.java"
public class test.InnerClassTest2$Inner1 extends java.lang.Object{
final test.InnerClassTest2 this$0;
  Signature: Ltest/InnerClassTest2;

public test.InnerClassTest2$Inner1(test.InnerClassTest2, java.util.List);
  Signature: (Ltest/InnerClassTest2;Ljava/util/List;)V
  Code:
   0:   aload_0
   1:   aload_1
   2:   putfield        #1; //Field this$0:Ltest/InnerClassTest2;
   5:   aload_0
   6:   invokespecial   #2; //Method java/lang/Object."<init>":()V
   9:   aload_0
   10:  aload_2
   11:  putfield        #3; //Field arg:Ljava/util/List;
   14:  return

public int getX(int);
  Signature: (I)I
  Code:
   0:   iload_1
   1:   ireturn

}

and the only differences are that my implementation has (to make the code compile):

    public int getX(int i) {
        return i;
    }

and the fact that your package name is probably different (org.benf.cfr.tests ?).

Other than that though, my output is pretty much the same. Are there any other differences in the code that might explain what you're seeing? From what I know about the compilation process and class files I wouldn't expect to see a difference in the output.

Good question - be interesting to find out why you're seeing this



回答2:

Using Javap on InnerClassTest2$Inner1 gives

Compiled from "InnerClassTest2.java"
public class org.benf.cfr.tests.InnerClassTest2$Inner1<E> {
  final org.benf.cfr.tests.InnerClassTest2 this$0;
  public org.benf.cfr.tests.InnerClassTest2$Inner1(java.util.List<E>);
  public int getX(int);
}

Disassembling with Krakatau gives

.version 51 0
.source InnerClassTest2.java
.class super public org/benf/cfr/tests/InnerClassTest2$Inner1
.super java/lang/Object

.field final private arg Ljava/util/List;
.field synthetic final this$0 Lorg/benf/cfr/tests/InnerClassTest2;

.method public <init> : [_13]
    .limit stack 2
    .limit locals 3
    aload_0
    aload_1
    putfield org/benf/cfr/tests/InnerClassTest2$Inner1 this$0 Lorg/benf/cfr/tests/InnerClassTest2;
    aload_0
    invokespecial java/lang/Object <init> ()V
    aload_0
    aload_2
    putfield org/benf/cfr/tests/InnerClassTest2$Inner1 arg Ljava/util/List;
    return
.end method

.method public getX : (I)I
    .limit stack 1
    .limit locals 2
    iconst_2
    ireturn
.end method

.const [_13] = Utf8 (Lorg/benf/cfr/tests/InnerClassTest2;Ljava/util/List;)V

As you can see, the Krakatau output shows that the descriptor is actually correct, but for some reason Javap isn't displaying it. One thing about Javap is that it tries to arrange the output so it looks more like Java. Perhaps this is a new feature introduced in JDK7 that tries to make disassembled generics look more like Java by hiding the compiler added parameters. Unfortunately this makes Javap (even more) useless for seeing what's really there.

Interesting catch!