Java中的泛型

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#这种正常的泛型程序来说,两个赋值都是错误的,但当类型擦除之后,第二个赋值就可以通过,这显然不符合语义;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值