泛型类中的Java泛型方法

时间:2013-08-01 18:15:32

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

如果在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);
  }
}

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

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<K>(实际上,这可以解释 - 请参阅下面的答案):

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节)M的类型是原始的类型,对应于与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是否适用于泛型方法。泛型方法的类型(前面示例中的<K>)不是无类型的,因为它的类型由方法参数决定 - 只有类是无类型/原始类型。

6 个答案:

答案 0 :(得分:8)

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

4.8中的JLS代码段(您引用)涵盖了构造函数,实例方法和成员字段 - 泛型方法通常只是实例方法的一个特例。所以看来你的情况就是这个片段所涵盖的。

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

  

泛型方法的类型是与之对应的原始类型   在与C相对应的通用声明中擦除其类型。

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

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

  

原始类型的使用仅允许作为兼容性的让步   遗留代码。在编写后的代码中使用原始类型   将通用性引入Java编程语言是   强烈气馁。 Java的未来版本可能是有可能的   编程语言将禁止使用原始类型

java开发人员的一些想法很明显:

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

(bug + fix显示方法的返回类型被视为此类型擦除目的的方法类型的一部分)

还有this request,有人似乎要求你描述的行为 - 只删除类通用参数,而不是其他泛型 - 但它被这个推理拒绝了:

  

请求是修改类型擦除,以便在类型声明Foo<T>中,擦除仅从参数化类型中删除T。然后,恰好在Map<K,V>的声明中,Set<Map.Entry<K,V>>将删除Set<Map.Entry>

     

但如果Map<K,V>的方法采用类型Map<String,V>,则其删除只会是Map<String>。对于类型擦除来改变类型参数的数量是可怕的,特别是对于编译时方法的解析。我们绝对不会接受这个要求。

     

期望能够使用原始类型(Map)同时仍然获得一些类型安全的泛型(Set<Map.Entry>)是太多了。

答案 1 :(得分:4)

使用Java Generics,如果您使用泛型类的原始形式,那么该类上的所有泛型,甚至是不相关的泛型方法,例如makeSingletonListdoSomething方法,变得生硬。我理解它的原因是提供与前编写的Java代码的向后兼容性。

如果您的通用类型参数T没有用处,那么只需将其从MyGenericClass中移除,将您的方法保留为K。否则,你必须忍受这样一个事实,即类类型参数T必须提供给你的类,以便在类中的其他任何东西上使用泛型。

答案 2 :(得分:2)

我发现完全丢弃泛型的一个原因(但是它并不是很好)。 原因是:泛型可能有限。考虑这个课程:

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<K>进行了未经检查的强制转换(但编译器会显示警告)。

答案 3 :(得分:1)

这不能回答基本问题,但确实解决了为什么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);

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

答案 4 :(得分:0)

这样做的原因是与预先通用代码向后兼容。 pre-generics代码不使用泛型参数,而是使用今天似乎是原始类型的东西。 pre-generics代码将使用Object引用而不是使用泛型类型的引用,而原始类型对所有泛型参数使用类型Object,因此代码确实向后兼容。

例如,请考虑以下代码:

List list = new ArrayList();

这是预先通用的代码,一旦引入了泛型,这就被解释为等同于的原始泛型类型:

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

因为?之后没有extendssuper关键字,它会转换为:

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

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

答案 5 :(得分:0)

从我对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。虽然在某些情况下它不会产生任何差别(如您的示例),但编译器分析时间至少非常复杂。甚至可能是不可能的。

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