java - 什么是原始类型,为什么我们不应该使用它呢?

标签 java generics raw-types

问题:


Java中的原始类型是什么?为什么我经常听到不应该在新代码中使用它们的信息?
如果我们不能使用原始类型,那有什么选择呢?有什么更好的选择?

最佳答案

什么是原始类型?
Java语言规范对原始类型的定义如下:
JLS 4.8 Raw Types

原始类型定义为以下之一:

通过采用通用类型声明的名称而没有随附的类型参数列表而形成的引用类型。

数组类型,其元素类型为原始类型。

不是从static的超类或超接口继承的原始类型R的非R成员类型。



这是一个例子说明:

public class MyType<E> {
    class Inner { }
    static class Nested { }
    
    public static void main(String[] args) {
        MyType mt;          // warning: MyType is a raw type
        MyType.Inner inn;   // warning: MyType.Inner is a raw type

        MyType.Nested nest; // no warning: not parameterized type
        MyType<Object> mt1; // no warning: type parameter given
        MyType<?> mt2;      // no warning: type parameter given (wildcard OK!)
    }
}

在这里,MyType<E>是参数化类型(JLS 4.5)。通常将这种类型简称为MyType,但从技术上讲,名称为MyType<E>
mt在上述定义的第一个要点之前具有原始类型(并生成编译警告); inn在第三个要点之前也具有原始类型。
MyType.Nested不是参数化类型,即使它是参数化类型MyType<E>的成员类型,因为它是static
mt1mt2都使用实际的类型参数声明,因此它们不是原始类型。

原始类型有何特别之处?
本质上,原始类型的行为与引入泛型之前的行为相同。也就是说,以下在编译时完全合法。
List names = new ArrayList(); // warning: raw type!
names.add("John");
names.add("Mary");
names.add(Boolean.FALSE); // not a compilation error!

上面的代码可以正常运行,但是假设您还具有以下内容:
for (Object o : names) {
    String name = (String) o;
    System.out.println(name);
} // throws ClassCastException!
  //    java.lang.Boolean cannot be cast to java.lang.String

现在我们在运行时遇到了麻烦,因为names包含的内容不是instanceof String
大概,如果您希望names仅包含String,则可能仍可以使用原始类型并亲自检查每个add,然后手动将String中的每个项目强制转换为names。更好的是,尽管不要使用原始类型,而让编译器利用Java泛型的强大功能为您完成所有工作。
List<String> names = new ArrayList<String>();
names.add("John");
names.add("Mary");
names.add(Boolean.FALSE); // compilation error!

当然,如果您确实希望names允许Boolean,则可以将其声明为List<Object> names,并且上面的代码可以编译。
也可以看看

Java Tutorials/Generics


原始类型与使用<Object>作为类型参数有何不同?
以下是来自Effective Java 2nd Edition,项目23的引用:不要在新代码中使用原始类型:

原始类型List和参数化类型List<Object>之间有什么区别?松散地说,前者选择了泛型类型检查,而后者则明确告诉编译器它能够保存任何类型的对象。虽然可以将List<String>传递给类型为List的参数,但不能将其传递给类型为List<Object>的参数。有泛型的子类型化规则,List<String>是原始类型List的子类型,但不是参数化类型List<Object>的子类型。因此,如果您使用诸如List之类的原始类型,则会失去类型安全性,但是如果您使用诸如List<Object>之类的参数化类型,则不会失去类型安全性。

为了说明这一点,请考虑以下采用List<Object>并附加new Object()的方法。
void appendNewObject(List<Object> list) {
   list.add(new Object());
}

Java中的泛型是不变的。 List<String>不是List<Object>,因此以下内容将生成编译器警告:
List<String> names = new ArrayList<String>();
appendNewObject(names); // compilation error!

如果您已声明appendNewObject将原始类型List作为参数,则它将被编译,因此您将失去从泛型获得的类型安全性。
也可以看看

What is the difference between <E extends Number> and <Number>?
java generics (not) covariance


