Java泛型中List、List<Object>、List<?>的区别
Java 1.5中引入了泛型的概念以增加代码的安全性与清晰度,同时为了提供对旧代码的兼容性,让旧代码不经过改动也可以在新版本中运行,Java提供了原生态类型(或称原始类型)。但是实际中在新的代码中已经不应该使用原生态类型。
原生态类型的含义是不带任何实际参数的泛型名称,例如Java 1.5后改为泛型实现的List<E>
,List
就是它的原生态类型,与没有引入泛型之前的类型完全一致。
而在虚拟机层面上,是没有泛型这一概念的——所有对象都属于普通类。在编译时,所有的泛型类都会被视为原生态类型。
那么为什么不应该使用原生态类型呢?
如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势。——Effective Java
泛型的目的简单地说就是可以让一些运行时才能发现的错误可以在编译期间就可以被编译器所检测出,运行时出问题的代价与编译期出现问题的代价的差别可想而知。换句话说,泛型是编译器的一种及时发现错误的机制,同时也给用户带来了代码的清晰与简洁的附加好处(不必再写一些复杂而危险并且不直观的强制类型转换)。
下面就进入正题谈谈以List
为例时List
、List<Object>
、List<?>
的区别。
先下定义:
下面看一段代码:
1 | public class DiffInGeneric { |
我们创建了一个List<String>
类型的对象strings
,再把它赋给原生态类型List
,这是可以的。但是第5行中尝试把它传递给List<Object>
时,出现了一个类型不相容错误,注意,这是一个编译期错误。
这是因为泛型有子类型化的规则:
List<String>
是原生态类型List
的一个子类型。虽然String
是Object
的子类型,但是由于泛型是不可协变的,List<String>
并不是List<Object>
的子类型,所以这里的传递无法通过编译。
如果像上面那样使用原生态类型会有什么隐患呢?看下面一段代码:
1 | public class DiffInGeneric { |
编译器提示了两条警告:
第8行:
1 | warning: [rawtypes] found raw type: List |
警告发现了原生态类型List
,同时还贴心地指出了List<E>
的形式以及E
的来源。
第9行:
1 | warning: [unchecked] unchecked call to add(E) as a member of the raw type List |
同样指出了我们正在把一个对象添加到List
中,而这个添加过程由于我们使用了原生态类型而无法被检验。
如果忽略这两条警告并运行这个程序,显然会出现一条错误:
第5行:
1 | Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String |
我们试图把一个自动装箱后的Integer
对象插入到了一个被声明为List<String>
的List
中,由于我们在unsafeAdd
方法中使用了原生态类型,从而使得编译器无法在编译期间检查add
参数的合法性,从而没有产生编译错误而是产生了一条警告,运行后当试图把这个错误的Integer
对象作为String
取出时就会出现ClassCaseException
异常,这是个运行时的异常,导致了程序中断。
如果我们把unsafeAdd
方法的参数从List
改为List<Object>
会发生什么呢?正如之前所说的那样,由于List<String>
并不是List<Object>
的子类型,所以在传递参数的时候就会出现第一段代码中出现的编译期错误。这体现了泛型所带来的安全性。
可以这么说,List<Object>
唯一特殊的地方只是Object
是所有类型的超类,由于泛型的不可协变性,它只能表示List
中可以容纳所有类型的对象,却不能表示任何参数类型的List<E>
。
而List<?>
则是通配符类型中的一种特例,它并没有extend
或super
这样的限制,从而可以做到引用任意参数类型的List<E>
。但由于没有表示类型的符号(E
),在方法中无法引用这个类型,所以它只用于无需使用具体类型的方法之中,如果不是这个情况,则需要使用泛型方法(只用List<?>
的不是一个泛型方法,它具有List<?>
这个固定的参数`)。
但是List<?>
还是不能用作上面的unsafeAdd
的参数,修改后会出现一条奇怪的编译错误:
1 | error: no suitable method found for add(Object) |
这是因为无法将任何元素(null
除外)放入List<?>
中。这又是为什么呢?先来看一个有限定通配符的例子:
1 | public class DiffInGeneric { |
第7行报出了与之前相似的编译错误:
1 | error: no suitable method found for add(Integer) |
这次我们可以看出错误的原因:可以将一个List<Integer>
传递给List<? extends Number>
,因为Integer
是Number
的子类,符合限定符的条件。同理,也可以将类似的对象传递给它,当然也可以把List<Number>
传递给它。
如果允许这个对象的add
操作,我们无法知道这个参数是否与对象的泛型参数相同,因为我们只知道它是Number
的一个子类。
1 | List<? extends Parent> list = new ArrayList<Child>(); |
上面的1,2两行是完全合法的,如果允许第3行的add
操作,那么会把一个Parent
对象加入到一个实际类型是Child
的List
中,而Parent
is-not-a Child
,这破坏了Java的类型安全,是绝对不允许的。
上面是有限制通配符的情况,那么针对List<?>
这样的无限制通配符更是如此。因此,为了保证类型安全,不允许对List<?>
或List<? extends E>
这样的通配符类型进行类似add
的操作。
使用泛型方法可以避免这个问题(重申通配符类型并不是泛型方法),使用无限制通配符类型可以取代其他需要表示包含某一种对象类型的泛型类型的情况而不是使用原生态类型List
。