参考资料:
Java 泛型总结(一):基本用法与类型擦除
Java 泛型总结(二):泛型与数组
Java 泛型总结(三):通配符的使用
java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
JAVA——泛型类和泛型方法(静态方法泛型)
泛型的设计
泛型类
假设我要创建一个泛型类,比如天平,Balance。那我可以采用如下代码定义这个类:
public class Balance<T>{
private T someThing;
public void put(T a){
someThing = a;
}
public T get(){
return someThing;
}
}
这就是一个简单的泛型类,即通过尖括号来表示Table引入了一个类型T,要注意的是T可以用其他大写符号代替,不过常用T,U,S表示任意类型,E表示集合类型,K,V表示关键字和值。
如果采用了多个变量,则可以写为public class Balance<T, U>
用逗号分开。
类型变量T的限定
如果这个天平只想用来称食物(Food类),则我们可以对类型变量T进行限定,限定词采用extends,使用方法:public class Balance <T extends Food>{}
。
并且限定不仅仅可以用类,也可以用接口(如甜的Sweet接口,酸的Sour接口),类型变量T,U直接用逗号隔开,限定类型用&号。多个限定可以写为<T extends Food & Sweet & Sour, U extends Cloth>
但是要注意的是限定接口数量不限,但是限定类只用有一个(因为Java是单继承制)。
泛型方法
泛型方法可以参考Java中的泛型方法
泛型方法可以定义在泛型类中,也可以定义在普通类之中。假如我想定义一个新的泛型方法读数read1,可以在天平类中加入:
public class Balance<T>{
public int read(T a){
return 9;
}
public <T> int read1(T a){
return 19;
}
}
在这里想说的是,read是一个泛型类的普通方法,而read1则为泛型方法,识别方法就是public后int前的<T>
,要注意的是这个T和泛型类的T指的不是同一个类,泛型方法的T起到了类似掩盖的作用,比如在Balance<apple>
中,可以有read1(new apple())
,也可以使用read1(new banana())
,但是不能有read(new banana())
,因为read的接受参数T由类Balance决定,而read1的接受参数由该方法的声明<T>
决定。
另外,对于泛型类中的静态方法只能写成泛型静态方法,而不能使用泛型类中的类型变量。原因参考类型擦除,我的理解是由于编译过后,泛型类的类型变量被擦除了,而静态方法不需要泛型类实例化,所以此时静态方法就没法找到对应的类型变量。
类型擦除
对于Java泛型来说,他是一个伪泛型。即泛型类的泛型变量只是在编译时存在,在运行时候全部会被擦除,比如当泛型类为Banlance<T>
时,Balance<apple>``Balance<orange>
在运行时全部会变为Balance<Object>
,如果T有限定,如Balance<T extends Food>
,则会变成Balance<Food>
,唯一的区别就是在输出是,Java会自动帮你进行强制转换。
类型擦除导致的后果
桥方法
不仅泛型类会被类型擦除,泛型方法也会。但是考虑到Java的多态特性(不同的类会自动调用对应的同名方法),这很可能会导致不好的后果。比如
public abstract class Food{
public int getWeight(){
return 0;
}
}
public class Apple extends Food{
public int getWeight(){
return 3;
}
}
public class Orange extends Food{
public int getWeight(){
return 5;
}
}
public class Balance<T extends Food>{
public int read(T a, int number){
return T.getWeight()*number;
}
}
public class Test{
public static void main(String[] args){
Balance<Apple> aBal = new Balance<>();
aBal.read(new apple(), 2);
}
}
在上面这个例子里面,在运行时,Balance的类型被擦除了,那么aBal.read()实际是调用的Food的还是Apple的.read()
就会有问题。因此,编译器通过内置的桥方法来解决这一问题(详见P318)。
不支持泛型数组
参考
java中,数组为什么要设计为协变?
Java——协变数组和类型擦除(covariant array & type erasure)
简单来说,就是对一个父类变量,如Number[] num
,我们可以赋给其子类数组,如Number[] num = new Integer[10]
。这就是因为数组的协变,因为数组在每次存入时都会进行类型检查,所以是安全的。但是对泛型来说,由于在运行时,类型会被擦除,所以类型检查对泛型就无效了,因此就不能运行泛型数组的出现。
解决方案有很多,个人认为比较方便的是采用数组链表类,即ArrayList<Table<T>>
的形式。
泛型的使用
泛型类的继承
即使泛型类的类型变量T有继承关系,对应的泛型类之间也是没有的。如Balance<Fruit>
和Balance<Apple>
就没有继承关系。
通配符
参考
Java 之泛型通配符 ? extends T 与 ? super T 解惑
Java 泛型 <? super T>
中 super 怎么 理解?与 extends 有何不同?
通配符“?”的一个作用就是为解决变量的转型问题。比如我们可以设置水果类变量f,并把新建的苹果对象赋给f
Fruit f = new apple();
但是我们不能对泛型类这么做,如
Balance<Fruit> bF = new banlance<Apple>();//报错
为了解决这样的情况(特别是在方法中规定形参时),我们引入了通配符。
值得注意的是,Balance<T extends Fruit>
和Balance<? extends Fruit>
的使用是完全不同的。前者用在对泛型类,泛型方法的设计编写之中,后者用在对已经编写好的泛型类的引用上。
上界限定通配符
形如Balance<? extends Fruit>
表示上界限定通配符,即对于这个泛型类,其中所能包含的类型最高就是Fruit类,其余的就是Fruit的子类了。
这就意味着:
- 无法存入数据
- 可以读取数据
无法存入数据是因为当我第一次存入数据时,在程序中Java会临时分配一个类型编号给这个类,再次存入数据时,由于与这个临时类型不同,而存入失败,因此上界限定通配符就只能存入null。
可以读取数据,因为该泛型类中最高类为Fruit类,因此只要声明一个Fruit变量,就可以接受该泛型类中的数据(Furit子类的数据会向上转型,并应用多态)。
下界限定通配符
形如Balance<? super Fruit>
表示下界限定通配符,即对于这个泛型类,其中所能包含的类型最低为Fruit类,其余的都是Fruit的父类如Object,Food等。
这就意味着:
- 可以存入Fruit类及子类数据
- 可以读出Object类的数据
可以存入限定类及其子类数据,是因为给泛型类中,最低为Fruit类,因此只要存入子类都可以向上转型存入而不会产生错误。
只能读出Object类的数据,因为下界限定通配符只能保证下界,上界最高到Object类,因此为了保证转型正确,只能输出Object类。
无限定通配符
形如Balance<?>
的称为无限定通配符。无限定通配符资料较少,涉及到的用法详见核心卷一的P334
补充
关于通配符的使用,有时候很方便,有时候有些麻烦。根据核心卷一,提出了一种改善方法,就是采用辅助方法。具体如下:
public static void swap(Pair<?> p){
swapHelper(p);
}
public static void swapHelper(Pair<T> p){
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
这样就可以避开<?>
禁止读写的权限了。
反射与泛型
反射在泛型中主要是应用于新建对象,构造工厂方法。即利用Class<T>
泛型类的构造器T newInstance()
去产生任意类的工厂方法。详见核心卷一P338.