Java中的泛型
Copyright©stonee
在JDK5之前,为了实现参数类型的任意化,都是 先通过Object类型引入,再进行强制类型转换 来处理。这种方式不仅使得代码臃肿,还要求程序员必须对实际所使用的参数类型已知的情况下才能进行,否则容易引起ClassCastException异常。而且,泛型把这些错误提前到编译期,更安全更方便。
- 我们尝试比较一下通过Object类实现参数类型任意化和通过泛型实现参数类型任意化的栗子:
通过Object类来实现上述结果
package chapter08;
/** *object类实现参数类型任意化
* @author www.stonee.club
*/
public class ttest {
public static void main(String [] args){
c cc = new c();
int a = (int)cc.getA(5); //必须强制类型转换为int,意味着必须要已知 c中getA方法的参数类型才可以 。这意味着:当上述语句循环的时候,判断参数类型将是很麻烦的事
System.out.println(a);
cc.add("888"); //此处没有检查,可以添加任何对象
}
}
class c{
private Object a;
public Object getA(int i) { //必须要已知这里的参数类型
return i;
}
public void add(Object o){
System.out.println(o);
}
}
通过泛型来实现上述结果:
package chapter08;
/** * 泛型类,用于和object继承作比较
* @author www.stonee.club
*/
public class ttestfanxing {
public static void main(String [] args){
ct <String> cc = new ct<>();
String a = cc.getA("44"); //不用强制类型转换
System.out.println(a);
cc.add("888"); //此处必须添加string对象 ,否则会报错
}
}
class ct <T>{
private T a;
public T getA(T i) {
return i;
}
public void add(T o){
System.out.println(o);
}
}
ok,可以看出来,泛型最大的特点就是实现参数类型的任意化。而且,泛型和Object的本质不一样,后者是通过 向上转型后再向下转型 实现的参数任意化,而前者根本不用转型。那么下面具体通过泛型类,泛型方法,泛型接口来了解泛型
语法
1. 泛型类
泛型<>中必须是引用数据类型,且前后类型必须一致
泛型一般不定义为Object
- 泛型类的语法为:
class className < customName >{
private customName a; //此时customName代替原有的类型变量
}
public customName getA(){
return a;
}
}
调用:
className<String> c = new className<>();//jdk7之后后面尖括号不用写
- 我们可以把一个方法的返回值设置为泛型类
public className <String> methodName(String paramName){
return new className<>(); //不能return String;
}
一定要注意,虽然此类可以作为String用,但是返回值千万不能是String,而必须还要是这个泛型类的实例才可以!所以从这个角度来说,泛型类的构造方法必须要适应固定的需求才行。
2. 泛型方法
- 在普通类中定义泛型方法:
class normalClass{
public static <T> T getW(T...a){ //<T> 声明定义泛型方法,T表示返回的泛型方法
return a[a.length/2];
}
}
- 当类和方法都具有泛型时:
class className<T>{
public <Q> void method(Q a){
System.out.println(a);
}
}
//调用
class main{
public static void main(String [] args){
className<String> c = new className<>();
c.method(ture); //boolean 自动装箱
}
}
静态方法必须有自己的泛型,不能和类的泛型一致,因为泛型必须是在类加载实例的基础上的
3. 泛型接口
泛型接口和类的语法差不多
interface in <T>{
T a();
}
尝试用类实现接口
class ci implements in<String>{
@Override
public String a() {
return "stoneeissocool";
}
}
当接口和类都有泛型的时候
class cii<T> implements in<T>{
//平常没必要在实现接口的时候加泛型
@Override
public T a() {
return null;
}
}
4. 类型变量的限定
假如我们需要对传入类型参数进行限制:
public <T> T min(T a){
T s = 1;
if ( s.compareTo(a) > 0 )
s = a;
}
显而易见,此时我们需要T实现Comparable接口,这时就必须对其进行限制,如:<T extends Comparable>
(此处虽然是实现,但是语法是规定的extends)
如果需要继承多个:<T extends Comparable & Serializable>
5.泛型中的通配符
我们知道,虽然Employee和Manager是父子关系,但是
Pair<Employee>
和Pair<Manager>
并没有任何关系,那我们如何让方法public void print(Pair<Employee> p)
也可以接收Pair<Manager>
类型的参数呢?这就用到了通配符
Pair<? extends E> e
是E以及其子类的基类,可以接收Pair<sonOfE>
类型的参数
此处注意不能调用更改器方法,因为编译器无法对setFirst()方法的输入进行强制类型检查。
Pair<? super E> e
E及其子类拿出来
此处不能调用访问器方法,因为编译器无法给getFirst()方法输出前插入强制类型转换。除了将getFirst()方法返回值赋给一个Object,否则编译错误。
- 无限定通配符
Pair<?>
泛型类的内存逻辑模型
我们首先要知道的是,和内部类,lambda表达式一样,Java中的泛型语法在jvm中是不存在的。所以Java编译器需要先把泛型语法转换为虚拟机可已识别的语言。其实,就是转换成了我们开篇所提到的通过Object来实现参数任意化,只不过强制类型转化等需要虚拟机来完成。
假设此时有一个泛型类:
Pair<T> {
private T a;
public void set(T x){
this.a = x;
}
public T get(){
return a;
}
}
当这个泛型类被编译的时候,和一般的类有些不同,编译器把它变成的字节码文件中含有两种,一种是泛型类信息和泛型类的普通类。因为在前面说过,jvm并不承认泛型,所以两种类型中的普通类是为了适应jvm。
当在另外一个类中调用Pair类时:
Pair <String> a = new Pair <> ();
这段代码的调用方式为:
- 第一步:编译器来说,确实存在Pair类,调用get和set方法的时候也会正常把类型变为String,所以如果此时参数类型不为String时,编译器会报错
- 第二步:当编译器把java文件编译为字节码文件时,因为jvm不理解泛型语法,所以编译器需要把带有
<T>
全部擦除(类型擦除)原来的参数类型全部变为Object(详情可自习揣摩本文开篇的例子),然后编译器加上强制类型转换,然后传给jvm。
下面是内存逻辑图:
由于类型擦除引出的问题:
class Pair<T>{
T first;
public void setFirst(T first){
this.first = first;
}
}
class a extends Pair<String>{
@Override
public void setFirst(String first){
this.first = first;
}
}
当编译器将类型擦除后class a
变为:
class a extends Pair{
public void setFirst(Object first){
this.first = first;
}
}
这显然和原因不符合,这就需要编译器在Pair中生成一个桥方法对类型进行强制转换:
class Pair<T>{
T first;
public void setFirst(T first){
this.first = first;
}
public void setFirst(Object first){
setFirset((String) first);
}
}
注:同样的,由于getset返回值一个是Object,另一个是String,因为具有相同返回类型的两个同名通参方法是不合法的,所以此处运行会报错
泛型的约束和局限性
- 不能用基本类型实例化参数类型
例如:pair<double>
当类型擦除后会变成pair
,由于Object中没有基本类型,所以不能转换
- 运行时类型查询只适用于原始类型
例如: (pair<String>) a.getClass()
的返回值是pair
- 不能创建参数化类型的数组
例如:
//数组协变
Object[] table = new Pair[10];
table[0] = new pair();
table[1] = new Object();//编译时通过运行时不通过
上面是一个正常数组的例子,当我们用泛型数组的时候:
Pair<String>[] table = new Pair<String>[10]; //擦除后变为 Pair[] table = new Pair[10]
Object[] objarray = table;
objarray[0] = "hello";
objarray[1] = new Pair<Emm>();
这个程序对于C#这种正常的泛型程序来说,两个赋值都是错误的,但当类型擦除之后,第二个赋值就可以通过,这显然不符合语义;