【问题标题】:Java generic methods in generics classes泛型类中的 Java 泛型方法
【发布时间】:2013-08-01 18:15:31
【问题描述】:

如果在Java中创建一个泛型类(该类有泛型类型参数),可以使用泛型方法(该方法接受泛型类型参数)吗?

考虑以下示例:

public class MyClass {
  public <K> K doSomething(K k){
    return k;
  }
}

public class MyGenericClass<T> {
  public <K> K doSomething(K k){
    return k;
  }

  public <K> List<K> makeSingletonList(K k){
    return Collections.singletonList(k);
  }
}

正如您对泛型方法所期望的那样,我可以使用任何对象在 MyClass 的实例上调用 doSomething(K)

MyClass clazz = new MyClass();
String string = clazz.doSomething("String");
Integer integer = clazz.doSomething(1);

但是,如果我尝试使用 MyGenericClass 的实例 而不 指定泛型类型, 我调用doSomething(K) 返回一个Object,不管K 传入了什么:

MyGenericClass untyped = new MyGenericClass();
// this doesn't compile - "Incompatible types. Required: String, Found: Object"
String string = untyped.doSomething("String");

奇怪的是,如果返回类型是泛型类,它将编译 - 例如。 List&lt;K&gt;(其实这个可以解释——见下文答案):

MyGenericClass untyped = new MyGenericClass();
List<String> list = untyped.makeSingletonList("String"); // this compiles

此外,如果泛型类被键入,即使只使用通配符,它​​也会编译:

MyGenericClass<?> wildcard = new MyGenericClass();
String string = wildcard.doSomething("String"); // this compiles
  • 在无类型的泛型类中调用泛型方法不起作用是否有充分的理由?

  • 我是否缺少一些与泛型类和泛型方法相关的巧妙技巧?

编辑:

为了澄清,我希望无类型或原始类型的泛型类不尊重泛型类的类型参数(因为它们尚未提供)。但是,我不清楚为什么无类型或原始类型的泛型类意味着泛型方法不被尊重。

事实证明,这个问题已经在 SO 上提出,c.f. this question。对此的答案解释说,当一个类是无类型的/以其原始形式时,所有泛型都从该类中删除 - 包括泛型方法的类型。

但是,对于为什么会这样,并没有真正的解释。所以请允许我澄清一下我的问题:

  • 为什么 Java 会删除无类型或原始类型泛型类上的泛型方法类型?这有充分的理由吗,还是只是疏忽?

编辑 - JLS 的讨论:

已建议(在回答上一个 SO 问题和此问题时)在 JLS 4.8 中处理此问题,其中指出:

未从其超类或超接口继承的原始类型 C 的构造函数(第 8.8 节)、实例方法(第 8.4 节、第 9.4 节)或非静态字段(第 8.3 节)的类型是原始类型对应于 C 对应的泛型声明中擦除其类型的类型。

我很清楚这与非类型化类有何关系——类泛型类型被擦除类型替换。如果类泛型已绑定,则擦除类型对应于这些边界。如果它们未绑定,则擦除类型为 Object - 例如

// unbound class types
public class MyGenericClass<T> {
  public T doSomething(T t) { return t; }
}
MyGenericClass untyped = new MyGenericClass();
Object t = untyped.doSomething("String");

// bound class types
public class MyBoundedGenericClass<T extends Number> {
  public T doSomething(T t) { return t; }
}
MyBoundedGenericClass bounded = new MyBoundedGenericClass();
Object t1 = bounded.doSomething("String"); // does not compile
Number t2 = bounded.doSomething(1); // does compile

虽然泛型方法是实例方法,但我不清楚 JLS 4.8 是否适用于泛型方法。泛型方法的类型(前面示例中的&lt;K&gt;)不是无类型的,因为它的类型由方法参数决定——只有类是无类型/原始类型的。

【问题讨论】:

  • @PaulBellora - 谢谢。第一个非常相关 - 尽管我对 SO 的大量搜索没有找到它,所以感谢您的参考。第二个不太相关,因为它只涵盖泛型类,而不是泛型方法。
  • 抱歉,有点OT。但是我尝试搜索它,静态类是一个新结构吗? AFAIK 它不会在 Java 6 中编译(它会在 Java 7 中编译吗?)。
  • @hajder - 抱歉,我最初将这些作为嵌套类写在测试类中(因此使它们成为静态),并且在我对问题进行 C&P 时忽略了删除它们。现已移除。

标签: java generics language-design generic-method raw-types


【解决方案1】:

'为了向后兼容'似乎是擦除类泛型类型的充分理由——它是必需的,例如允许您返回一个无类型列表并将其传递给一些遗留代码。将此扩展到泛型方法似乎是一个棘手的子案例。

4.8 中的 JLS sn-p(您引用的)涵盖了构造函数、实例方法和成员字段——泛型方法通常只是实例方法的一个特例。所以看来你的情况已经被这个 sn-p 覆盖了。

使 JLS 4.8 适应这种特定情况:

