为什么泛型方法和泛型类型具有不同的类型引入语法?

时间:2017-08-08 12:10:14

标签: java generics

在研究泛型时,我注意到generic methodsgeneric types(类或接口)之间的类型引入语法有所不同,这让我很困惑。

泛型方法的语法是

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

文档说

  

泛型方法的语法包括一个类型参数,在尖括号内,并且出现在方法的返回类型之前

泛型类型的语法是

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

文档说

  

由尖括号(&lt;&gt;)分隔的类型参数部分,在类名之后。它指定了类型参数

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

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

为什么之前必须引入一个,之后是另一个?

我主要以List<String>的形式使用泛型,并认为<String>List可能看起来很奇怪,但这是一个主观论证,除了方法之外,它也是如此。你可以拨打doStuff this.<String>doStuff("a string");

寻找技术解释我认为在指定返回类型之前必须先将<T>引入方法,因为T可能 返回类型且编译器可能不是能够像这样向前看,但这听起来很奇怪,因为编译器很聪明。

我认为除了“语言设计师就是这样做”之外,还有一个解释,但我找不到它。

6 个答案:

答案 0 :(得分:16)

答案确实存在于GJ Specification,已经链接,引自文件,第14页:

  

通过解析约束使得在方法名称之前传递参数的惯例是必要的:使用更常规的“方法名称之后的类型参数”约定,表达式f (a<b,c>(d))将有两个可能的解析。

评论实施:

f(a<b,c>(d))可以解析为f(a < b, c > d) (传递给f的比较中的两个布尔值)f(a<B, C>(d)) (带有类型参数的a调用) B和C以及值参数d传递给f)。我认为这也可能是Scala选择使用[]代替<>进行泛型的原因。

答案 1 :(得分:9)

据我所知,Java中的泛型,当它们被引入时,基于GJ(支持泛型类型的Java编程语言的扩展)的泛型的概念。因此语法来自GJ,请参阅GJ Specification

这是对您的问题的正式回答,但不是GJ背景下您的问题的答案。但很明显它与C ++语法无关,因为在C ++参数部分先于class关键字和方法的返回类型。

答案 2 :(得分:8)

我强烈的假设是,因为正如你所说的方法,泛型参数也可以是函数的返回类型:

public <RETURN_TYPE> RETURN_TYPE getResult();

所以当编译器到达函数的返回类型时,它的类型已经被遇到(因为它知道它是泛型类型)。

如果您的语法类似

public RETURN_TYPE getResult<RETURN_TYPE>();

需要第二次扫描才能解析。

对于类,这不是问题,因为对泛型类型的所有引用都出现在类定义块中,即在声明泛型类型之后。

答案 3 :(得分:5)

对此没有一些深刻的理论上的理由 - 这似乎是&#34;语言设计师就是这样做的。&#34;例如,C#确实使用了您想知道Java为什么不实现的语法。以下代码:

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

将编译。 C#与Java相似,这意味着Java没有理论上没有理由认为Java也没有实现同样的东西(特别是考虑到两种语言在开发早期都实现了泛型)。

现在使用Java语法的优点是,使用当前语法为方法实现LL(1)解析器要容易得多。

答案 4 :(得分:4)

原因是因为在编译期间通用类型和参数化类型的处理方式不同。一个是在删除过程中看到 Eliding type parameters ,另一个是 Eliding type arguments

Generics于2004年在正式版J2SE 5.0中添加到Java中。在Oracle文档&#34; Using and Programming Generics in J2SE 5.0&#34;说明

  

幕后花絮

     

泛型由Java编译器实现为前端   转换称为擦除,这是翻译或翻译的过程   将泛型重写为非泛型代码(即映射)的代码   当前JVM规范的新语法)。换句话说,这个   转换会删除所有通用类型信息;所有信息   尖括号之间被删除。例如,LinkedList   将成为LinkedList。其他类型变量的使用被替换为   类型变量的上限(例如,Object),以及何时   生成的代码类型不正确,转换为适当的类型   插入。

关键是 Type Erasure 。没有进行JVM更改以支持泛型,因此Java不记得过去编译的泛型类型。

在新奥尔良大学发布的一个名为Cost Of Erasure的公告中,为我们打破了Erasure的步骤:

  

在类型擦除期间执行的步骤包括:

     
      
  • Eliding类型参数:当编译器找到泛型类型或方法的定义时,它会删除每种类型的所有匹配项   参数用最左边的边界替换它,如果没有绑定则用Object替换它   已指定。

  •   
  • Eliding类型参数:当编译器找到参数化类型,泛型类型的实例化时,它会删除类型   参数。例如,类型List<String>已转换为List

  •   

对于泛型方法,编译器正在寻找最左边的泛型类型定义。 it literally means furthest to the left这就是Bounded Typed Parameters 出现在方法返回类型之前的原因对于泛型类或接口,编译器正在寻找与泛型类型不同的参数化类型它不在类定义的最左边,而是在className之后。然后编译器删除类型参数,以便JVM可以理解它。

如果您查看Cost Of Erasure论文的附录部分。它很好地演示了编译器如何处理泛型接口和方法。

  

桥梁方法

     

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

注意:此外,编译器有时可能需要插入合成桥接方法。桥接方法是类型擦除过程的一部分。桥接方法负责确保在类型擦除后方法签名匹配。在Effects of Type Erasure and Bridge Methods

了解详情

编辑:OP指出我的结论是&#34;最左边的#34;从字面上看,最左边的手段不够坚固。 (OP确实在他的问题中声明他对#34;我认为&#34;答案类型不感兴趣)所以我做了一点挖掘并找到了这个GenericsFAQ。从示例看,类型参数的顺序似乎很重要。即<T extends Cloneable & Comparable<T>>在类型输入后变为Cloneable但不是Comparable

enter image description here

这是另一个直接来自Oracle Erasure of Generic Type

的例子

在以下示例中,通用Node类使用有界类型参数:

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

Java编译器将绑定类型参数T替换为第一个绑定类Comparable

我认为技术上更正确的方法是说类型擦除用第一个绑定类(或Object如果T无限制)替换绑定类型它只是发生由于Java中的语法,第一个绑定类是最左边的绑定。

答案 5 :(得分:1)

我认为,这是因为您可以将其声明为返回类型:

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

您需要在声明返回类型之前声明类型,因为您不能使用尚未定义的内容。例如,在声明somwhere之前,你不能使用变量x。 我喜欢(任何)语言遵循一些逻辑规则,然后更容易使用它,并且在某些方面知道它你只知道你可以从中得到什么。这是java的情况,它有一些可能性,但一般来说它遵循一些规则。在声明它之前你不能使用的东西在java中是非常强大的规则,对我而言它非常好,因为当你试图理解java代码时它会产生更少的WTF,这就是为什么我认为这是背后的原因。但是我不知道究竟是谁对这个决定负责,维基百科引用了一句话:

  

1998年,Gilad Bracha,Martin Odersky,David Stoutamire和Philip   Wadler创建了Generic Java,它是Java语言的扩展   支持泛型类型。[3] Generic Java是用Java(2004,Java 5)编写的   添加通配符。

我认为我们应该问上面引文中提到的某个人得到明确的答案,为什么会这样。

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