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

提问:

  • Java中的原始类型是什么,为什么我经常听到它们不应该在新代码中使用?
  • 如果我们不能使用原始类型,替代方案是什么,如何更好?
322984 次浏览

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一起使用。这一事实是泛型的主要驱动因素。它将运行时错误的来源更改为可以在编译时检查的东西。

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

“原始类型”是使用泛型类而不为其参数化类型指定类型参数,例如使用List而不是List<String>。当泛型被引入Java时,一些类被更新为使用泛型。将这些类用作“原始类型”(不指定类型参数)允许遗留代码仍然编译。

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

如果我们不能使用原始类型,替代方案是什么,如何更好?

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

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

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

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

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

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

当从Set中检索内容时,您不知道会出现什么。让我们假设您希望它都是int,当double 3.45出现时,您正在运行时将其转换为Integer;异常。

类型参数添加到Set后,您将立即收到编译错误。这种先发制人的错误可让您在运行时发生爆炸之前修复问题(从而节省时间和精力)。

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

什么是原始类型?

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 typeMyType.Inner inn;   // warning: MyType.Inner is a raw type
MyType.Nested nest; // no warning: not parameterized typeMyType<Object> mt1; // no warning: type parameter givenMyType<?> mt2;      // no warning: type parameter given (wildcard OK!)}}

在这里,MyType<E>参数化类型jls4.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自己,然后手动铸造Stringnames.甚至更好的每个项目,虽然不是使用原始类型和让编译器为你做所有的工作,利用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第2版生效,条款23:不要在新代码中使用原始类型的引用:

原始类型List和参数化类型List<Object>有什么区别?粗略地说,前者选择了泛型类型检查,而后者明确地告诉编译器它能够保存任何类型的对象。虽然你可以将List<String>传递给类型List的参数,但你不能将其传递给类型List<Object>的参数。泛型有子类型规则,List<String>是原始类型List的子类型,但不是参数化类型List<Object>的子类型。因此,List<Object>0。

为了说明这一点,考虑以下方法,它接受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: ListList<String> names = rawType.getNames();// compilation error!// incompatible types: Object cannot be converted to Stringfor (String str : rawType.getNames())System.out.print(str);}}

当我们使用原始MyType时,getNames也会被擦除,因此它返回原始List

jls4.6继续解释以下内容:

类型擦除还将构造函数或方法的签名映射到没有参数化类型或类型变量的签名。擦除构造函数或方法签名s是由与s同名的签名和s中给出的所有形式参数类型的擦除组成的签名。

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

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

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


如果它不安全,为什么允许使用原始类型?

以下是JLS 4.8的另一个引用:

只允许使用原始类型作为对遗留代码兼容性的让步。在将泛型引入Java编程语言后编写的代码中使用原始类型是强烈不鼓励的。Java编程语言的未来版本可能不允许使用原始类型。

有效Java第二版也有这个要添加:

既然您不应该使用原始类型,为什么语言设计者允许它们?提供兼容性。

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 oneaNumberList.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 errorString sOne = aNumberList.get(0);//works fine

比较:

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

更复杂的Compareable接口:

//raw, not type save can compare with Other classesclass MyCompareAble implements CompareAble{int id;public int compareTo(Object other){return this.id - ((MyCompareAble)other).id;}}//Genericclass 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,并在铸造后将其传递给第一个函数。

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

 private static List<String> list = new ArrayList<String>();

您应该指定类型参数。

警告建议定义为支持泛型的类型应该参数化,而不是使用它们的原始形式。

List被定义为支持泛型:public class List<E>。这允许许多类型安全的操作,这些操作在编译时检查。

也就是说,你的list是一个未指定对象的List。也就是说,Java不知道列表中有什么样的对象。然后,当你想迭代列表时,你必须强制转换每个元素,以便能够访问该元素的属性(在本例中为String)。

一般来说,参数化集合是一个更好的主意,所以你没有转换问题,你只能添加参数化类型的元素,你的编辑器会为你提供合适的方法来选择。

private static List<String> list = new ArrayList<String>();

编译器希望你这样写:

private static List<String> list = new ArrayList<String>();

因为否则,您可以将任何您喜欢的类型添加到list中,使实例化成为new ArrayList<String>()毫无意义。Java泛型只是编译时的特性,因此使用new ArrayList<String>()创建的对象将很乐意接受IntegerJFrame元素,如果分配给“原始类型”List的引用——对象本身对它应该包含的类型一无所知,只有编译器知道。

原始类型是没有任何类型参数的泛型类或接口的名称。例如,给定泛型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类(例如集合类)不是泛型的。使用原始类型时,你基本上会得到预泛型行为——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重新编译:未选中以获取详细信息。

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

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

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

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

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

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

来源:Java教程

我在做了一些示例练习后发现了这个页面,并且有完全相同的困惑。

============== 我从示例提供的代码开始 ===============

