混合使用原始类型和泛型方法

29

以下是一个问题,这段代码在JDK 1.6和JDK 1.7下编译都没有问题:

ArrayList<String> a = new ArrayList<String>();
String[] s = a.toArray(new String[0]);

然而,如果我将 List 引用声明为原始类型:
ArrayList a = new ArrayList();
String[] s = a.toArray(new String[0]);

我收到了编译器错误,它说需要String[],但是找到了Object[]

这意味着我的编译器将泛型方法解释为返回Object[],尽管它接收到的参数是String[]

我仔细检查了toArray(myArray)方法的签名:

<T> T[] toArray(T[] a);

因此,这是一种参数化方法,其类型参数<T>与List的类型参数(即<E>)没有任何关系。
我不知道在此处使用原始类型如何影响使用独立类型参数的参数化方法的评估。
  • 有人知道为什么这段代码无法编译吗?
  • 有人知道任何记录了此行为的参考资料吗?

1
如果您使用(String[])a.toArray();,由于您没有使用泛型,所以必须进行强制转换。 - Hovercraft Full Of Eels
@HovercraftFullOfEels 感谢您的建议。我知道我可以进行强制类型转换,但我正在为程序员认证考试学习,并且在尝试一些泛型代码时才发现了这个问题,我无法解释它。以前从未遇到过这样的情况。因此,我更希望得到一个为什么会发生这种情况的解释。 - Edwin Dalorzo
1
有趣的发现。奇怪的是,如果你将通用参数明确指定为Object(ArrayList<Object>),该错误就会消失。这很奇怪,因为Object应该是省略类型的默认值。 - Perception
1
同样有趣的是,如果你使用通配符进行参数化,ArrayList<?> a = new ArrayList();,错误就会消失。此外,如果使用Integer作为类型:ArrayList<Integer> a = new ArrayList();,错误也会消失。 - creemama
可能是重复的问题:https://dev59.com/WXNA5IYBdhLWcg3wI6V4。 - creemama
@creemama 是的,看起来此前已经有人提出过这个问题,但是还没有给出解释。在之前的问题中,答案是:“您忘记在List中指定类型参数”。但我想知道为什么不指定类型参数E会影响参数化方法中的类型参数T - Edwin Dalorzo
5个回答

37

如果你在原始形式中引用一个泛型类,它与你期望的不完全相同,因为这会使得你丧失使用泛型 任何方式 在实例成员中的能力。 它不仅限于泛型方法,可以看下面的例子:

 public class MyContainer<T> {

     public List<String> strings() {
         return Arrays.asList("a", "b");
     }
 }

 MyContainer container = new MyContainer<Integer>();
 List<String> strings = container.strings(); //gives unchecked warning!

这是JLS(4.8)的相关部分:

对于一个非从父类或父接口继承而来的原始类型 C 的构造函数 (§8.8)、实例方法 (§8.4, §9.4) 或非静态域 (§8.3),它们的类型为与泛型声明中 C 的擦拭相对应的原始类型。


+1 看起来编译器会根据使用原始类型的提示,在给定引用中禁用任何类型的泛型。嗯,有趣。我想知道这是否在JLS中有记录。你碰巧知道吗? - Edwin Dalorzo
@Edwin:我在我的答案中引用了它,看起来Premraj也找到了它。 - Mark Peters
@MarkPeters 太棒了,这正是我在寻找的。b(^_^)d - Edwin Dalorzo

8
当您不使用泛型时,编译器会将其视为原始数据类型,因此每个泛型类型都变成了Object,因此您不能传递String[],因为它需要Object[]
所以这里的问题是 - 如果您使用泛型

List l = new ArrayList<String>();

您正在使用原始类型,其所有实例成员都被其擦除后的对应项所替换。特别是,在实例方法声明中出现的每个参数化类型都将被其原始对应项所替换。有关详细信息,请参见JLS 4.8。


编译器如何知道我没有使用泛型,List的类型参数与泛型方法toArray的类型参数无关? - Edwin Dalorzo
编译器知道你是否将'a'声明为ArrayList或ArrayList<String>。它知道这一点是通过...嗯,看你写了什么...;-) - Neil Coffey
改变主意后,我认为说编译器进入1.4编译模式是不准确的。我理解你的意思是它将引用处理为原始类型(就像在没有泛型的JDK 1.4中一样)。但你的陈述可能被解释为如果我向编译器传递了target=1.4,那么代码将被编译为生成1.4兼容的字节码,但我认为情况并非如此。无论如何,在其他人用投票反对你之前,你可能需要澄清这一点。;-) - Edwin Dalorzo
确实,我只是想简化理解的事情... 我会更新答案 :) - Premraj

6
这是我在规范中找到的最接近描述这种观察行为的描述:
构造函数(§8.8)、实例方法(§8.8、§9.4)或非静态字段(§8.3)M的类型,对于一个不从其超类或超接口继承的原始类型C是在与C对应的泛型声明中被擦除的。对于原始类型C的静态成员的类型与其在与C对应的泛型声明中的类型相同。
向不从其超类或超接口继承的原始类型C的非静态类型成员传递实际类型参数是编译时错误。
基于上述和观察到的行为,我认为可以安全地说:所有泛型参数类型都从原始类型中移除。当然,在非遗留代码中不鼓励使用原始类型本身:
仅允许使用原始类型来兼容遗留代码。在引入泛型进入Java编程语言之后编写的代码中强烈不建议使用原始类型。未来版本的Java编程语言可能会禁止使用原始类型。

我感谢 Perception 进行的研究。这确实让我更清楚了。加一分给这个粗体引用。 - Edwin Dalorzo

1

可能很有趣的是,这种行为可以被“解决”。使用两个接口,一个基本的非泛型接口和一个泛型接口。然后编译器知道基本非泛型接口的所有函数都不是泛型的,并会像这样对待它们。

如果使用流和函数链接,这种行为非常令人恼火,因此我会按照以下方式解决它。

通过接口继承解决方案

public interface GenericInterface<X extends Y> extends BaseInterface
{
    X getValue();
}

public interface BaseInterface
{
    String getStringValue();
}

现在您可以无需警告或问题就执行以下操作:
GenericInterface object = ...;
String stringValue = object.getStringValue();

0

由于您的ArrayList是非参数化列表,因此无法将类型参数传递到toArray()方法中,它只知道它保存对象。 a.toArray()将始终返回一个Object[]数组。如果您想指定它保存特定的String类型,则必须再次将其强制转换为(String[])(其中包含所有危险)。


我很感激你的回答,但是无论我是否使用泛型,ArrayList 的内部数组始终为 Object[] 类型。这不会影响到当我使用参数化的 ArrayList 引用时的参数化方法。然而,所讨论的方法是参数化的,期望一个 T[] 作为其返回类型。如果它与 ArrayList 的参数化类型无关,那么编译器为什么会忽略它呢?我仍然看不出 TE 之间的关系,以至于编译器会表现出这种行为。 - Edwin Dalorzo

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