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

a 夏天 提交于 2020-01-06 14:10:45

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

问题:

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

#1楼

原始类型是没有任何类型参数的泛型类或接口的名称。 例如,给定通用Box类:

public class Box<T> {
    public void set(T t) { /* ... */ }
    // ...
}

要创建Box<T>的参数化类型,请为形式类型参数T提供一个实际的类型参数:

Box<Integer> intBox = new Box<>();

如果省略实际类型参数,则创建Box<T>的原始类型:

Box rawBox = new Box();

因此, Box是通用类型Box<T>的原始类型。 但是,非泛型类或接口类型不是原始类型。

原始类型显示在旧版代码中,因为在JDK 5.0之前,许多API类(例如Collections类)不是通用的。 使用原始类型时,您实际上会获得泛型行为Box为您提供Object 。 为了向后兼容,允许将参数化类型分配给其原始类型:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;               // OK

但是,如果将原始类型分配给参数化类型,则会收到警告:

Box rawBox = new Box();           // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox;     // warning: unchecked conversion

如果您使用原始类型来调用在相应的泛型类型中定义的泛型方法,也会收到警告:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8);  // warning: unchecked invocation to set(T)

该警告表明原始类型会绕过通用类型检查,从而将不安全代码的捕获推迟到运行时。 因此,应避免使用原始类型。

Type Erasure部分提供了有关Java编译器如何使用原始类型的更多信息。

未检查的错误消息

如前所述,将旧代码与通用代码混合时,您可能会遇到类似于以下内容的警告消息:

注意:Example.java使用未经检查或不安全的操作。

注意:使用-Xlint:unchecked重新编译以获取详细信息。

当使用对原始类型进行操作的较旧API时,可能会发生这种情况,如以下示例所示:

public class WarningDemo {
    public static void main(String[] args){
        Box<Integer> bi;
        bi = createBox();
    }

    static Box createBox(){
        return new Box();
    }
}

术语“未检查”是指编译器没有足够的类型信息来执行确保类型安全所需的所有类型检查。 尽管编译器会给出提示,但是默认情况下“ unchecked”警告是禁用的。 要查看所有“未选中”的警告,请使用-Xlint:unchecked重新编译。

使用-Xlint:unchecked重新编译前面的示例将显示以下附加信息:

WarningDemo.java:4: warning: [unchecked] unchecked conversion
found   : Box
required: Box<java.lang.Integer>
        bi = createBox();
                      ^
1 warning

要完全禁用未检查的警告,请使用-Xlint:-unchecked标志。 @SuppressWarnings("unchecked")注释禁止未检查的警告。 如果您不熟悉@SuppressWarnings语法,请参阅注释。

原始资料: Java教程


#2楼

Java中的“原始”类型是非泛型的类,它处理“原始”对象,而不是类型安全的泛型类型参数。

例如,在Java泛型可用之前,您将使用如下收集类:

LinkedList list = new LinkedList();
list.add(new MyObject());
MyObject myObject = (MyObject)list.get(0);

当您将对象添加到列表时,它并不关心它是什么类型的对象,并且当您从列表中获取对象时,必须将其显式转换为期望的类型。

使用泛型,您可以删除“未知”因素,因为必须明确指定可以在列表中找到的对象类型:

LinkedList<MyObject> list = new LinkedList<MyObject>();
list.add(new MyObject());
MyObject myObject = list.get(0);

请注意,对于泛型,您不必强制转换来自get调用的对象,该集合是预定义的,仅适用于MyObject。 这是仿制药的主要驱动因素。 它将运行时错误的来源更改为可以在编译时检查的错误。


#3楼

什么是原始类型?为什么我经常听到它们不应该在新代码中使用?

“原始类型”是在没有为其类型化的参数指定类型实参的情况下使用泛型类,例如,使用List而不是List<String> 。 当将泛型引入Java时,几个类已更新为使用泛型。 将这些类用作“原始类型”(不指定类型实参)允许遗留代码继续编译。

“原始类型”用于向后兼容。 不建议在新代码中使用它们,因为将泛型类与类型参数一起使用可实现更强的键入,这反过来又可以提高代码的可理解性并导致更早地发现潜在的问题。

如果我们不能使用原始类型,那有什么选择呢?有什么更好的选择?

首选的替代方法是按预期使用通用类-带有合适的类型参数(例如List<String> )。 这使程序员可以更具体地指定类型,将有关变量或数据结构的预期用途的更多含义传达给未来的维护者,并允许编译器强制实施更好的类型安全性。 这些优点一起可以提高代码质量,并有助于防止某些编码错误的引入。

例如,对于程序员希望确保名为“名称”的List变量仅包含字符串的方法:

List<String> names = new ArrayList<String>();
names.add("John");          // OK
names.add(new Integer(1));  // compile error

#4楼

原始类型是在使用泛型类型时缺少类型参数

不应使用Raw-type,因为它可能会导致运行时错误,例如将double插入到应该为intSet中。

Set set = new HashSet();
set.add(3.45); //ok

Set检索内容时,您不知道会发生什么。 假设您期望它全部是int ,并且将其强制转换为Integerdouble精度3.45出现时在运行时发生异常。

类型参数添加到Set ,您将立即获得编译错误。 这种先发制人的错误使您可以在运行时发生故障之前解决问题(从而节省了时间和精力)。

Set<Integer> set = new HashSet<Integer>();
set.add(3.45); //NOT ok.

#5楼

什么是原始类型?

Java语言规范将原始类型定义如下:

JLS 4.8原始类型

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

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

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

  • 原始类型R的非static成员类型,该成员类型不继承自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 Stringinstanceof 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 ,以及上面的代码将编译。

也可以看看


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

以下是来自有效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作为参数,则它将被编译,因此您将失去从泛型获得的类型安全性。

也可以看看


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

List<Object>List<String>等都是List<?> ,因此很可能会说它们只是List而已。 但是,有一个主要区别:由于List<E>仅定义add(E) ,因此您不能仅将任意对象添加到List<?> 。 另一方面,由于原始类型List没有类型安全性,因此您几乎可以add任何内容addList

请考虑以下片段的以下变体:

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:

这是可能作为一种类型的使用参数化的类型或数组类型,其元素类型是参数化类型的擦除的擦除这种类型称为原始类型

[...]

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

未从其超类或超接口继承的原始类型C的构造函数,实例方法或非static字段的类型为原始类型,该原始类型对应于在与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);
    }
}

当我们使用原始的MyTypegetNames将被擦除,因此它返回原始的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>

也可以看看

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!