public static void main(String[] args) throws IOException {
Map wordMap = new HashMap();if (args.length > 0) {for (int i = 0; i < args.length; i++) {countWord(wordMap, args[i]);}} else {getWordFrequency(System.in, wordMap);}for (Iterator i = wordMap.entrySet().iterator(); i.hasNext();) {Map.Entry entry = (Map.Entry) i.next();System.out.println(entry.getKey() + " :\t" + entry.getValue());}

public static void main(String[] args) throws IOException {// replace with TreeMap to get them sorted by nameMap<String, Integer> wordMap = new HashMap<String, Integer>();if (args.length > 0) {for (int i = 0; i < args.length; i++) {countWord(wordMap, args[i]);}} else {getWordFrequency(System.in, wordMap);}for (Iterator<Entry<String, Integer>> i = wordMap.entrySet().iterator(); i.hasNext();) {Entry<String, Integer> entry =   i.next();System.out.println(entry.getKey() + " :\t" + entry.getValue());}
}

===============================================================================

它可能更安全,但花了4个小时来分解哲学…

我在这里考虑多个案例,通过这些案例你可以澄清这个概念

1. ArrayList<String> arr = new ArrayList<String>();2. ArrayList<String> arr = new ArrayList();3. ArrayList arr = new ArrayList<String>();

案例1

ArrayList<String> arr它是一个ArrayList引用变量,类型为String,引用类型为StringArralyList对象。这意味着它只能保存String类型的Object。

它是一个严格的String不是一个原始类型,所以,它永远不会引发警告。

    arr.add("hello");// alone statement will compile successfully and no warning.
arr.add(23);  //prone to compile time error.//error: no suitable method found for add(int)

案例2

在这种情况下,ArrayList<String> arr是严格类型,但您的对象new ArrayList();是原始类型。

    arr.add("hello"); //alone this compile but raise the warning.arr.add(23);  //again prone to compile time error.//error: no suitable method found for add(int)

这里arr是一个严格类型。因此,添加integer时会引发编译时错误。

警告:-Raw类型对象被引用到ArrayListStrict类型引用变量。

案件3

在这种情况下,ArrayList arr是原始类型,但您的对象new ArrayList<String>();是严格类型。

    arr.add("hello");arr.add(23);  //compiles fine but raise the warning.

它将向其中添加任何类型的Object,因为arr是Raw类型。

警告:-Strict类型对象被引用到raw类型引用的变量。

教程页面

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

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

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

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

如果省略了实际类型参数,则创建一个原始类型的Box:

Box rawBox = new Box();

这是另一个原始类型会咬你的情况:

public class StrangeClass<T> {@SuppressWarnings("unchecked")public <X> X getSomethingElse() {return (X)"Testing something else!";}
public static void main(String[] args) {final StrangeClass<String> withGeneric    = new StrangeClass<>();final StrangeClass         withoutGeneric = new StrangeClass();final String               value1,value2;
// Compilesvalue1 = withGeneric.getSomethingElse();
// Produces compile error:// incompatible types: java.lang.Object cannot be converted to java.lang.Stringvalue2 = withoutGeneric.getSomethingElse();}}

这是违反直觉的,因为您希望原始类型只影响绑定到类类型参数的方法,但它实际上会影响具有自己类型参数的泛型方法。

正如在接受的答案中提到的,您将失去对原始类型代码中泛型的所有支持。每个类型参数都转换为其擦除(在上面的示例中只是Object)。

原始类型在表达你想表达的内容时很好。

例如,反序列化函数可能返回List,但它不知道列表的元素类型。所以List是这里合适的返回类型。

避免原始类型。

原始类型是指在不指定类型参数的情况下使用泛型类型。

例如:

list是原始类型,而List<String>是参数化类型。

当在JDK 1.5中引入泛型时,原始类型仅保留以保持与旧版本Java的向后兼容性。

虽然使用原始类型仍然是可能的,但应该避免:

  • 他们通常需要石膏。
  • 它们不是类型安全的,一些重要类型的错误只会在运行时出现。
  • 它们的表现力较差,并且不会以与参数化类型相同的方式进行自我记录。

示例:

import java.util.*;public final class AvoidRawTypes {void withRawType() {//Raw List doesn't self-document,//doesn't state explicitly what it can containList stars = Arrays.asList("Arcturus", "Vega", "Altair");Iterator iter = stars.iterator();while (iter.hasNext()) {String star = (String) iter.next(); //cast neededlog(star);}}
void withParameterizedType() {List < String > stars = Arrays.asList("Spica", "Regulus", "Antares");for (String star: stars) {log(star);}}
private void log(Object message) {System.out.println(Objects.toString(message));}} 

参考:https://docs.oracle.com/javase/tutorial/java/generics/rawTypes.html

稍微综合一下:原始类型是没有类型参数的泛型类型(例如:ListList<E>的原始类型)不应该使用原始类型。它们的存在是为了与旧版本的Java兼容。我们希望尽快发现错误(编译时),使用原始类型可能会在运行时导致错误。我们仍然需要在两种情况下使用原始类型:

  • 类文字的用法(List.class)
  • instance of的用法

示例:

//Use of raw type : don't !private final Collection stamps = ...stamps.add(new Coin(...)); //Erroneous insertion. Does not throw any errorStamp s = (Stamp) stamps.get(i); // Throws ClassCastException when getting the Coin
//Common usage of instance ofif (o instanceof Set){Set<?> = (Set<?>) o;}