泛型方法的类型是对应于 在对应于 C 的泛型声明中擦除其类型。

(这里方法的“类型”将包括所有参数和返回类型)。如果您将“擦除”解释为“擦除所有泛型”,那么这似乎与观察到的行为相匹配,尽管它不是很直观甚至没有用。删除所有泛型,而不仅仅是泛型类参数,这似乎是一种过分热心的一致性(尽管我是谁来猜测设计者)。

类泛型参数与方法泛型参数交互时可能会出现问题——在您的代码中,它们是完全独立的,但您可以想象它们被分配/混合在一起的其他情况。我认为值得指出的是,根据 JLS,不建议使用原始类型:

只允许使用原始类型作为兼容性的让步 遗留代码。在之后编写的代码中使用原始类型 将泛型引入 Java 编程语言是 强烈劝阻。 Java 的未来版本可能 编程语言将不允许使用原始类型

Java 开发人员的一些想法在这里很明显:

http://bugs.sun.com/view_bug.do?bug_id=6400189

(错误 + 修复显示方法的返回类型被视为方法类型的一部分,以便进行这种类型擦除)

还有this request,似乎有人要求你描述的行为——只删除类泛型参数,而不是其他泛型——但它被拒绝了,理由如下:

请求修改类型擦除,以便在类型声明Foo&lt;T&gt; 中,擦除仅从参数化类型中删除T。然后,碰巧在Map&lt;K,V&gt; 的声明中,Set&lt;Map.Entry&lt;K,V&gt;&gt; 擦除为Set&lt;Map.Entry&gt;

但如果Map&lt;K,V&gt; 有一个采用Map&lt;String,V&gt; 类型的方法,它的擦除将只是Map&lt;String&gt;。对于类型擦除,更改类型参数的数量是可怕的,尤其是对于编译时方法解析。我们绝对不会接受这个请求。

期望能够使用原始类型 (Map) 同时仍然获得一些泛型的类型安全 (Set&lt;Map.Entry&gt;),这太过分了。

【讨论】:

  • 我同意向后兼容是一个很好的理由——只是不清楚为什么需要对 all 泛型进行类型擦除来实现这一点。 +1 用于错误报告,其中此问题被认为是错误,但修复它会影响兼容性(这与说兼容性需要它不完全相同......)。不幸的是,该修复程序似乎尚未合并到已发布的版本中...
  • 还有这个请求:bugs.sun.com/bugdatabase/view_bug.do?bug_id=6256320 似乎有人请求你描述的行为 - 只删除类泛型参数,而不是其他泛型 - 但它被拒绝了
  • +1 良好的研究 - 考虑在您的答案中引用链接错误/请求的评估。 @amaidment 我为赏金推荐这个答案,因为它是您最接近了解语言设计者意图的地方。
  • 确实——当之无愧的赏金,现已颁发。
【解决方案2】:

使用 Java 泛型,如果您使用泛型类的原始形式,那么类上的所有泛型,甚至不相关的泛型方法(例如您的 makeSingletonListdoSomething 方法)都将变为原始形式.据我了解,这样做的原因是为了提供与 Java 代码编写的预泛型的向后兼容性。

如果您的泛型类型参数T 没有用处,则只需将其从MyGenericClass 中删除,将您的方法留给K。否则,您将不得不接受这样一个事实,即必须将类类型参数 T 提供给您的类,才能在类中的其他任何东西上使用泛型。

【讨论】:

  • 谢谢 - 在我的用例中,我需要泛型类类型参数。上面的示例中没有使用它们,因为我想要一个简洁的 SSCCE。此外,我不清楚为什么擦除原始类中的所有泛型可以确保向后兼容性。
  • @amaidment 假设你有这个接口:interface C&lt;T&gt; { &lt;K&gt; K foo(); }。答案中解释的行为确保了一些预通用 C 可以安全地自动“生成”而无需更改旧代码。否则C 接口的用户应该像这样调用fooc.&lt;SomeType&gt;foo()
  • @MAnyKey - 您可能想发布您的答案作为答案......无论如何,我认为您错过了重点。如果您有 C 的无类型实现,则不仅类泛型 (T) 将被视为 Object(这是意料之中的),而且泛型方法 (&lt;K&gt; K foo()) 也将被视为无类型,所以foo() 的实现必须是public Object foo(){...},这又回到了原来的问题。
【解决方案3】:

我找到了一个完全放弃泛型的理由(但它不是很好)。 原因是:泛型可能是有界的。考虑这个类:

public static class MyGenericClass<T> {
    public <K extends T> K doSomething(K k){
        return k;
    }

    public <K> List<K> makeSingletonList(K k){
        return Collections.singletonList(k);
    }
}

当您使用没有泛型的类时,编译器必须丢弃doSomething 中的泛型。 而且我认为所有泛型都被丢弃以符合这种行为。

makeSingletonList 编译是因为 Java 执行了从 ListList&lt;K&gt; 的未经检查的强制转换(但编译器显示警告)。

