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

2022-08-31 04:04:10

问题:

  • Java中有哪些原始类型,为什么我经常听说它们不应该在新代码中使用?
  • 如果我们不能使用原始类型,还有什么替代方案,它如何更好?

答案 1

什么是原始类型?

Java 语言规范定义了原始类型,如下所示:

JLS 4.8 原始类型

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

  • 通过采用泛型类型声明的名称而不带随附的类型参数列表而形成的引用类型。

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

  • 原始类型的非成员类型,它不是从 的超类或超接口继承的。staticRR

下面是一个示例来说明:

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!)
    }
}

这里,是一个参数化类型JLS 4.5)。通常将这种类型简单地称为简称,但从技术上讲,该名称是 。MyType<E>MyTypeMyType<E>

mt具有原始类型(并生成编译警告)由上述定义中的第一个项目符号点; 还有第三个项目符号点的原始类型。inn

MyType.Nested不是参数化类型,即使它是参数化类型 的成员类型,因为它是 .MyType<E>static

mt1,并且都是使用实际类型参数声明的,因此它们不是原始类型。mt2


原始类型有什么特别之处?

从本质上讲,原始类型的行为与引入泛型之前一样。也就是说,以下内容在编译时是完全合法的。

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

现在我们在运行时遇到麻烦,因为包含的东西不是.namesinstanceof String

据推测,如果您只想包含 ,您也许仍然可以使用原始类型并手动检查每个自己,然后手动转换为 .更好的是,虽然不是使用原始类型,而是让编译器为您完成所有工作,利用Java泛型的强大功能。namesStringaddStringnames

List<String> names = new ArrayList<String>();
names.add("John");
names.add("Mary");
names.add(Boolean.FALSE); // compilation error!

当然,如果你确实想允许一个,那么你可以把它声明为,上面的代码就会编译。namesBooleanList<Object> names

另请参见


原始类型与用作类型参数有何不同?<Object>

以下是《有效的 Java 第 2 版》第 23 项中的一句话:不要在新代码中使用原始类型

原始类型和参数化类型之间有什么区别?从广义上讲,前者选择退出泛型类型检查,而后者明确告诉编译器它能够容纳任何类型的对象。虽然可以将 a 传递给 类型的参数,但不能将其传递给 类型的参数。泛型有子类型规则,并且是 原始类型的子类型 ,但不是参数化类型的子类型。因此,如果使用原始类型(如 List),则会失去类型安全性,但如果使用参数化类型(如 List<Object>),则不会丢失类型安全性。ListList<Object>List<String>ListList<Object>List<String>ListList<Object>

为了说明这一点,请考虑以下方法,该方法采用 a 并附加一个 。List<Object>new Object()

void appendNewObject(List<Object> list) {
   list.add(new Object());
}

Java中的泛型是不变的。A 不是 ,因此以下内容将生成编译器警告:List<String>List<Object>

List<String> names = new ArrayList<String>();
appendNewObject(names); // compilation error!

如果您已声明将原始类型作为参数,那么这将进行编译,因此您将失去从泛型中获得的类型安全性。appendNewObjectList

另请参见


原始类型与用作类型参数有何不同?<?>

List<Object>,等等都是,所以可能很容易说他们只是相反。但是,有一个主要区别:由于 a 仅定义 ,因此不能将任意对象添加到 .另一方面,由于原始类型没有类型安全性,因此您几乎可以对.List<String>List<?>ListList<E>add(E)List<?>ListaddList

请考虑上一个代码段的以下变体:

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 listList<String> names


原始类型是该类型的擦除

返回 JLS 4.8:

可以将参数化类型的擦除或元素类型为参数化类型的数组类型的擦除用作类型。此类类型称为原始类型

[...]

原始类型的超类(分别是超接口)是泛型类型的任何参数化的超类(超接口)的擦除。

未从其超类或超接口继承的原始类型的构造函数、实例方法或非字段的类型是原始类型,它与在对应于 的泛型声明中擦除其类型相对应。staticCC

简单来说,使用原始类型时,构造函数、实例方法和非字段也会被擦除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);
    }
}

当我们使用 raw 时,也会被删除,因此它返回 raw !MyTypegetNamesList

JLS 4.6 继续解释以下内容:

类型擦除还将构造函数或方法的签名映射到没有参数化类型或类型变量的签名。构造函数或方法签名的擦除是一种签名,其名称与 中给出的所有形式参数类型的名称相同,并且擦除了 。sss

如果方法或构造函数的签名被擦除,则方法的返回类型和泛型方法或构造函数的类型参数也会被擦除。

泛型方法的签名的擦除没有类型参数。

下面的错误报告包含编译器开发人员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.classList<String>.class
  • instanceof操作数,例如 ,不是o instanceof Seto instanceof Set<String>

另请参见


答案 2

Java中有哪些原始类型,为什么我经常听说它们不应该在新代码中使用?

原始类型是Java语言的古老历史。起初,他们拥有更多的东西,也没有少一点。对所需转换的每个操作都从到所需的类型。CollectionsObjectsCollectionsObject

List aList = new ArrayList();
String s = "Hello World!";
aList.add(s);
String c = (String)aList.get(0);

虽然这在大多数情况下都有效,但错误确实发生了

List aNumberList = new ArrayList();
String one = "1";//Number one
aNumberList.add(one);
Integer iOne = (Integer)aNumberList.get(0);//Insert ClassCastException here

旧的无类型集合无法强制执行类型安全,因此程序员必须记住他在集合中存储的内容。
为了绕过这个限制而发明的泛型,开发人员将声明一次存储的类型,编译器将这样做。

List<String> aNumberList = new ArrayList<String>();
aNumberList.add("one");
Integer iOne = aNumberList.get(0);//Compile time error
String sOne = aNumberList.get(0);//works fine

比较:

// Old style collections now known as raw types
List aList = new ArrayList(); //Could contain anything
// New style collections with Generics
List<String> aList = new ArrayList<String>(); //Contains only Strings

更复杂的可比较界面:

//raw, not type save can compare with Other classes
class MyCompareAble implements CompareAble
{
   int id;
   public int compareTo(Object other)
   {return this.id - ((MyCompareAble)other).id;}
}
//Generic
class MyCompareAble implements CompareAble<MyCompareAble>
{
   int id;
   public int compareTo(MyCompareAble other)
   {return this.id - other.id;}
}

请注意,不可能使用原始类型实现接口。为什么你不应该使用它们:CompareAblecompareTo(MyCompareAble)

  • 存储在 中的任何内容都必须先进行转换,然后才能使用ObjectCollection
  • 使用泛型可实现编译时检查
  • 使用原始类型与将每个值存储为Object

编译器的作用:泛型是向后兼容的,它们使用与原始类型相同的java类。魔术主要发生在编译时。

List<String> someStrings = new ArrayList<String>();
someStrings.add("one");
String one = someStrings.get(0);

将编译为:

List someStrings = new ArrayList();
someStrings.add("one"); 
String one = (String)someStrings.get(0);

这与直接使用原始类型时编写的代码相同。以为我不确定界面会发生什么,我猜它创建了两个函数,一个取a,另一个取一个,并在转换后将其传递给第一个函数。CompareAblecompareToMyCompareAbleObject

原始类型的替代方案有哪些:使用泛型


推荐