Java泛型详解
一、泛型概述
1.什么是泛型?为什么要试用泛型?
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参列表,普通方法的形参列表中,每个形参的数据类型的确定的,而变量是一个参数。在调用普通方法时需要传入对应形参数据类型的变量(实参),若传入的实参与形参定义的数据类型不匹配,则会报错。
那参数化类型是什么?以方法的定义为例,在方法定义时,将方法签名中的形参的数据类型也设置为参数(也可称之为类型参数),在调用该方法时在从外部传入一个具体的数据类型和变量。
泛型的本质是为了将类型参数化。也就是说,在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型。而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口以及泛型方法。
2.泛型的使用场景
在
ArrayList
集合中,可以放入所有类型的对象,假设现在需要一个只存储了String
类型对象的ArrayList
集合。
代码如下:
@Test
public void test() {
ArrayList list = new ArrayList();
list.add("aaa");
list.add("bbb");
list.add("ccc");
for (int i = 0; i < list.size(); i ++) {
System.out.println((String)list.get(i));
}
}
- 上面代码没有任何问题,在遍历
ArrayList
集合时,只需将Object
对象进行向下转型成String
类型即可得到String
类型对象。
但如果在添加
String
对象时,不小心添加了一个Integer
对象,会发生什么?
看下面代码:
@Test
public void test() {
ArrayList list = new ArrayList();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add(111);
for (int i = 0; i < list.size(); i ++) {
System.out.println((String)list.get(i));
}
}
输出结果:
aaa
bbb
ccc
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
- 上述代码在编译时没有报错,但在运行时却抛出了一个
ClassCastException
异常,其原因是Integer
对象不能强转为String
类型。
那如何可以避免上述异常的出现?即我们希望当我们向集合中添加了不符合类型要求的对象时,编译器能直接给我们报错,而不是在程序运行后才产生异常。这个时候便可以使用泛型了。
使用泛型后代码如下:
@Test
public void test() {
ArrayList<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add(111); // 在编译阶段,编译器会报错
for (int i = 0; i < list.size(); i ++) {
System.out.println((String)list.get(i));
}
}
<String>
是一个泛型,其限制了ArrayList
集合中存放对象的数据类型只能是String
,当添加一个非String
对象时,编译器会直接报错。这样,我们便解决了上面产生的ClassCastException
异常的问题(这里体现了泛型的类型安全检测机制)。
3.泛型概述小结
- 与使用
Object
对象替代一切引用数据类型对象这样简单粗暴的方式相比,泛型使得数据类型的类别可以像参数一样由外部传递进来。它提供了一种扩展能力,更符合面向对象开发的软件编程宗旨。 - 当具体的数据类型确定后,泛型又提供了一种类型安全检测机制,只有数据类型相匹配的变量才能正常的赋值,否则编译器就不通过。所以说,泛型一定程度上提高了软件的安全性,房子出现低级的失误。
- 泛型提高了程序代码的可读性。在定义泛型阶段(类、接口、方法)或者对象实例化阶段,由于
<类型参数>
需要在代码中显式地编写,所以程序员能够快速猜测出代码说要操作的数据类型,提高了代码的可读性。
泛型有三种使用方式,分别为:泛型类、泛型接口和泛型方法,下面将正式介绍泛型的相关知识。
二、泛型类
1.泛型类的定义
(1)类型参数用于类的定义中,则该类被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:
List
、Set
、Map
等。
泛型类的基本语法如下:
Class 类名称<泛型标识> {
private 泛型标识 /*(成员变量类型)*/ 变量名;
......
}
-
尖括号
<>
中的泛型标识被称作是类型参数,用于指代任何数据类型。 -
泛型标识是任意设置的(如果你想可以设置为
Hello
都行),Java常见的泛型标识以及其代表含义如下:T: 代表一般的任何类。 E: 代表Element元素的意思,或者Exception异常的意思。 K: 代表Key的意思。 V: 代表Value的意思,通常与K一起配合使用 S: 代表Subtype的意思,文章后面部分会讲解示意。
举例如下:
public class Generic<T> { // key这个成员变量的数据类型变成了T,T的类型由外部传入 private T key; // 泛型构造方法形参key的类型也为T,T的类型由外部传入 public Generic(T key) { this.key = key; } // 泛型方法getKey的返回值类型为T,T的类型由外部指定 public T getKey() { return key; } }
-
在泛型类中,类型参数定义的位置有三处,分别为:
- 非静态的成员属性类型
- 非静态方法的形参类型(包括非静态成员方法和构造器)
- 非静态的成员方法的返回值类型
(2)泛型类中的静态方法和静态变量不可以试用泛型类所声明的类型参数
代码如下:
public class Test<T> {
public static T one; // 编译错误
public static T show(T one) { // 编译错误
return null;
}
}
- 泛型类中类型参数的确定是在创建泛型类对象的时候(例如
ArrayList<Integer>
)。 - 静态变量和静态方法在类加载时已经初始化,直接使用类名调用。在泛型类的类型参数未确定时,静态成员有可能被调用,因此泛型类的类型参数是不能在静态成员中使用的。
(3)静态泛型方法中可以使用自身方法签名中新定义的类型参数(即泛型方法,后面会说到),而不能使用泛型类中定义的类型参数。
代码如下:
public class Test2<T> {
// 泛型类定义的类型参数T不能在静态方法中使用
public static<E> E show(E one) { // 这是正确的,因为E是在静态方法签名中新定义的类型参数
return null;
}
}
(4)泛型类不只接受一个类型参数,它还可以接受多个类型参数。
代码如下:
public class MultiType<E, T> {
E value1;
T value2;
public E getValue1() {
return value1;
}
public T getValue2() {
return value2;
}
}
2.泛型类的使用
在创建泛型类的对象时,必须指定类型参数T的具体数据类型,即尖括号<>
中传入的什么数据类型,T便会被替换成对应的类型。如果<>
中什么都不传入,则默认是<Object>
。
假设有个泛型类如下:
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
当创建一个Generic<T>
类对象时,会向尖括号<>
中传入具体的数据类型。
代码如下:
@Test
public void test() {
Generic<String> generic = new Generic<>();// 传入 String 类型
// <>中什么都不传入,等价于Generic<Object> generic = new Generic<>();
Generic generic = new Generic();
}
扩展如下:
public class Generic {
private String key;
public Generic(String key) {
this.key = key;
}
public String getKey() {
return key;
}
}
- 可以发现,泛型类中的类型参数
T
被<>
中的String
类型全部替换了。 - 使用泛型的上述特性便可以在集合中限制添加对象的数据类型,若集合中添加的对象与指定的泛型数据类型不一致,则编译器会直接报错,这也是泛型的类型安全检测机制的实现原理。
三、泛型接口
泛型接口和泛型类的定义差不多,基本语法如下:
public interface 接口名<类型参数> {
...
}
举例如下:
public interface Inter<T> {
public abstract void show(T t) ;
}
重要!泛型接口中的类型参数在该接口被继承或被实现时确定。
解释如下:
(1)定义一个泛型接口如下:
- 注意:在泛型接口中,静态成员也不能使用泛型接口定义的类型参数。
interface IUsb<U, R> {
int n = 10;
U name; // 报错!接口中的属性默认是静态的,因此不能使用类型参数声明
R get(U u); // 普通方法中,可以使用类型参数
abstract void hi(R r); // 抽象方法中,可以使用类型参数
// 在jdk8中,可以在接口中使用默认方法, 默认方法可以使用泛型接口的类型参数
default R method(U u) {
return null;
}
}
(2)定义了一个接口
IA
继承了泛型接口IUsb
,在接口IA
定义时必须确定泛型接口IUsb
中的类型参数。
代码如下:
// 在继承泛型接口时,必须确定泛型接口的类型参数
interface IA extends IUsb<String, Double> {
...
}
// 当去实现IA接口时,因为IA在继承IUsu接口时,指定了类型参数U为String,R为Double
// 所以在实现IUsb接口的方法时,使用String替换U,用Double替换R
class AA implements IA {
@Override
public Double get(String s) {
return null;
}
@Override
public void hi(Double d) {
...
}
}
(3)定义一个类
BB
实现了泛型接口IUsb
,在类BB
定义时需要确定泛型接口IUsb
中的类型参数。
代码如下:
// 实现接口时,需要指定泛型接口的类型参数
// 给U指定Integer,给R指定了Float
// 所以,当我们实现IUsb方法时,会使用Integer替换U, 使用Float替换R
class BB implements IUsb<Integer, Float> {
@Override
public Float get(Integer integer) {
return null;
}
@Override
public void hi(Float afloat) {
...
}
}
(4)定义一个类
CC
实现了泛型接口IUsb
时,若是没有确定泛型接口IUsb
中的类型参数,则默认为Object
。
代码如下:
// 实现泛型接口时没有确定类型参数,则默认为Object
// 建议直接写成IUsb<Object, Object>
class CC implements IUsb {//等价class CC implements IUsb<Object, Object>
@Override
public Object get(Object o) {
return null;
}
@Override
public void hi(Object o) {
...
}
}
定义一个类
DD
实现了泛型接口IUsb
时,若是没有确定泛型接口IUsb
中的类型参数,也可以将DD
类也定义为泛型类,其声明的类型参数必须要和接口IUsb
中的类型参数相同。
代码如下:
// DD类定义为泛型类,则不需要确定接口的类型参数
// 但DD类定义的类型参数要和接口中类型参数的一致
class DD<U, R> implements IUsb<U, R> {
...
}
四、泛型方法
1.泛型方法的定义
当在一个方法签名中的返回值前面声明了一个
<T>
时,该方法就被声明为一个泛型方法。<T>
表明该方法声明了一个类型参数T
,并且这个类型参数T
只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数。
基本语法如下:
public <类型参数> 返回类型 方法名(类型参数 变量名) {
...
}
(1)只有在方法签名中声明了
<T>
的方法才是泛型方法,仅使用了泛型类定义的类型参数的方法并不是泛型方法。
举例如下:
public class Test<U> {
// 该方法只是使用了泛型类定义的类型参数,不是泛型方法
public void testMethod(U u){
System.out.println(u);
}
// <T>真正声明了下面的方法是一个泛型方法
public <T> T testMethod1(T t){
return t;
}
}
(2)泛型方法中可以同时声明多个类型参数。
举例如下:
public class TestMethod<U> {
public <T> U testMethod(T t, U u) {
return u;
}
}
特别注意的是:泛型类中定义的类型参数和泛型方法中定义的类型参数是相互独立的,它们一点关系都没有。
举例如下:
public class Test<T> {
public void testMethod(T t) {
System.out.println(t);
}
public <T> T testMethod1(T t) {
return t;
}
}
上面代码中,
Test<T>
是泛型类,testMethod()
是泛型类中的普通方法,其使用的类型参数是于泛型类中定义的类型参数。而
testMethod1()
是一个泛型方法,他使用的类型参数是于方法签名中声明的类型参数。虽然泛型中定义的类型参数标识和泛型方法中定义的类型参数标识都为<T>
,但它们彼此之间是相互独立的。也就是说,泛型方法始终以自己声明的类型参数为准。
注意事项
<T>
表明该方法声明了一个类型参数T
,并且这个类型参数T
只能在该方法中使用。- 为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。
- 与泛型类的类型参数定义一样,此处泛型方法中的
T
可以写成“任意标识”,常见的如T
、E
、K
、V
等形式的参数常用于表示泛型。
补充:将静态方法声明为泛型方法
前面在泛型类的定义中提到,在静态成员中不能使用泛型类定义的类型参数,但我们可以将静态成员方法定义为一个泛型方法。
代码如下:
public class Test2<T> {
// 泛型类定义的类型参数T不能在静态方法中使用
// 但可以将静态方法声明为泛型方法,方法中便可以使用其声明的类型参数了
public static <E> E show(E one) {
return null;
}
}
2.泛型方法的使用
泛型类,在创建类的对象时确定类型参数的具体类型。
泛型方法,在调用方法时确定类型参数的具体类型。
泛型方法签名中声明的类型参数只能在该方法里使用,而泛型接口、泛型类中声明的类型参数则可以在整个接口、类中使用。
当调用泛型方法时,根据外部传入的实际对象的数据类型,编译器就可以判断出类型参数
T
所代表的具体数据类型。
举例如下:
public class Demo {
public static void main(String args[]) {
GenericMethod d = new GenericMethod(); // 创建GenericMethod对象
String str = d.fun("汤姆"); // 给GenericMethod中的泛型方法传递字符串
int i = d.fun(30); // 给GenericMethod中的泛型方法传递数字,自动装箱
System.out.println(str); // 输出 汤姆
System.out.println(i); // 输出 30
GenericMethod.show("Lin");// 输出: 静态泛型方法Lin
}
}
class GenericMethod {
// 普通的泛型方法
public <T> T fun(T t) { // 可以接收任意类型的数据
return t;
}
// 静态的泛型方法
public static <E> void show(E one){
System.out.println("静态泛型方法 " + one);
}
}
不难发现,当调用泛型方法时,根据传入的实际对象,编译器会判断出形参T
所代表的的具体数据类型。
3.泛型方法中的类型判断
在调用泛型方法的时候,可以显式地指定类型参数,也可以不指定。
- 当泛型方法的形参列表中有多个类型参数时,在不指定类型参数的情况下,方法中声明的类型参数为泛型方法中几种类型参数的共同父类的最小级,直到
Object
。- 在指定了类型参数的时候,传入泛型方法中的实参的数据类型必须为指定数据类型或者其子类。
举例如下:
public class Test {
// 这是一个简单的泛型方法
public static <T> T add(T x, T y) {
return y;
}
public static void main(String[] args) {
// 一、不显式地指定类型参数
//(1)传入的两个实参都是Integer,所以泛型方法中的<T> == <Integer>
int i = Test.add(1, 2);
//(2)传入的两个实参一个是Integer,另一个是Float,
// 所以<T>取共同父类的最小级,<T> == <Number>
Number f = Test.add(1, 1.2);
// 传入的两个实参一个是Integer,另一个是String,
// 所以<T>取共同父类的最小级,<T> == <Object>
Object o = Test.add(1, "asd");
// 二、显式地指定类型参数
//(1)指定了<T> = <Integer>,所以传入的实参只能为Integer对象
int a = Test.<Integer>add(1, 2);
//(2)指定了<T> = <Integer>,所以不能传入Float对象
int b = Test.<Integer>add(1, 2.2); // 编译错误
//(3)指定<T> = <Number>,所以可以传入Number对象
// Integer和Float都是Number的子类,因此可以传入两者的对象
Number c = Test.<Number>add(1, 2.2);
}
}
五、类型擦除
1.什么是类型擦除
泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作。
换言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。也就是说,成功编译之后的class文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段。
看一个例子,加入给ArrayList
集合传入两种不同的数据类型,并比较它们的类信息。
代码如下:
public class GenericType {
public static void main(String[] args) {
ArrayList<String> arrayString = new ArrayList<String>();
ArrayList<Integer> arrayInteger = new ArrayList<Integer>();
System.out.println(arrayString.getClass() == arrayInteger.getClass()); // true
}
}
在这个例子中,我们定义了两个ArrayList
集合,不过一个是ArrayList<String>
,只能存储字符串。一个是ArrayList<Integer>
,只能存储整型对象。我们通过arrayString
对象和arrayInteger
对象的getClass()
方法获取它们的类信息并比较,发现结果为true
。
明明我们在<>
中传入了两种不同的数据类型,按照上文所说的,它们的类型参数T
不是应该被替换成我们传入的数据类型了吗,那为什么它们的类信息还是相同呢?
这是因为,在编译期间,所有的泛型信息都会被擦除,ArrayList<Integer>
和ArrayList<String>
类型,在编译后都会变成ArrayList<Object>
类型。
再看一个例子,假设定义一个泛型类如下:
public class Calculate<T> {
private T num;
}
在该泛型类中定义了一个属性num
,该属性的数据类型是泛型类声明的类型参数T
,这个T
具体是什么类型,我们也不知道,它只与外部传入的数据类型有关。
将这个泛型反编译后代码如下:
public class Calculate {
public Caculate() {} // 默认构造器,不用管
private Object num; // T被替换为Object类型
}
可以发现编译器擦除了Calculate
类后面的泛型标识<T>
,并且将num
的数据类型替换为Object
类型,而替换了T
的数据类型我们称之为原始数据类型。
那么是不是所有的类型参数被擦除之后都以Object
类进行替换呢?
答案是否定的,大部分情况下,类型参数T被擦除后都会以Object
类进行替换,而有一种情况则不是,那就是使用到了extends
和super
语法的有界类型参数(即泛型通配符,后面会详细解释)。
再看一个例子,假设定义一个泛型类如下:
public class Calculate<T extends Number> {
private T num;
}
将其反编译:
public class Calculate {
public Calculate() {} // 默认构造器,不用管
private Number num;
}
可以发现,使用到了extends
语法的类型参数T
被擦除之后会替换为Number
而不再是Object
。
extends
和super
是一个限定类型参数边界的语法,extends
限定T
只能是Number
或者是Number
的子类。也就是时候,在创建Calculate
类对象的时候,尖括号<>
中只能传入Number
或者Number
的子类的数据类型,所以在创建Calculate
类对象时无论传入什么数据类型,Number
都是其父类,于是可以使用Number
类作为T
的原始数据类型,进行类型擦除并替换。(这一部分涉及到了泛型通配符,在下面还会具体介绍)
2.类型擦除的原理
假如我们定义了一个ArrayList<Integer>
泛型集合,若向该集合中插入String
类型的对象,不需要运行程序,编译器就会直接报错。这里可能就会产生疑问:
- 不是说泛型信息在编译的时候就会被擦除掉吗?那既然泛型信息被擦除了,如何保证我们在集合中只添加指定的数据类型的对象呢?
- 换言之,我们虽然定义了
ArrayList<Integer>
泛型集合,但其泛型信息最终被擦除后就变成了ArrayList<Object>
集合,那为什么不允许向其中插入String
对象呢?
Java是如何解决这个问题的?
其实在创建一个泛型类的对象时,Java编译器是先检查代码中传入<T>
的数据类型并记录下来,然后再对代码进行编译,编译的同时进行类型擦除;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换。
可以把泛型的类型安全检查机制和类型擦除想象成演唱会的验票机制,以ArrayList<Integer>
泛型集合为例:
- 当我们在创建一个
ArrayList<Integer>
泛型集合的时候,ArrayList
可以看作是演唱会仓管,而<T>
就是场馆的验票系统,Integer
是验票系统设置的门票类型。 - 当验票系统设置好为
<Integer>
后,只有持有Integer
门票的人才可以通过验票系统,进入演唱会场馆(集合)中;若是未持有Integer
门票的人想进场,则验票系统会发出警告(编译器报错)。 - 在通过验票系统时,门票会被收掉(类型擦除),但场馆后台(JVM)会记录下观众信息(泛型信息)。
- 进场后的观众变成了没有门票的普通人(原始数据类型)。但是,在需要查看观众的信息时(操作对象),场馆后台可以找到记录的观众信息(编译器会自动将对象进行类型转换)。
举例如下:
public class GenericType {
public static void main(String[] args) {
ArrayList<Integer> arrayInteger = new ArrayList<Integer>(); // 设置验票系统
arrayInteger.add(111); // 观众进场,验票系统验票,门票会被收走(类型擦除)
Integer n = arrayInteger.get(0); // 获取观众信息,编译器会进行强制类型转换
System.out.println(n);
}
}
擦除ArrayList<Integer>
的泛型信息后,get()
方法的返回值将返回Object
类型,但编译器会自动插入Integer
的强制类型转换。也就是说,编译器把get()
方法调用翻译为两条字节码指令:
- 对原始方法
get()
的调用,返回的是Object
类型; - 将返回的
Object
类型强制转换为Integer
类型。
代码如下:
Integer n = arrayInteger.get(0); // 这条代码底层如下:
//(1)get()方法的返回值返回的是Object类型
Object object = arrayInteger.get(0);
//(2)编译器自动插入Integer的强制类型转换
Integer n = (Integer) object;
3.类型擦除小结
- 泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数
T
会被统一替换为其原始类型(默认是Object
类,若有extends
或者super
则另外分析)。 - 在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换(从原始类型转换为未擦除前的数据类型)。
六、泛型通配符
1.泛型的继承
在介绍泛型通配符之前,先提出一个问题,在Java的多态中,我们知道可以将一个子类对象赋值给其父类的引用,这也叫向上转型。
举例如下:
public class GenericType {
public static void main(String[] args) {
List list = new ArrayList();
}
}
上面的代码很好地体现了Java的多态特性。
在Java标准库中的集合ArrayList<T>
类实现了List<T>
接口,其源码大致如下:
public class ArrayList<T> implements List<T> {...}
那现在我们思考一个问题:在ArrayList<T>
泛型集合中,当传入<T>
中的数据类型相同时,是否还能将一个ArrayList<T>
对象赋值给其父类的引用List<T>
。
代码如下:
public class GenericType {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
}
}
上面的代码没有问题,即ArrayList<T>
对象可以向上转型为List<T>
,但两者传入<T>
中的数据类型必须相同。
继续思考一个问题:已知Integer
类是Number
类的子类,那如果ArrayList<>
泛型集合中,在<>之间使用向上转型,也就是将ArrayList<Integer>
对象赋值给List<Number>
的引用,是否被允许呢?
举例如下:
public class GenericType {
public static void main(String[] args) {
List<Number> list01 = new ArrayList<Integer>(); // 编译错误
ArrayList<Number> list02 = new ArrayList<Integer>(); // 编译错误
}
}
上面代码会报错,我们发现并不能把ArrayList<Integer>
对象赋值给List<Number>
的引用,甚至不能把ArrayList<Integer>
对象赋值给ArrayList<Number>
的引用。这也说明了在一般泛型中,不能向上转型。
这是为什么?
如果我们假设ArrayList<Integer>
可以向上转型为ArrayList<Number>
。
观察下面代码:
public class GenericType {
public static void main(String[] args) {
// 创建一个ArrayList<Integer>集合
ArrayList<Integer> integerList = new ArrayList<>();
// 添加一个Integer对象
integerList.add(new Integer(123));
// “向上转型”为ArrayList<Number>
ArrayList<Number> numberList = integerList;
// 添加一个Float对象,Float也是Number的子类,编译器不报错
numberList.add(new Float(12.34));
// 从ArrayList<Integer>集合中获取索引为1的元素(即添加的Float对象):
Integer n = integerList.get(1); // ClassCastException,运行出错
}
}
- 当我们把一个
ArrayList<Integer>
向上转型为ArrayList<Number>
类型后,这个ArrayList<Number>
集合就可以接收Float
对象了,因为Float
类是Number
类的子类。 - 但是,
ArrayList<Number>
实际上和ArrayList<Integer>
是同一个集合,而在泛型的定义中,ArrayList<Integer>
集合时不可以接收Float
对象的。这是因为,在使用get()
方法获取集合元素的时候,编译器会自动将Float
对象强转为Integer
对象,而这会产生ClassCastException
异常。
正因如此,编译器为了避免发生这种错误,根本就不允许把ArrayList<Integer>
对象向上转型为ArrayList<Number>
;换言之,ArrayList<Integer>
和ArrayList<Number>
两者之间没有继承关系。
2.泛型通配符的引用
我们上面讲到了泛型的继承关系,ArrayList<Integer>
不是ArrayList<Number>
的子类。
先看一个问题:假设我们定义了一个Pair<T>
类。如下:
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}
然后,我们针对Pair<Number>
类型写了一个静态方法,它接收的参数类型是Pair<Number>
。
代码如下:
public class PairHelper {
static int addPair(Pair<Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
}
在测试类中创建一个Pair<Number>
对象,并调用addPair()
方法。
代码如下:
public class Main {
public static void main(String[] args) {
Pair<Number> pair = new Pair<>(1, 2);
int sum = PairHelper.addPair(pair);
}
}
上面的代码正常编译运行。但我们发现,在实际创建Pair<Number>
对象的时候,我们传入的实参(1, 2)
实际上是Integer
类型;那我们是否可以直接创建一个Pair<Integer>
对象,并将其传给add()
方法呢?
代码如下:
public class Main {
public static void main(String[] args) {
Pair<Integer> pairInteger = new Pair<>(123, 456);
int sum = PairHelper.addPair(pairInteger);
}
}
上述代码编译器会直接报错,原因是Pair<Integer>
并不是Pair<Number>
的子类,而addPair()
方法的形参数据类型为Pair<Number>
。因此,Pair<Integer>
对象不能传给addPair()
方法。
那有没有办法使得addPair()
方法可以接收Pair<Integer>
对象?总不能重新定义一个新的addPair()
方法来处理Pair<Integer>
对象吧,这显然与Java中的多态理念相违背。
因此我们需要一个在逻辑上可以表示为Pair<Integer>
和Pair<Number>
这两者的父类引用类型,由此,泛型通配符变应运而生。
3.什么是泛型通配符
在现实编码中,确实有这样的需求,希望泛型能够处理某一类型范围内的类型参数,比如某个泛型类和它的子类,为此Java引入了泛型通配符****这个概念。
泛型通配符有3种形式:
<?>
:被称作无限定的通配符。<? extends T>
:被称作有上界的通配符。<? super T>
:被称作有下界的通配符。
在引入泛型通配符之后,我们便得到了一个在逻辑上可以表示为某一类型参数范围的父类引用类型。举例来说,泛型通配符可以表示Pair<Integer>
和Pair<Number>
两者的父类引用类型。
接下来将分别介绍3种形式的泛型通配符。
4.上界通配符<? extends T>
4.1.<? extends T>
的定义
上界通配符
<? extends T>
:T
代表了类型参数的上界,<? extends T>
表示类型参数的范围是T
和T
的子类。需要注意的是:<? extends T>
也是一个数据类型实参,它和Number
、String
、Integer
一样都是一种实际的数据类型。
(1)在泛型的继承中我们说到,
ArrayList<Integer>
和ArrayList<Number>
之间不存在继承关系。而引入上界通配符的概念后,我们便可以在逻辑上将ArrayList<? extends Number>
看做是ArrayList<Integer>
的父类,但实质上它们之间没有继承关系。
举例如下:
public class GenericType {
public static void main(String[] args) {
ArrayList<Number> list01 = new ArrayList<Integer>(); // 编译错误
ArrayList<? extends Number> list02 = new ArrayList<Integer>(); // 编译正确
}
}
逻辑上可以将ArrayList<? extends Number>
看作是ArrayList<Integer>
的父类,因此,在使用了上界通配符<? extends Number>
后,便可以将ArrayList<Integer>
对象向上转型了。
(2)
ArrayList<? extends Number>
可以代表ArrayList<Integer>
、ArrayList<Float>
、...、ArrayList<Number>
中的某一个集合,但我们不能指定ArrayList<? extends Number>
的数据类型(这里有点难理解)。
举个例子:
public class GenericType {
public static void main(String[] args) {
ArrayList<? extends Number> list = new ArrayList<>();
list.add(new Integer(1)); // 编译错误
list.add(new Float(1.0)); // 编译错误
}
}
可以这样理解,ArrayList<? extends Number>
集合表示了:我这个集合可能是ArrayList<Integer>
集合,也可能是ArrayList<Float>
集合,...,还可能是ArrayList<Number>
集合;但到底是哪一个集合,不能确定;程序员也不能指定。
所以,在上面的代码中,创建了一个ArrayList<? extends Number>
集合list
,但我们并不能往list
中添加Integer
、Float
等对象,这也说明了list
集合并不是某个确定了数据类型的集合。
思考:那既然
ArrayList<? extends Number>
可以代表ArrayList<Integer>
或ArrayList<Float>
,那为什么不能向其中加入Integer
、Float
等对象呢?
其原因是ArrayList<? extends Number>
表示的是一个未知类型的ArrayList
集合,它可以代表ArrayList<Integer>
或ArrayList<Float>
...等集合,但却不能确定它到底是ArrayList<Integer>
还是ArrayList<Float>
集合。
因此,泛型的特性决定了不能往ArrayList<? extends Number>
集合中加入Integer
、Float
等对象,以防止在获取ArrayList<? extends Number>
集合中元素的时候,产生ClassCastException
异常。
那为什么还需要引入上界通配符的概念?
答:是为了拓展方法形参中类型参数的范围。
在泛型通配符的引入部分,我们提出了一个问题,有没有办法使得
addPair(Pair<Number> p)
方法接收Pair<Integer>
对象?而在有了上界通配符的概念后,这个问题便有了解决办法,就是将addPair()
方法改写。
代码如下:
// 改写前
public class PairHelper {
static int addPair(Pair<Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
}
// 改写后
public class PairHelper {
static int addPair(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
}
- 改写
addPair()
方法,用<? extends Number>
替换了<Number>
,由于Pair<Integer>
可以向上转型为Pair<? extends Number>
,所以调用addPair()
方法时,我们便可以传入Pair<Integer>
对象了。 - 除了可以传入
Pair<Integer>
对象,我们还可以传入Pair<Douoble>
对象,Pair<BigDecimal>
对象等等,因为Double
类和BigDecimal
类也都是Number
的子类。
4.2<? extends T>
的用法
上面说到,我们无法确定ArrayList<? extends Number>
具体是什么数据类型的集合,因此其add()
方法会受限(即不能往集合中添加任何数据类型的对象);但是可以往集合中添加null
,因为null
表示任何类型。
我们可以调用get()
方法从集合中获取元素,并赋值给集合中的最高父类Number
(即<? extends T>
的上界)。
上界通配符<? extends T>
的正确用法:
public class Test {
public static void main(String[] args) {
// 创建一个ArrayList<Integer>集合
ArrayList<Integer> integerList = new ArrayList<>();
integerList.add(1);
integerList.add(2);
// 将ArrayList<Integer>传入printIntVal()方法
printIntVal(integerList);
// 创建一个ArrayList<Float>集合
ArrayList<Float> floatList = new ArrayList<>();
floatList.add((float) 1.0);
floatList.add((float) 2.0);
// 将ArrayList<Float>传入printIntVal()方法
printIntVal(floatList);
}
public static void printIntVal(ArrayList<? extends Number> list) {
// 遍历传入的集合,并输出集合中的元素
for (Number number : list) {
System.out.print(number.intValue() + " ");
}
System.out.println();
}
}
输出如下:
1 2
1 2
在printIntVal()
方法中,其形参为ArrayList<? extends Number>
,因此,可以给该方法传入ArrayList<Integer>
、ArrayList<Float>
等集合。
需要注意的是:在printIntVal()
方法内部,必须要将传入集合中的元素赋值给Number对象,而不能赋值给某个子类对象。因为根据ArrayList<? extends Number>
的特性,并不能确定传入集合的数据类型(即不能确定传入的是ArrayList<Integer>
还是ArrayList<Float>
)。
假设在printIntVal()
方法中存在下面代码:
Integer intNum = (Integer) number;
若是传入集合为ArrayList<Float>
,则必然会产生ClassCastException
异常。
上界通配符<? extends T>
的错误用法:
public class Test {
public static void main(String[] args) {
ArrayList<? extends Number> list = new ArrayList();
list.add(null);// 编译正确
list.add(new Integer(1));// 编译错误
list.add(new Float(1.0));// 编译错误
}
public static void fillNumList(ArrayList<? extends Number> list) {
list.add(new Integer(0));//编译错误
list.add(new Float(1.0));//编译错误
list.set(0, new Integer(2));// 编译错误
list.set(0, null);// 编译成功,但不建议这样使用
}
}
在ArrayList<? extends Number>
集合中,不能添加任何数据类型的对象,只能添加空值null
,因为null可以表示任何数据类型。
4.3.<? extends T>
小结
一句话总结:使用
extends
通配符表示可以读,不能写。
5.下界通配符<? super T>
5.1.<? super T>
的定义
下界通配符
<? super T>
:T代表了类型参数的下界,<? super T>
表示类型参数的范围是T
和T
的超类,直至Object
。需要注意的是:<? super T>
也是一个数据类型实参,它和Number
、String
、Integer
一样都是一种实际的数据类型。
ArrayList<? super Integer>
在逻辑上表示为Integer
类以及Integer
类的所有父类,它可以代表ArrayList<Integer>
、ArrayList<Number>
、ArrayList<Object>
中的某一个集合,但实质上它们之间没有继承关系。
举个例子:
public class GenericType {
public static void main(String[] args) {
ArrayList<Integer> list01 = new ArrayList<Number>(); // 编译错误
ArrayList<? super Integer> list02 = new ArrayList<Number>();
// 编译正确
}
}
逻辑上可以将ArrayList<? super Integer>
看做是ArrayList<Number>
的父类,因此,在使用了下界通配符<? super Integer>
后,便可以将ArrayList<Number>
对象向上转型了。
ArrayList<? super Integer>
只能表示指定类型参数范围中的某一个集合,但我们不能指定ArrayList<? super Integer>
的数据类型。(这里有点难理解)
看一个例子:
public class GenericType {
public static void main(String[] args) {
ArrayList<? super Number> list = new ArrayList<>();
list.add(new Integer(1)); // 编译正确
list.add(new Float(1.0)); // 编译正确
// Object是Number的父类
list.add(new Object()); // 编译错误
}
}
这里奇怪的地方出现了,为什么和ArrayList<? extends Number>
集合不同,ArrayList<? super Number>
集合中可以添加Number
类及其子类的对象呢?
其原因是,ArrayList<? super Number>
的下界是ArrayList<Number>
。因此,我们可以确定Number
类及其子类的对象自然可以加入ArrayList<? super Number>
集合中;而Number
类的父类对象就不能加入ArrayList<? super Number>
集合中了,因为不能确定ArrayList<? super Number>
集合的数据类型。
5.2.<? super T>
的用法
下界通配符<? super T>
的正确用法:
public class Test {
public static void main(String[] args) {
// 创建一个ArrayList<? super Number>集合
ArrayList<Number> list = new ArrayList();
// 往集合中添加Number类及其子类对象
list.add(new Integer(1));
list.add(new Float(1.1));
// 调用fillNumList()方法,传入ArrayList<Number>集合
fillNumList(list);
System.out.println(list);
}
public static void fillNumList(ArrayList<? super Number> list) {
list.add(new Integer(0));
list.add(new Float(1.0));
}
}
输出如下:
[1, 1.1, 0, 1.0]
与带有上界通配符的集合ArrayList<? extends T>
的用法不同,带有下界通配符的集合ArrayList<? super Number>
中可以添加Number
类及其子类的对象;ArrayList<? super Number>
的下界就是ArrayList<Number>
集合,因此,其中必然可以添加Number
类及其子类的对象;但不能添加Number
类的父类对象(不包括Number
类)。
下界通配符<? super T>
的错误用法:
public class Test {
public static void main(String[] args) {
// 创建一个ArrayList<Integer>集合
ArrayList<Integer> list = new ArrayList<>();
list.add(new Integer(1));
// 调用fillNumList()方法,传入ArrayList<Integer>集合
fillNumList(list); // 编译错误
}
public static void fillNumList(ArrayList<? super Number> list) {
list.add(new Integer(0)); // 编译正确
list.add(new Float(1.0)); // 编译正确
// 遍历传入集合中的元素,并赋值给Number对象;会编译错误
for (Number number : list) {
System.out.print(number.intValue() + " ");
System.out.println();
}
// 遍历传入集合中的元素,并赋值给Object对象;可以正确编译
// 但只能调用Object类的方法,不建议这样使用
for (Object obj : list) {
System.out.println(obj);使用
}
}
}
注意,ArrayList<? super Number>
代表了ArrayList<Number>
、ArrayList<Object>
中的某一个集合,而ArrayList<Integer>
并不属于ArrayList<? super Number>
限定的范围,因此,不能往fillList()
方法中传入ArrayList<Integer>
集合。
并且,不能将传入集合的元素赋值给Number
对象,因为传入的可能是ArrayList<Object>
集合,向下转型可能会产生ClassCastException
异常。
不过,可以将传入集合的元素赋值给Object
对象,因为Object
是所有类的父类,不会产生ClassCastException
异常,但这样的话便只能调用Object
类的方法了,不建议这样使用。
5.3.<? super T>
小结
一句话总结:使用super通配符表示可以写,不能读。
6.无限定通配符<?>
我们已经讨论了<? extends T>
和<? super T>
作为方法参数的作用。实际上,Java的泛型还允许使用无限定通配符<?>
,即只定义一个?
符号。
无界通配符
<?>
:?
代表了任何一种数据类型,能代表任何一种数据类型的只有null
。需要注意的是:<?>
也是一个数据类型实参,它和Number
、String
、Integer
一样都是一种实际的数据类型。
注意:Object
本身也算是一种数据类型,但却不能代表任何一种数据类型,所以ArrayList<Object>
和ArrayList<?>
的含义是不同的,前者类型是Object
,也就是继承树的最高父类,而后者的类型完全是未知的;ArrayList<?>
是ArrayList<Object>
逻辑上的父类。
(1)
ArrayList<?>
在逻辑上表示为所有数据类型的父类,它可以代表ArrayList<Integer>
、ArrayList<Number>
、ArrayList<Object>
中的某一个集合,但实质上它们之间没有继承关系。
举例如下:
public class GenericType {
public static void main(String[] args) {
ArrayList<Integer> list01 = new ArrayList<>(123, 456);
ArrayList<?> list02 = list01; // 安全地向上转型
}
}
上述代码是可以正常编译运行的,因为ArrayList<?>
在逻辑上是ArrayList<Integer>
的父类,可以安全地向上转型。
(2)
ArrayList<?>
既没有上界也没有下界,因此,它可以代表所有数据类型的某一个集合,但我们不能指定ArrayList<?>
的数据类型。
举例如下:
public class GenericType {
public static void main(String[] args) {
ArrayList<?> list = new ArrayList<>();
list.add(null); // 编译正确
Object obj = list.get(0); // 编译正确
list.add(new Integer(1)); // 编译错误
Integer num = list.get(0); // 编译错误
}
}
ArrayList<?>
集合的数据类型是不确定的,因此我们只能往集合中添加null
;而我们从ArrayList<?>
集合中取出的元素,也只能赋值给Object
对象,不然会产生ClassCastException
异常(原因可以结合上界和下界通配符理解)。
(3)大多数情况下,可以用类型参数
<T>
代替<?>
通配符
举例如下:
static <?> void isNull(ArrayList<?> list) {
...
}
// 替换如下:
static <T> void isNull(ArrayList<T> list) {
...
}
7.<? extends T>
与<? super T>
对比
(1)对于
<? extends 类型>
,编译器将只允许读操作,不允许写操作。即只可以取值,不可以设值。(2)对于
<? super 类型>
,编译器将只允许写操作,不允许读操作。即只可以设值(比如set
操作),不可以取值(比如get
操作)。
以上两点都是针对于源码里涉及到了类型参数的方法而言的。
比如对于List
而言,不允许的写操作有add
方法,因为它的方法签名是boolean add(E e);
,此时这个形参E就变成了一个涉及了通配符的类型参数;
而不允许的读操作有get
方法,因为它的方法签名是E get(int index);
,此时这个返回值E就变成了一个涉及了通配符的类型参数。
作为方法形参,
<? extends T>
类型和<? super T>
类型的区别在于:
<? extends T>
允许调用读方法T get()
获取T
的引用,但不允许调用写方法set(T)
传入T
的引用(传入null
除外)。<? super T>
允许调用写方法set(T)
传入T
的引用,但不允许调用读方法T get()
获取T的引用(获取Object
除外)。
先记住上面的结论,我们来看Java标准库的Collections
类定义的copy()
方法。
(1)
copy()
方法的作用是把一个List
中的每个元素依次添加到另一个List
中。它的第一个形参是List<? super T>
,表示目标List
,第二个形参是List<? extends T>
,表示源List
。
代码如下:
public class Collections {
// 把src的每个元素复制到dest中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
// 获取src集合中的元素,并赋值给变量t,其数据类型为T
T t = src.get(i);
// 将变量t添加进dest集合中
dest.add(t); // 添加元素进入dest集合中
}
}
}
我们可以简单地用for
循环实现复制。在for
循环中,我们可以看到,对于<? extends T>
集合src
,我们可以安全地获取类型参数T
的引用(即变量t
),而对于<? super T>
集合dest
,我们可以安全地传入类型参数T
的引用。
(2)
copy()
方法的定义完美地展示了通配符extends
和super
的意图:
copy()
方法内部不会读取dest
,因为不能调用dest.get()
方法来获取T
的引用(如果调用则编译器会直接报错)。copy()
方法内部也不会修改src
,因为不能调用src.add(T)
方法(如果调用则编译器会直接报错)。这是由编译器检查来实现的。如果在方法代码中意外修改了
src
集合,或者意外读取了dest
集合,就会导致一个编译错误。
代码如下:
public class Collections {
// 把src的每个元素复制到dest中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
...
// 获取<? super T>集合的元素只能赋值给Object对象
T t = dest.get(0); // 编译错误
// 不能向<? extends T>集合中添加任何类型的对象,除了null
src.add(t); // 编译错误
}
}
根据上面介绍的,获取<? super T>
集合dest
的元素后只能赋值给Object
对象,而不能赋值给其下界类型T
;我们不能向<? extends T>
集合src
中添加任何类型的对象,除了null
。
(3)
copy()
方法的另一个好处是可以安全地把一个List<Integer>
添加到List<Number>
,但是无法反过来添加。
代码如下:
// 将List<Integer>复制到List<Number>
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);// 编译正确
// 不能将List<Number>复制到List<Integer>
Collections.copy(intList, numList);// 编译错误
这个很好理解,List<Number>
集合中可能有Integer
、Float
等对象,所以肯定不能赋值到List<Integer>
集合中;而List<Integer>
集合中只有Integer
对象,因此肯定可以复制到List<Number>
集合中。
8.PECS原则
我们何时使用
extends
,何时使用super
通配符呢?为了便于记忆,我们可以用PECS原则:Producer Extends Consumer Super。即:如果需要返回
T
,则它是生产者(Producer),要使用extends
通配符;如果需要写入T
,则它是消费者(Consumer),要使用super
通配符。
还是以Collections
的copy()
方法为例:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
T t = src.get(i); // src是producer
dest.add(t); // dest是consumer
}
}
}
需要返回T
的src
是生产作者,因此声明为List<? extends T>
,需要写入T
的dest
是消费者,因此声明为List<? super T>
。
七、面试题
1.Java中的泛型是什么?使用泛型的好处是什么?
- 泛型是一种参数化类型的机制。它可以使得代码适用于各种数据类型,从而编写更加通用的代码,例如集合框架。
- 泛型是一种编译时类型确认机制。它提供了代码编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时产生
ClassCastException
异常。
2.Java的泛型是如何工作的?什么是类型擦除?
- 泛型的正常工作是依赖编译器在编译源码的时候,先进行类型检查,然后进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。
- 类型擦除:编译器在编译时擦除了代码中所有与泛型相关的信息,所以在运行时不存在任何泛型信息。例如
List<String>
类在运行时仅用一个List类型来表示。而为什么要进行擦除呢?这是为了避免类型膨胀。
3.什么是泛型中的限定通配符和非限定通配符?
- 限定通配符对类型参数的范围进行了限制。有两种限定通配符,一种是
<? extends T>
,它通过确保泛型类型必须是T
的子类来设定类型参数的上界;另一种是<? super T>
,它通过确保泛型类型必须是T
的父类来设定类型参数的下界。 - 泛型类型必须使用限定范围内的类型来进行初始化,否则会导致编译错误。另一方面
<?>
表示了非限定通配符,因为<?>
可以用任意数据类型来替代。
4.List<? extends T>
和List<? super T>
之间有什么区别?
- 这和上一题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。
- 这两个
List
的声明都是限定通配符的例子,List<? extends T>
可以接受任何继承自T
的类型的List
,而List<? super T>
可以接受任何T
的父类构成的List
。 - 例如:
List<? extends Number>
可以接受List<Integer>
或List<Float>
;List<? super Number>
可以接受List<Object>
但不能接受List<Integer>
。
5.如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?
-
编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用
T
,E
,K
,V
等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架,最简单的情况下,一个泛型方法可能会像这样:public class TestMethod<U> { public <T, S> T testMethod(T t, S s) { return null; } }
6.Java中如何使用泛型编写带有类型参数的类?
-
这是上一道题的延伸,面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的类型占位符。举例如下:
public class Generic<T> { // key 这个成员变量的数据类型为T, T的类型由外部传入 private T key; // 泛型构造方法形参key的类型也为T,T的类型由外部传入 public Generic(T key) { this.key = key; } // 泛型方法getKey的返回值类型为T,T的类型由外部指定 public T getKey(){ return key; } }
7.编写一段泛型程序来实现LRU缓存?
- 对于喜欢Java编程的人来说这相当于是一次练习。提示,
LinkedHashMap
可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap
提供了一个称为removeEldestEntry()
的方法,该方法会被put()
和putAll()
调用来删除最老的键值对。
8.你可以把List<String>
传递给一个接受List<Object>
参数的方法吗?
-
对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来
String
是Object
的子类,所以List<String>
应当可以向上转型为List<Object>
。但是事实并非如此,List<String>
与List<Object>
之间没有继承关系,真这样做的话会导致编译错误。List<Object> objectList; List<String> stringList; objectList = stringList; // 编译错误
9.Array中可以用泛型吗?
- 这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道
Array
事实上并不支持泛型,这也是为什么《Effective Java》一书中建议使用List
来代替Array
,因为List
可以提供编译期的类型安全保证,而Array
却不能。
10.Java中List<Object>
和原始类型List
之间的区别?
-
原始类型和
<Object>
之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对泛型类型<Object>
进行检查。<Object>
通过使用Object
作为类型参数,可以告知编译器可以接收任何数据类型的对象,比如String
或Integer
。 这道题的考察点在于对泛型中原始类型的正确理解。 -
它们之间的第二点区别是,你可以把任何泛型类型传递给接收原始类型
List
的方法,但却不能把List<String>
传递给List<Object>
的方法,因为会产生编译错误。举例如下:public class Test { public static void main(String[] args) { // 创建一个 ArrayList<String> 集合 List<String> list = new ArrayList(); fillNumList(list); // 编译正确 fillObjList(list); // 编译错误 } public static void fillList(List list) { ... } public static void fillObjList(List<Object> list) { ... } }
11.Java中List<?>
和List<Object>
之间的区别是什么?
-
这道题跟上一道题看起来很像,实质上却完全不同。
List<?>
是一个不确定的未知类型的List
,而List<Object>
是一个确定的Object
类型的List
。 -
List<?>
在逻辑上是所有List<T>
的父类,你可以把List<String>
、List<Integer>
等集合赋值给List<?>
的引用;而List<Object>
只代表了自己这个泛型集合类,只能把List<Object>
赋值给List<Object>
的引用,但是List<Object>
集合中可以加入任意类型的数据,因为Object
类是最高父类。举例如下:List<?> listOfAnyType; List<Object> listOfObjectType = new ArrayList<Object>(); List<String> listOfString = new ArrayList<String>(); List<Integer> listOfInteger = new ArrayList<Integer>(); listOfAnyType = listOfString; // 编译正确 listOfAnyType = listOfInteger; // 编译正确 listOfObjectType = listOfString; // 编译错误
12.Java中List<String>
和原始类型List
之间的区别。
-
该题类似于“
List<Object>
和原始类型List
之间的区别”。泛型数据类型是类型安全的,而且其类型安全是由编译器保证的,但原始类型List
却不是类型安全的。你不能把String
之外的任何其它类型的对象存入List<String>
中,而你可以把任何类型的对象存入原始List中。 -
使用泛型数据类型你不需要进行类型转换,但是对于原始类型,你则需要进行显式的类型转换。举例如下:
List listOfRawTypes = new ArrayList(); listOfRawTypes.add("abc"); listOfRawTypes.add(123); String item = (String) listOfRawTypes.get(0);// 获取元素时需要显式的类型转换 // 编译器不报错,但运行时会产生ClassCastException异常,因为Integer不能被转换为String item = (String) listOfRawTypes.get(1); List<String> listOfString = new ArrayList(); listOfString.add("abcd"); listOfString.add(1234); // 编译器直接报错 item = listOfString.get(0); // 不需要显式的类型转换,编译器会自动转换