30

如果在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 上提出,参见这个问题。对此的答案解释说,当一个类是无类型/原始形式时,所有泛型都会从该类中删除 - 包括泛型方法的类型。

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

  • 为什么 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 是否适用于泛型方法。泛型方法的类型(<K>在前面的示例中)不是无类型的,因为它的类型由方法参数确定 - 只有类是无类型/原始类型的。

4

6 回答 6

8

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

4.8 中的 JLS 片段(您引用)涵盖了构造函数、实例方法和成员字段 - 泛型方法通常只是实例方法的一个特例。因此,此代码段似乎涵盖了您的案例。

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

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

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

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

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

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

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

(错误 + 修复显示方法的返回类型被视为方法类型的一部分,以用于此类型擦除)

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

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

但如果Map<K,V>有一个带类型的方法Map<String,V>,它的擦除就可以了Map<String>。对于类型擦除,更改类型参数的数量是可怕的,尤其是对于编译时方法解析。我们绝对不会接受这个请求。

期望能够使用原始类型MapSet<Map.Entry>

于 2013-08-06T14:17:48.453 回答
4

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

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

于 2013-08-01T18:20:43.687 回答
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 进行了未经检查的从Listto强制转换List<K>(但是编译器显示警告)。

于 2013-08-06T11:48:42.117 回答
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.

于 2013-08-06T12:48:39.710 回答
0

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

例如,考虑以下代码:

List list = new ArrayList();

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

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

因为 ? 后面没有extendsorsuper关键字,它被转换成这样:

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

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

于 2013-08-06T10:30:12.230 回答
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. 尽管在某些情况下它不会产生任何影响(例如您的示例),但编译器何时分析至少非常复杂。甚至可能是不可能的。

我知道这个情景似乎有点构建,但我敢肯定它不时发生。

于 2013-08-06T14:15:15.140 回答