泛型
泛型——参数化类型:
方法或接口或类型,其定义中包含未知类型的参数/局部变量/返回值/字段。
其中的未知类型用写成大写字母的类型参数表示,一般有一个泛型参数就写T,两个就写T,U。
实例化时,需要用类型变量指定泛型参数。
一、泛型定义
泛型类
修饰符 class 类名<泛型参数列表>{
类定义字段(其中一定有部分类型被泛型参数替代)
}
如:
public class ClassName<T>{
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
泛型接口
修饰符 interface 接口名<泛型参数列表>{
接口定义字段(其中一定有部分类型被泛型参数替代)
}
泛型方法
修饰符 <泛型参数列表> 返回类型 方法名(…){
…
}
如:
class ArrayAlg{
public static <T> T getMiddle(T... a){
return a[a.length/2];
}
}
note:泛型可以嵌套、继承、实现…
二、类型变量的限定
背景:若一个代码可供多种类型使用,那么很有可能是这多种类型都实现了同样的方法,在这份代码中调用了这个共同的方法。显然,没实现这个方法的类型是不能使用这个泛型代码的。
限定格式:<类型参数 extends 类/接口>,多个限定用 & 连接。
每个类型参数可以有多个接口超类型,但只能最多有一个超类。如果有一个类作为限定,一定要放在第一个。
三、使用泛型——指定具体类型
省略:
所有能推断出具体类型的地方可以省略< >里的内容或者连尖括号都可以省略;
有时省略会引起歧义,则编译器会报错,此时不能省略。
使用泛型类
-
new一个泛型类实例:
ClassName c = new ClassName<>(…);
-
定义一个普通方法时用泛型类的具体实现作为返回类型:
返回类型的位置写:ClassName
-
继承、组合等同2。
使用泛型接口
- 实现泛型接口时可以类似使用泛型类。
- 也可以不指定具体类型。(相当于普通类中定义了泛型方法,再使用该方法时再指定具体类型)
调用泛型方法
// 此处可以省略<String>,通过传入的参数可以推断出String类型
String middle = ArrayAlg.<String>getMiddle("Mark", "Ben", "Alice");
// 此处省略<>里的类型,会报错
double middle = ArrayAlg.getMiddle(3.14, 1000, 0); // Error
// 报错原因:编译器将参数自动装箱为1个Double和2个Integer对象,因它们类型不同所以寻找它们共同的超类型。他们共同的超类型有2个:Number和Comparable接口。不止1个,则报错。
是编译器而不是JVM处理泛型:
-
泛型被定义后,编译器加载泛型定义时,将类型变量擦除,并替换成第一个类型限定(若无则为Object),生成一个原始类型,加载的是这个原始类型,而非泛型:
这一步仅取决于泛型变量列表;
-
在使用泛型时,要么明着给类型参数赋予了类型变量,要么省略了编译器可推断出的类型,编译器要根据实际的类型增加必要的强制类型转换语句:
擦除并替换后的类型限定or Object->实际使用赋予的类型,下转。
(加上这些语句的作用是:编译器可以进行类型检查)
-
若泛型被继承,且子类覆写了泛型中参数类型是类型变量的方法,则类型擦除会与多态发生冲突,编译器的解决方法是在子类中生成桥方法。桥方法中含有强制类型转换。
了解类型擦除的意义——理解泛型的使用限制。
四、类型通配符
类型通配符是使用?代替方法具体的类型实参。
1. <? extends Parent> 指定了泛型类型的上界
2. <? super Child> 指定了泛型类型的下界
3. <?> 指定了没有限制的泛型类型
如:
// 假定定义了泛型类Plate盘子,定义了水果接口Fruit,定义了实现Fruit接口的类Apple
Plate<Apple> p1 = new Plate<Apple>; // 没问题
Plate<Fruit> p2 = new Plate<Apple>; // 有问题,原因:Fruit和Apple有父子关系,但Plate<Fruit>和Plate<Apple>没有
Plate<? extends Fruit> p3 = new Plate<Apple>; //没问题