0

我在应用程序代码中有一个奇怪的错误,它是一个注释处理器,我可以发现错误的根本原因是当我使用反射方法查询类时,该类com.sun.tools.javac.tree.JCTree$JCClassDecl包含该方法两次。这两个版本仅在返回类型上有所不同。这在 JVM 代码中是合法的,但在 Java 中是不合法的。这不是方法重载,因为只有返回类型不同,返回类型不是方法签名的一部分。getSimpleName()getMethods()

这个问题可以用简单的代码来演示:

Method[] methods = com.sun.tools.javac.tree.JCTree.JCClassDecl.class.getMethods();
for (int i = 0; i < methods.length; i++) {
    System.out.println(methods[i]);
}

它会打印

...
public javax.lang.model.element.Name com.sun.tools.javac.tree.JCTree$JCClassDecl.getSimpleName()
public com.sun.tools.javac.util.Name com.sun.tools.javac.tree.JCTree$JCClassDecl.getSimpleName()
...

(省略号代表更多的输出行,显示了我们现在不感兴趣的各种其他方法。)

我用来测试的Java版本是

$ java -version
java version "11" 2018-09-25
Java(TM) SE Runtime Environment 18.9 (build 11+28)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11+28, mixed mode)

在 Windows 10 机器上。

问题:这个类代码是如何创建的?我的理解是这部分代码是用Java编写的,但是在Java中这是不可能的。另外:拥有两个相同签名版本的方法的目的是什么?有什么提示吗?

4

1 回答 1

2

如果您查看源代码1,您会发现只有一个名称为getSimpleName(). 此方法返回com.sun.tools.javac.util.Name. 有两个关键的事情需要注意:

  1. 该方法实际上是重写com.sun.source.tree.ClassTree#getSimpleName()的,它被声明为 return javax.lang.model.element.Name
  2. com.sun.tools.javac.util.Name抽象类实现了接口,并且由于被覆盖的javax.lang.model.element.Name方法返回前者,它利用了协变返回类型

根据这个 Oracle 博客,一个覆盖另一个但声明协变返回类型的方法是使用桥接方法实现的。

这是如何实施的?

尽管 java 语言不允许基于返回类型的重载,但 JVM 始终允许基于返回类型的重载。JVM 使用方法的完整签名进行查找/解析。完整签名除了参数类型之外还包括返回类型。即,一个类可以有两个或多个仅在返回类型上有所不同的方法。javac 使用这个事实来实现协变返回类型。在上面的 CircleFactory 示例中,javac 生成的代码等价于以下内容:

class CircleFactory extends ShapeFactory {
    public Circle newShape() {
       // your code from the source file
       return new Circle();
    }
    // javac generated method in the .class file
    public Shape newShape() {
       // call the other newShape method here -- invokevirtual newShape:()LCircle;
    }  
}

我们可以在类上使用带有 -c 选项的 javap 来验证这一点。请注意,我们仍然不能在源语言中使用基于返回类型的重载。但是,javac 使用它来支持协变返回类型。这样,在 JVM 中无需更改即可支持协变返回类型。

事实上,如果你运行以下命令:

javap -v com.sun.tools.javac.tree.JCTree$JCClassDecl

将输出以下内容(仅包括相关方法):

public com.sun.tools.javac.util.Name getSimpleName();
descriptor: ()Lcom/sun/tools/javac/util/Name;
flags: (0x0001) ACC_PUBLIC
Code:
  stack=1, locals=1, args_size=1
     0: aload_0
     1: getfield      #13                 // Field name:Lcom/sun/tools/javac/util/Name;
     4: areturn
  LineNumberTable:
    line 801: 0
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0       5     0  this   Lcom/sun/tools/javac/tree/JCTree$JCClassDecl;

和:

public javax.lang.model.element.Name getSimpleName();
descriptor: ()Ljavax/lang/model/element/Name;
flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
  stack=1, locals=1, args_size=1
     0: aload_0
     1: invokevirtual #96                 // Method getSimpleName:()Lcom/sun/tools/javac/util/Name;
     4: areturn
  LineNumberTable:
    line 752: 0
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0       5     0  this   Lcom/sun/tools/javac/tree/JCTree$JCClassDecl;

如您所见,第二种方法,即返回的方法,javax.lang.model.element.Name既是综合的,也是桥梁的。换句话说,该方法由编译器生成,作为协变返回类型实现的一部分。它还简单地委托给“真实”方法,即源代码中实际存在的返回com.sun.tools.javac.util.Name.


1. 源代码链接适用于 JDK 13。

于 2019-12-13T10:37:55.023 回答