java 类型擦除如何处理 '?'

Eit*_*s30 3 java generics type-erasure

希望在了解通用边界之后,我尝试了解通配符上限和下限。我的参考是: https: //docs.oracle.com/javase/tutorial/java/generics/wildcards.html 我发现有一句话我能理解:“通配符可以在多种情况下使用:作为参数、字段或局部变量; “字段和局部变量?无法想象。为什么如此重要的来源不通过一个简单的例子来强调它?

我试图了解 java 编译器用哪个引用来替换(同时擦除)“?”。也许我有一个很大的误解,并且会发生任何删除(因此以下所有示例都不相关)。在下面的例子中: 1.

public static void funcA (List<? extends Number>l)
Run Code Online (Sandbox Code Playgroud)

2.

public static void funcB(List<? super Integer>l)
Run Code Online (Sandbox Code Playgroud)

第二个示例和以下代码之间有区别吗: 3.

public static <T extends Integer> funcC(List<? extends T>l)
Run Code Online (Sandbox Code Playgroud)

示例2与下面的示例是否有不同: 4.

public static <T extends Integer> void funcC(List<T>l)
Run Code Online (Sandbox Code Playgroud)

Sla*_*law 6

前言:您已经提出了许多有关类型擦除的问题,包括在聊天室中。虽然这个答案将解决这个特定问题,但它也可能回答您的一些其他问题(甚至可能是您还没有问过的一些问题)。

\n\n

警告:这个答案很长,并且本文中特别提出的问题仅在最后直接解决。

\n\n
\n\n

什么是类型擦除?

\n\n

其他问答中已经对此进行了很好的介绍,因此我将简单地链接到它们:

\n\n\n\n
\n\n

类型擦除的规则是什么?

\n\n

类型擦除的规则在Java 语言规范 (JLS)\xc2\xa74.6 类型擦除中指定:

\n\n
\n

类型擦除是从类型(可能包括参数化类型和类型变量)到类型(永远不是参数化类型或类型变量)的映射。我们写作|T|是为了擦除类型T。擦除映射定义如下:

\n\n \n\n

类型擦除还将构造函数或方法的签名 ( \xc2\xa78.4.2 ) 映射到没有参数化类型或类型变量的签名。构造函数或方法签名的擦除是由与 中给出的所有形参类型s同名和擦除组成的签名。ss

\n\n

如果方法或构造函数的返回类型 ( \xc2\xa78.4.5 ) 和泛型方法或构造函数 ( \xc2\xa78.4.4\xc2\xa78.8.4 ) 的类型参数也会被擦除签名被删除。

\n\n

泛型方法签名的擦除没有类型参数。

\n
\n\n

对于这个答案,我们应该关注第一点和第四点:

\n\n
\n

参数化类型 ( \xc2\xa74.5 )的擦除G<T1,...,Tn>|G|

\n
\n\n

和:

\n\n
\n

类型变量 ( \xc2\xa74.4 )的擦除是其最左边界的擦除。

\n
\n\n

分别。特别是,请注意第四个要点如何仅解释类型变量的擦除。为什么这很重要?我会回过头来讨论这一点。

\n\n

术语

\n\n

理解这些术语对于理解如何应用规则很重要:

\n\n
    \n
  • 通用类

    \n\n
  • \n
  • 通用接口

    \n\n
  • \n
  • 通用方法

    \n\n
      \n
    • 在\xc2\xa78.4.4 JLS 的通用方法中指定。
    • \n
    • 泛型方法是一种声明一个或多个类型变量的方法。
    • \n
  • \n
  • 通用构造函数

    \n\n
      \n
    • 在\xc2\xa78.8.4 JLS 的通用构造函数中指定。
    • \n
    • 泛型构造函数是声明一个或多个类型变量的构造函数。
    • \n
  • \n
  • 类型变量

    \n\n
      \n
    • 在JLS 的\xc2\xa74.4 类型变量中指定。
    • \n
    • 类型变量是通过在泛型成员上声明的类型参数引入的。
    • \n
    • 类型变量的语法是:{Annotation} TypeIdentifier
    • \n
  • \n
  • 类型参数

    \n\n
      \n
    • JLS 的许多部分均进行了规定。我链接到的上述术语的每个部分都提到了类型参数
    • \n
    • 类型参数是泛型成员上类型变量及其边界的声明。
    • \n
    • 类型参数的语法是:where扩展为.{TypeParameterModifier} TypeIdentifier [TypeBound]TypeParameterModifierAnnotation
    • \n
  • \n
  • 参数化类型

    \n\n
      \n
    • 在\xc2\xa74.5 JLS参数化类型中指定。
    • \n
    • 参数化类型是对带有类型参数的泛型类泛型接口的实际使用。
    • \n
  • \n
  • 类型参数

    \n\n
  • \n
\n\n

请注意,类型参数类型实参之间的区别类似于 Java 区分方法参数和实参的方式。当您将方法声明为void bar(Object obj)参数Object obj时。但是,当您调用该方法时,就像参数bar(someObjInstance)的值一样。someObjInstance

\n\n

术语的代码示例

\n\n

查看代码中的一些示例可以帮助理解每个术语适用于代码的哪些部分。

