为什么generics方法和genericstypes有不同的types引入语法?

在研究generics时,我注意到generics方法和generics (类或接口)之间的types引入语法的差异使我困惑。

通用方法的语法是

<T> void doStuff(T t) { // Do stuff with T } 

文档说

generics方法的语法包括一个types参数,尖括号内,并出现在方法的返回types之前

genericstypes的语法是

 class Stuff<T> { // Do stuff with T T t; } 

文档说

由尖括号(<>)分隔的types参数部分在类名后面 。 它指定了types参数

既没有说明它为什么必须在之前或之后。


为了保持一致,我期望的方法语法是
void doStuff<T>(T t) {}或types语法(对于类)是class <T>Stuff {} ,但显然不是这种情况。

为什么要先介绍一个,之后呢?

我主要以List<String>的forms使用generics,并认为<String>List可能看起来很奇怪,但这是一个主观的论点,除了方法也是这样。 你可以像this.<String>doStuff("a string");调用doStuff this.<String>doStuff("a string");

寻找一个技术性的解释我认为在指定返回types之前,可能<T>必须被引入到一个方法中,因为T可能返回types,编译器也许不能像这样向前看,但这听起来很奇怪,因为编译器是聪明。

我认为除了“语言devise者就是这么做的”之外,还有一个解释,但我找不到它。

答案确实在于已经链接的GJ规范 ,引用文件,第14页:

通过parsing约束来使方法名称前的parameter passing成为必要的约定:对于更常规的“方法名称后的types参数”约定,expression式f (a<b,c>(d))将有两种可能的parsing。

从评论实施:

f(a<b,c>(d))可以parsing为f(a < b, c > d) (从传递给f的比较中的两个布尔值)f(a<B, C>(d)) 调用types参数B和C,并将值参数d传递给f) 。 我想这也可能是为什么Scalaselect使用[]而不是<>generics。

就我所知的Javagenerics而言,当它们被引入时,都是基于GJgenerics的思想(支持generics的Java编程语言的扩展)。 所以语法取自GJ,参见GJ规范 。

这是你的问题的正式答案,但不是在GJ的背景下回答你的问题。 但很显然,它与C ++语法无关,因为在C ++中,参数部分在方法的class关键字和返回types之前。

我强烈的假设是,这是因为像你说的一个方法一样,generics参数也可以是函数的返回types:

 public <RETURN_TYPE> RETURN_TYPE getResult(); 

所以在编译器到达函数的返回types时,它的types已经遇到(如在它知道它是一个genericstypes)。

如果你有一个类似的语法

 public RETURN_TYPE getResult<RETURN_TYPE>(); 

这将需要第二次扫描parsing。

对于类,这不是一个问题,因为genericstypes的所有引用都出现在类定义块中,也就是声明了genericstypes之后。

这里没有深刻的理论上的理由 – 这似乎是“语言devise者就是这么做的”。 例如,C# 确实使用了你想知道为什么Java没有实现的语法。 以下代码:

 private T Test<T>(T abc) { throw new NotImplementedException(); } 

将编译。 C#与Java相似,这意味着没有理论上的原因,Java也不能实现同样的事情(尤其是考虑到两种语言在开发早期就实现了generics)。

现在,Java语法的优点是,使用当前语法为方法实现LL(1)parsing器稍微容易一些。

原因是因为genericstypes和参数化types在编译期间处理不同。 一个是在删除过程中看到Elidingtypes参数 ,另一个是Elidingtypes参数

generics在2004年被添加到Java的正式版本J2SE 5.0中。 在Oracle文档“ 在J2SE 5.0中使用和编程generics ”中指出

在幕后

generics是由Java编译器实现的,称为擦除(Erasure),即转换或重写使用generics到非generics代码(即将新语法映射到当前JVM规范)的代码的前端转换。 换句话说,这种转换会消除所有通用types信息; 尖括号之间的所有信息都被删除。 例如,LinkedList将成为LinkedList。 其他typesvariables的使用由typesvariables(例如Object)的上限replace,并且当生成的代码不是types正确时,将插入到适当types的转换。

关键在于types擦除的过程。 没有JVM的变化支持generics,所以Java不记得通用types过去的编译。

在新奥尔良大学公布的一个叫做“擦除费用”中 ,为我们打破了擦除的步骤:

types擦除过程中执行的步骤包括:

  • 删除types参数:当编译器findgenericstypes或方法的定义时,它将删除每个types参数的最左边界的replace对象,如果没有指定边界,则删除所有对象。

  • 消除types参数:当编译器find一个参数化types(一个genericstypes的实例)时,它将删除types参数。 例如,typesList<String>被转换为List

对于generics方法,编译器正在寻找最左边的genericstypes定义。 它实际上意味着最左边 ,这就是为什么有界的types参数 出现在方法的返回types之前为了generics类或接口编译器正在寻找参数化types,这不同于genericstypes,它不在类的最左边界定义,而是跟在className之后。 编译器然后删除types参数,以便JVM可以理解它。

如果您查看“擦除成本”纸张的附录部分。 它很好地演示了编译器如何处理generics接口和方法。

桥梁方法

编译扩展参数化类或实现参数化接口的类或接口时,编译器可能需要创build一个称为桥接方法的合成方法,作为types擦除过程的一部分。 您通常不需要担心桥接方法,但是如果出现在堆栈轨迹中,您可能会感到困惑。

注意:另外,编译器有时可能需要插入合成桥接方法。 Bridge方法是types擦除过程的一部分。 types擦除后,Bridge方法负责确保方法签名匹配。 请参阅types擦除和桥接方法的更多信息


编辑:正如OP指出我的“最左边界”的结论意味着从字面上最左边的手段是不够坚实。 (OP在他的问题中声明他对“我认为”types的答案不感兴趣)所以我做了一点挖掘,发现了这个GenericsFAQ 。 从这个例子看来,types参数的顺序看起来很重要。 即<T extends Cloneable & Comparable<T>>在types化后变为Cloneable ,但不是Comparable

在这里输入图像说明

这里是另一个直接来自Oracle通用types擦除的例子

在下面的例子中,通用的Node类使用了一个有界的types参数:

 public class Node<T extends Comparable<T>> { ... } 

Java编译器用第一个绑定类Comparablereplace有界的types参数T

我认为更技术上正确的方法是说types擦除用第一个绑定类replace绑定types(或者如果T是无界的,则为Object ),只是由于Java中的语法,第一个绑定类是最左边的绑定。

我想,这是因为你可以声明它是一个返回types:

  <T> T doStuff(T t) { // Do stuff with T return t; } 

您需要在声明返回types之前声明该types,因为您不能使用尚未定义的内容。 例如,在声明它之前,你不能使用一个variablesx。 我喜欢(任何)语言遵循一些逻辑规则,那么使用它就更容易,并且在某种程度上知道它只是知道你可以从中得到什么。 这是与java的情况下,它有一些可能性,但总的来说,它遵循一些规则。 而且在声明它之前你不能使用某个东西在java中是非常强的规则,对我来说这是非常好的,因为当你试图理解java代码的时候它会产生更less的WTF,所以我认为这是它背后的推理。 但是我不知道究竟是谁对这个决定负责,维基百科的一个引用:

1998年,Gilad Bracha,Martin Odersky,David Stoutamire和Philip Wadler创build了Generic Java,这是Java语言的扩展,以支持genericstypes[3]。 Java(2004,Java 5)中增加了通配符。

我认为我们应该问上面引用中提到的人来得到明确的答案,为什么它是现实的。

我不相信它与以前版本的Java的向后兼容性有任何关系。

Javagenerics引入了Java 1.5。 新的语言function的想法是永远不会破坏以前的版本。 我们必须记住,generics是语言/开发人员的types安全function。 随着这两个新types引入了parameterized typestype variables

JLS 4.3引用types和 TypeArgumentTypeVariable提供以下语法。

ReferenceType:ClassOrInterfaceType TypeVariable ArrayType

ClassOrInterfaceType:ClassType InterfaceType

ClassType:TypeDeclSpecifier TypeArgumentsopt

InterfaceType:TypeDeclSpecifier TypeArgumentsopt

TypeDeclSpecifier:TypeName
ClassOrInterfaceType。 识别码

TypeName:标识符types名称。 识别码

TypeVariable:标识符

ArrayType:Type []

这些例子就是这样的

 Vector<String> Seq<Seq<A>> Seq<String>.Zipper<Integer> Collection<Integer> Pair<String,String> 

和参数化types

 Vector<String> x = new Vector<String>(); Vector<Integer> y = new Vector<Integer>(); return x.getClass() == y.getClass(); 

无论什么时候没有绑定,它将假定它是一个java.lang.Objecttypes擦除它将例如Vector<Object>因此它是以前版本的Java向后兼容。


类本身不是generics的generics方法的语法如下。

从JL 8.4方法声明

MethodDeclaration:MethodHeader MethodBody

MethodHeader:MethodModifiersopt TypeParametersopt结果MethodDeclarator Throwsopt

MethodDeclarator:标识符(FormalParameterListopt)

一个例子是这样的

 public class GenericMethod { public static <T> T aMethod(T anObject) { return anObject; } public static void main(String[] args) { String greeting = "Hi"; String reply = aMethod(greeting); } } 

其中types擦除的结果

 public class GenericMethod { public static Object aMethod(Object anObject) { return anObject; } public static void main(String[] args) { String greeting = "Hi"; String reply = (String) aMethod(greeting); } } 

而且它又一次向后兼容以前的Java版本。 看到两个提案文件更深入的推理

将generics添加到Java编程语言:参与者草稿规范

Javagenerics的专门化


关于技术部分。 创buildJava程序的步骤是编译.java文件。 你可以用javac命令来生成类文件。 JavacParser用上面的规范parsing整个文件并生成字节码。 看到这里的JavacParser源代码。

我们来看下面的Test.java文件

 class Things{} class Stuff<T>{ T t; public <U extends Things> U doStuff(T t, U u){ return u; }; public <T> T doStuff(T t){ return t; }; } 

为了保持向后兼容,JVM并没有改变它以前的类文件的属性。 他们添加了一个新的属性,并将其命名为Signature 。 从propsal纸

当用作方法或字段的属性时,签名会给出该方法或字段的完整(可能是通用的)types。 用作类属性时,签名指示类的types参数,后跟其超types,后跟其所有接口。 签名中的types语法扩展到参数化types和typesvariables。 正式types参数也有一个新的签名语法。 签名string的语法扩展如下所示:

JVM规范4.3.4定义了以下语法

MethodTypeSignature:FormalTypeParametersopt(TypeSignature *)ReturnType ThrowsSignature *

ReturnType:TypeSignature VoidDescriptor

抛出签名:^ ClassTypeSignature ^ TypeVariableSignature

通过使用javap -v反汇编Test.class文件,我们得到以下内容:

 class Stuff<T extends java.lang.Object> extends java.lang.Object minor version: 0 major version: 52 flags: ACC_SUPER Constant pool: #1 = Methodref #3.#20 // java/lang/Object."<init>":()V #2 = Class #21 // Stuff #3 = Class #22 // java/lang/Object #4 = Utf8 t #5 = Utf8 Ljava/lang/Object; #6 = Utf8 Signature #7 = Utf8 TT; #8 = Utf8 <init> #9 = Utf8 ()V #10 = Utf8 Code #11 = Utf8 LineNumberTable #12 = Utf8 doStuff #13 = Utf8 (Ljava/lang/Object;LThings;)LThings; #14 = Utf8 <U:LThings;>(TT;TU;)TU; #15 = Utf8 (Ljava/lang/Object;)Ljava/lang/Object; #16 = Utf8 <T:Ljava/lang/Object;>(TT;)TT; #17 = Utf8 <T:Ljava/lang/Object;>Ljava/lang/Object; #18 = Utf8 SourceFile #19 = Utf8 Test.java #20 = NameAndType #8:#9 // "<init>":()V #21 = Utf8 Stuff #22 = Utf8 java/lang/Object { T t; descriptor: Ljava/lang/Object; flags: Signature: #7 // TT; Stuff(); descriptor: ()V flags: Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 4: 0 public <U extends Things> U doStuff(T, U); descriptor: (Ljava/lang/Object;LThings;)LThings; flags: ACC_PUBLIC Code: stack=1, locals=3, args_size=3 0: aload_2 1: areturn LineNumberTable: line 8: 0 Signature: #14 // <U:LThings;>(TT;TU;)TU; public <T extends java.lang.Object> T doStuff(T); descriptor: (Ljava/lang/Object;)Ljava/lang/Object; flags: ACC_PUBLIC Code: stack=1, locals=2, args_size=2 0: aload_1 1: areturn LineNumberTable: line 11: 0 Signature: #16 // <T:Ljava/lang/Object;>(TT;)TT; } Signature: #17 // <T:Ljava/lang/Object;>Ljava/lang/Object; SourceFile: "Test.java" 

方法

 public <U extends Things> U doStuff(T t, U u){ return u; }; 

转换为签名来表明这是一种通用的方法

  Signature: #14 // <U:LThings;>(TT;TU;)TU; 

如果我们对以前的Java 1.5版本使用非generics类,例如

 public String doObjectStuff(Object t, String u){ return u; } 

会翻译成

  public java.lang.String doObjectStuff(java.lang.Object, java.lang.String); descriptor: (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/String; flags: ACC_PUBLIC Code: stack=1, locals=3, args_size=3 0: aload_2 1: areturn LineNumberTable: line 12: 0 

两者之间的唯一区别在于Signature属性字段表示它确实是一个通用的方法,而其他以前的Java 1.5版本没有它。 但是两者具有相同的descriptor属性

 Non-Generic method descriptor: (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/String; Generic method descriptor: (Ljava/lang/Object;LThings;)LThings; 

这使其向后兼容。 所以答案会如你所说

“语言devise师就是这么做的”

加上

“语言devise者就是这么做的, 为了让它向后兼容而不增加太多的代码


编辑:关于应该很容易处理不同的语法的评论,我发现了一本书的Javagenerics和collectionsPhilip Wadler,Maurice Naftalin

Java中的generics类似于C ++中的模板。 关于Javagenerics和C ++模板之间的关系,需要牢记两件重要的事情:语法和语义。 语法是故意相似的,而语义是故意不同的。
在句法上,尖括号被选中是因为它们对于C ++用户来说很熟悉,而且由于方括号很难分析。 但是,在语法上有一个区别。 在C ++中,嵌套参数需要额外的空格,所以你看到这样的事情:List <List> […]等

看这里