协慌网

登录 贡献 社区

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

问题:

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

答案

什么是原始类型?

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 String的 instanceof。

据推测,如果你想要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任何内容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:

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

[...]

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

未从其超类或超接口继承的原始类型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>

也可以看看

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

原始类型是 Java 语言的古老历史。最初有Collections ,它们只保留Objects ,也保留了对象。 Collections进行的每个操作都Object强制转换为所需的类型。

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;}
}

请注意,无法使用带有原始类型的compareTo(MyCompareAble) CompareAble为什么不应该使用它们:

  • Collection存储的任何Object都必须先进行投射,然后才能使用
  • 使用泛型可以进行编译时检查
  • 使用原始类型与将每个值存储为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);

如果直接使用原始类型,则这与您编写的代码相同。我以为我不确定用CompareAble接口会发生什么,我猜想它创建了两个compareTo函数,一个函数使用MyCompareAble ,另一个函数使用Object然后将其传递给第一个对象。

原始类型的替代方法是什么:使用泛型

原始类型是没有任何类型参数的泛型类或接口的名称。例如,给定通用 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)

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

“类型清除” 部分提供了有关 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 教程