\n\n

具有类型参数的泛型类

\n\n
public class Foo<T extends CharSequence, U> {\n    // class body...\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

有两种类型参数

\n\n
    \n
  1. T extends Charsequence

    \n\n
      \n
    • 类型变量T.
    • \n
    • 类型界限extends CharSequence
    • \n
  2. \n
  3. U

    \n\n
      \n
    • 类型变量U
    • \n
    • 类型界限extends Object(隐式定义的)
    • \n
  4. \n
\n\n

对于通用接口,代码看起来类似。

\n\n

具有类型参数的泛型方法

\n\n
public void <V extends Number> bar(V obj) {\n   // method body...\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

该方法有一个类型参数

\n\n
    \n
  1. V extends Number\n\n
      \n
    • 类型变量V.
    • \n
    • 类型界限extends Number.
    • \n
  2. \n
\n\n

泛型构造函数的代码看起来类似。

\n\n

参数化类型(无通配符)

\n\n
public <E extends Number> void bar(List<E> list) {\n    // method body...\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

有一种参数化类型

\n\n
    \n
  1. List<E>\n\n
      \n
    • 类型参数E.
    • \n
  2. \n
\n\n

参数化类型(带通配符)

\n\n
public void bar(List<? extends Number> list) {\n    // method body...\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

有一种参数化类型

\n\n
    \n
  1. List<? extends Number>\n\n
      \n
    • 类型参数? extends Number
    • \n
  2. \n
\n\n

回到规则

\n\n

正如我所提到的,值得注意的是,规则仅提到类型变量的擦除。这很重要的原因是因为在可以定义类型变量的地方(即在类型参数中)不允许使用通配符。通配符只能用在属于参数化类型一部分的类型参数中。

\n\n

参数化类型的擦除只是原始类型。

\n\n
\n\n

类型擦除何时重要?

\n\n

在通用代码的日常开发中,类型擦除实际上是无关紧要的。您必须详细了解类型擦除如何工作的唯一一次是在使用原始类型时。在理想且公正的世界中,当使用无法更改的遗留代码(从 Java 5 之前的日子开始)时,您只会使用原始类型。换句话说,除非您被迫使用原始类型,否则您应该始终适当地使用泛型。

\n\n

然而,也许您被迫使用原始类型或者您只是好奇。在这种情况下,您需要知道类型擦除的工作原理,因为擦除决定了使用什么类型。这是泛型类的示例:

\n\n
public class Foo<T extend CharSequence, U> {\n\n    private List<T> listField;\n    private U objField;\n\n    public void bar(List<? extends T> listParam) {\n        // method body...\n    }\n\n    public U baz(T objParam) {\n        // method body...\n    }\n\n    public <V extends Number> V qux(V objParam) {\n        // method body...\n    }\n\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

遵循前面提到的类型擦除规则,上面的类之后的样子如下:

\n\n
// the raw type of Foo\npublic class Foo {\n\n    private List listField;\n    private Object objField;\n\n    public void bar(List listParam) {\n        // method body...\n    }\n\n    public Object baz(CharSequence objParam) {\n        // method body...\n    }\n\n    public Number qux(Number objParam) {\n        // method body...\n    }\n\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

但同样,当您使用原始类型时,您只需要了解后一个版本。

\n\n
\n\n

将这些知识应用于您的问题

\n\n

到目前为止我们了解到的一些内容是通配符只能在类型参数中使用,因此仅适用于参数化类型参数化类型的擦除只是原始类型。如果我们将这些知识应用到您的示例中,您将得到以下结果:

\n\n
    \n
  1. 例子#1

    \n\n
  2. \n
  3. 例子#2

    \n\n
  4. \n
  5. 例子#3

    \n\n
  6. \n
  7. 例子#4

    \n\n
  8. \n
\n\n

强化要点

\n\n

为了真正指出类型变量的擦除和参数化类型的擦除之间的区别,让我们看另一个例子。

\n\n
public static void funcA(List<? extends Number> l)\n
Run Code Online (Sandbox Code Playgroud)\n\n

该方法bar有一个类型为 的参数T。参数直接使用类型变量类型变量的擦除是其最左边界的擦除,Number在本例中就是这样。这意味着擦除后该方法的参数为Number

\n\n

该方法baz有一个类型为 的参数List<? extends T>。这里,类型变量被用作 参数化类型类型参数T的上限。换句话说,尽管使用了类型变量,但这里实际使用的擦除是参数化类型的擦除。这意味着擦除后该方法的参数只是。即使类型参数是下限通配符(例如)、无界通配符(例如),甚至非通配符(例如),也会发生这种情况。ListList<? super T>List<?>List<T>

\n\n

类型擦除如何处理通配符

\n\n

要直接回答有关类型擦除如何处理通配符的问题,答案是有效的:它不,不直接。通配符会消失(当参数化类型被删除时),并且在生成的原始类型中没有任何意义。

\n\n

通配符为使用通用 API 的用户提供了灵活性。以下是一些针对该概念的问答:

\n\n\n\n

希望这些问答也能帮助回答您的辅助问题,即您帖子中的第二个和第四个示例之间的区别是什么。

\n