为什么这段通用的Java代码无法编译?

35

在这个简化的例子中,我有一个通用类和一个方法,该方法返回一个Map,无论类型参数是什么。如果我没有在包含类上指定类型,为什么编译器会擦除Map上的类型?

import java.util.Map;

public class MyClass<T>
{
    public Map<String, String> getMap()
    {   
        return null;
    }

    public void test()
    {   
        MyClass<Object> success = new MyClass<Object>();
        String s = success.getMap().get("");

        MyClass unchecked = new MyClass();
        Map<String, String> map = unchecked.getMap();  // Unchecked warning, why?
        String s2 = map.get("");

        MyClass fail = new MyClass();
        String s3 = fail.getMap().get("");  // Compiler error, why?
    }
}

我得到了这个编译器错误。

MyClass.java:20: incompatible types
found   : java.lang.Object
required: java.lang.String
                String s3 = fail.getMap().get("");  // Compiler error

未选中警告的确切文本是什么? - Powerlord
警告:[unchecked] 未经检查的转换 - Craig P. Motlin
4个回答

38

明白了。实际上,尽管看起来很奇怪,这并不是一个 bug。

根据 JLS 的第 4.8 节(原始类型)

对于原始类型 C 中的构造函数(§8.8)、实例方法(§8.8、§9.4)或非静态字段(§8.3),如果它们没有从超类或超接口中继承而来,则其类型是对应于 C 的泛型声明中其类型的擦除形式。原始类型 C 的静态成员的类型与对应于 C 的泛型声明中其类型相同。

因此,即使该方法的类型签名不使用类本身的任何类型参数,类型擦除仍然会生效,并且签名变得有效。

public Map getMap()

换句话说,我认为您可以将原始类型想象成与泛型类型相同的API,但从每个地方(在API中而不是实现中)删除了所有<X>位。
编辑:这段代码:
MyClass unchecked = new MyClass();
Map<String, String> map = unchecked.getMap();  // Unchecked warning, why?
String s2 = map.get("");

编译成功是因为从原始的Map类型到Map<String, String>类型存在一个隐式但未经检查的转换。在最后一种情况下,您可以通过进行显式转换(在执行时不执行任何操作)来获得相同的效果:

// Compiles, but with an unchecked warning
String x = ((Map<String, String>)fail.getMap()).get("");

3
嗯,有点道理...但同时也非常愚蠢... - Joachim Sauer

5

很抱歉我无法告诉你为什么它失败了,但是我可以给你一个简单的解决方法:

fail的类型更改为MyClass<?>,然后它就可以编译成功了。


一个强制转换也可以正常工作。为什么一个类型的问号会影响地图的类型?这对我来说仍然没有意义。 - Craig P. Motlin
1
@Motlin:Jon已经很好地解释了:MyClass<?>不是原始类型。 - Joachim Sauer
1
现在我明白了你的解决方法为什么有效,我将使用它而不是强制转换。谢谢。 - Craig P. Motlin

3
非常有趣的问题,Jon Skeet给出了非常有趣的答案。
我想补充一些关于Java编译器行为是否愚蠢的内容。
我认为编译器假定如果您没有在泛型类中指定类型参数,则无法(或不想)使用任何类型参数。您可以使用早于5的版本的Java,或喜欢手动转换类型。
对我来说,这似乎并不愚蠢。

1

泛型在编译后会被擦除。

当你执行以下操作时:

Map<String, String> map = unchecked.getMap();

你正在将Map强制转换为Map<String, String>,这就是未经检查的警告的原因。然而,在此之后,你可以执行以下操作:

String s2 = map.get("");

因为 map 的类型是 Map<String, String>。

然而,当你执行

String s3 = fail.getMap().get(""); 

你没有将 fail.getMap() 转换为任何类型,因此它被视为普通的 Map,而不是 Map<String, String>。

在后者中,你应该做的是:

String s3 = ((Map<String, String>fail.getMap()).get("");

这将仍然会引发警告,但仍然可以正常工作。


2
那第一个为什么能编译通过呢?“擦除”并不是完整的答案。 - Craig P. Motlin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接