【讨论】:

  • 否 - 方法类型 (&lt;K&gt;) 独立于类类型 (&lt;T&gt;)(除非另有限制,例如 &lt;K extends T&gt; 根据您的示例),并且彼此独立。因此我们可以有一个类C&lt;T extends Number&gt;,方法是public &lt;T&gt; T doSomething(T t){ return t; },它可以被称为new C&lt;Integer&gt;().doSomething("String")。这将编译(但对于未来的开发人员来说不是很清楚......)因为采用方法泛型类型而不是类泛型类型,并且它们是独立的。
  • 这与我写的任何内容都不矛盾。是的,方法类型独立于类类型。但是,如果方法类型受类类型的限制,编译器必须在使用没有泛型的类时将其全部丢弃。
  • +1 一个方法类型使用的例子,它依赖于类类型,这足以在使用原始类型时在类中的任何地方省略泛型。
  • 我不同意 - 编译器没有必须丢弃所有泛型类型(尽管这似乎已经完成)。相反,编译器可以将无类型的泛型视为Object。这就是我所期望的,但事实并非如此,因此问题仍然存在……为什么要删除 all 泛型类型?
  • 我认为完整的类型擦除与提到的向后兼容性相结合是有意义的。当创建没有泛型类型参数的对象时,可以假设对象的使用发生在非泛型代码中,因此所有泛型类型都立即替换为Object。我想编译器不可能猜测每个调用是否是通用的。如果将方法设为静态会发生什么?
【解决方案4】:

这并没有回答基本问题,但确实解决了为什么 makeSingletonList(...) 编译而 doSomething(...) 不编译的问题:

MyGenericClass 的无类型实现中:

MyGenericClass untyped = new MyGenericClass();
List<String> list = untyped.makeSingletonList("String");

...等价于:

MyGenericClass untyped = new MyGenericClass();
List list = untyped.makeSingletonList("String");
List<String> typedList = list;

这将编译(带有几个警告),但随后会出现运行时错误。

确实,这也类似于:

MyGenericClass untyped = new MyGenericClass();
List<Integer> list = untyped.makeSingletonList("String"); // this compiles!!!
Integer a = list.get(0);

...当您尝试获取 String 值并将其转换为 Integer 时,它会编译但会抛出运行时 ClassCastException

【讨论】:

    【解决方案5】:

    这样做的原因是与预泛型代码的向后兼容性。前泛型代码没有使用泛型参数,而是使用今天看起来是原始类型的东西。预泛型代码将使用 Object 引用而不是使用泛型类型的引用,原始类型对所有泛型参数使用类型 Object,因此代码确实向后兼容。

    例如,考虑以下代码:

    List list = new ArrayList();
    

    这是前泛型代码,一旦引入泛型,就会被解释为原始泛型类型,相当于:

    List<?> list = new ArrayList<>();
    

    因为 ?后面没有extendssuper 关键字,则转换为:

    List<Object> list = new ArrayList<>();
    

    在泛型使用Object 引用列表元素之前使用的List 版本,此版本还使用Object 引用列表元素,因此保留了向后兼容性。

    【讨论】:

    • 您的回答很好地解释了为什么 Java 会忽略无类型类中的类泛型类型。但是,问题实际上是关于泛型方法,在无类型的类上也忽略了类型,而您没有涵盖。
    • 4.8 中的 JLS sn-p(来自您链接的问题)涵盖了构造函数、实例方法和成员字段 - 泛型方法通常只是实例方法的一个特例。所以在我看来,tbodt 的回答确实涵盖了你的情况?
    • “向后兼容”不是一个充分的理由吗?一些想法在这里很明显:bugs.sun.com/view_bug.do?bug_id=6400189(错误 + 修复显示方法的返回类型被视为方法类型的一部分,以便进行这种类型擦除)
    【解决方案6】:

    来自我对 MAnyKeys 答案的评论:

    我认为完整的类型擦除与提到的向后兼容性相结合是有意义的。当对象在没有泛型类型参数的情况下创建时,它可以(或应该?)假设对象的使用发生在非泛型代码中。

    考虑一下这个遗留代码:

    public class MyGenericClass {
        public Object doSomething(Object k){
            return k;
        }
    }
    

    这样称呼:

    MyGenericClass foo = new MyGenricClass();
    NotKType notKType = foo.doSomething(new NotKType());
    

    现在这个类和它的方法是通用的:

    public class MyGenericClass<T> {
        public <K extends KType> K doSomething(K k){
            return k;
        }
    }
    

    现在上述调用者代码将不再编译,因为NotKType 不是KType 的替代类型。为了避免这种情况,泛型类型被替换为Object。尽管在某些情况下它不会产生任何影响(例如您的示例),但编译器何时分析至少非常复杂。甚至可能是不可能的。

    我知道这个情景似乎有点构建,但我相信它时不时会发生。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-30
      • 1970-01-01
      • 2011-05-11
      • 2021-02-16
      • 2021-02-18
      相关资源
      最近更新 更多