原始类型与使用<?>作为类型参数有何不同?
List<Object>List<String>等都是List<?>,所以可能会很想说它们只是List。但是,有一个主要区别:由于List<E>仅定义了add(E),因此您不能仅将任意对象添加到List<?>。另一方面,由于原始类型List不具有类型安全性,因此您可以对add几乎使用List进行任何操作。
请考虑以下片段的以下变体:
static void appendNewObject(List<?> list) {
    list.add(new Object()); // compilation error!
}
//...

List<String> names = new ArrayList<String>();
appendNewObject(names); // this part is fine!

编译器做了出色的工作,可以保护您避免违反List<?>的类型不变性!如果已将参数声明为原始类型List list,则代码将编译,并且违反了List<String> names的类型不变式。

原始类型是该类型的擦除
返回JLS 4.8:

可以将参数化类型的擦除或元素类型为参数化类型的数组类型的擦除用作类型。这种类型称为原始类型。
[...]
原始类型的超类(分别是超接口)是对泛型类型的任何参数化的超类(超接口)的擦除。
构造类型,实例方法或未从其超类或超接口继承的原始类型static的非C字段的类型是原始类型,其对应于在与C

简单来说,当使用原始类型时,构造函数,实例方法和非static字段也会被删除。
请看以下示例:
class MyType<E> {
    List<String> getNames() {
        return Arrays.asList("John", "Mary");
    }

    public static void main(String[] args) {
        MyType rawType = new MyType();
        // unchecked warning!
        // required: List<String> found: List
        List<String> names = rawType.getNames();
        // compilation error!
        // incompatible types: Object cannot be converted to String
        for (String str : rawType.getNames())
            System.out.print(str);
    }
}

当我们使用原始的MyType时,getNames也将被删除,从而返回原始的List
JLS 4.6继续解释以下内容:

类型擦除还将映射构造函数或方法的签名到没有参数化类型或类型变量的签名。删除构造函数或方法签名s是一种签名,由与s相同的名称以及s中给出的所有形式参数类型的擦除组成。
如果删除方法或构造函数的签名,则方法的返回类型以及通用方法或构造函数的类型参数也会被擦除。
通用方法签名的擦除没有类型参数。

以下错误报告包含编译器开发人员Maurizio Cimadamore和JLS的作者之一Alex Buckley关于为何应发生这种行为的一些想法:https://bugs.openjdk.java.net/browse/JDK-6400189。 (简而言之,它使规范更简单。)

如果不安全,为什么允许使用原始类型?
这是JLS 4.8的另一句话:

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

有效的Java 2nd Edition也要添加以下内容:

既然您不应该使用原始类型,那么语言设计者为什么要允许它们呢?提供兼容性。
引入泛型时,Java平台即将进入第二个十年,并且存在大量不使用泛型的Java代码。至关重要的是,所有这些代码都必须合法并可以与使用泛型的新代码互操作。将参数化类型的实例传递给设计用于普通类型的方法必须合法,反之亦然。这项要求称为迁移兼容性,因此决定支持原始类型。

总而言之,绝对不要在新代码中使用原始类型。您应该始终使用参数化类型。

有没有例外?
不幸的是,由于Java泛型是非泛型的,因此在新代码中必须使用原始类型有两个例外:

类文字,例如List.class,而不是List<String>.class
instanceof操作数,例如o instanceof Set,而不是o instanceof Set<String>

也可以看看

Why is Collection<String>.class Illegal?

关于java - 什么是原始类型,为什么我们不应该使用它呢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60214218/

相关文章:

java - 安全地将对象添加到无法设置的原始列表中(Java AST)

java - Arraylist 通过 java 中的 tcp?

Java 泛型,强制枚举参数

c++ - 来自模板参数的变量名?

generics - 有没有办法确定 Lua 函数的签名?

java - 从整数列表的索引中打印字符串

generics - Scala 类无法覆盖扩展 java.util.comparator 的 Java 接口(interface)中的比较方法

java - cucumber testng 运行程序失败

java - WebSocket RemoteEndpoint 不可用将数据发送回客户端的问题

C# 如何创建泛型类?