3.java 泛型

对于我们java中的泛型,可能很多人知道怎么使用并且使用的还不错,但是我认为想要恰到好处的使用泛型,还是需要深入的了解一下它的各种概念和内部原理。本文将尽可能的囊括java泛型中的重要的概念。主要内容如下:

  • 泛型的定义及为什么要使用泛型
  • 定义一个简单的泛型类
  • 定义一个简单的泛型方法
  • 类型参数的限定
  • 泛型内部实现的基本原理
  • 泛型通配符(难点)
  • 泛型的其他实现细节
  1. 何谓泛型
    泛型程序设计意味着编写的代码可以被不同中类型的对象重用。例如:MyList,MyList是一种类型,MyList也是一种类型,但是使用的代码块都是MyList,这也就是java中引入泛型的一种原因:可以增强代码的复用性,当然这种限定死类型的方式也会使得代码的安全性和可读性更高。
  2. 一个简单的泛型类
    先看一个完整的泛型类:
/*一个简单的泛型类的声明如下*/
public class Pair<T> {
    private T a;
    private T b;
    public Pair(T a, T b){
        this.a = a;
        this.b = b;
    }
}

由此可以看出来,泛型类型和普通类型的区别主要在于:类名之后多了个,并且实例域类型可以不是具体的类型而是不确定的T类型。其中,我们管T叫做类型变量,类型变量一般使用大写字母表示并且很短(在java中使用E表示集合的元素类型,K和V分别表示关键字和值的类型)。
使用具体的类型来替换类型变量的过程我们叫做实例化泛型类型。例如:Pair,等。这其中需要注意的是:java中的9中基本类型是不能作为类型变量的,也就是:Pair,是会不允许的。当然,声明一个泛型类时,不局限于一个类型变量,可以由多个类型变量,例如:

/*声明两个类型变量也是可以的*/
public class Pair<T,U> {
    private T a;
    private U b;
    public Pair(T a, U b){
        this.a = a;
        this.b = b;
    }
}
//Pair<String,Integer> p new Pair<>("abc",12);
//创建泛型类实例变量的时候,可以省略类型变量,编译器可以推测出来

3.一个简单的泛型方法

怎么定义泛型类,我们已经介绍过了,接下来我们一起看看泛型方法是如何定义和调用的。

/*泛型类中定义了一个泛型方法*/
public class Pair<T> {
    //声明一个泛型方法
    public <T> T getA(T c){
        return c;
    }
}
/*main函数中调用泛型方法*/
public class Test2 {
    public static void main(String[] args){
        Pair<Integer> p = new Pair<Integer>(1,2);
        //调用泛型方法
        System.out.println(p.<Integer>show(10));
    }
}

我们可以看到,声明一个泛型方法:public T getA(T c),放在返回值前面,修饰符后面,T表示返回类型。泛型方法的调用:p.show(10),在方法名前面放置类型变量,当然也可以选择省略,当编译器没有足够的信息推测出来时就会报错,那时你再添加也不迟(但是,如果你能减轻计算机的工作的话,想必是可以提高效率的)
小结一下,泛型类和泛型方法。泛型类中可以声明泛型方法也可以声明普通方法,泛型方法可以出现在泛型类中也可以出现在普通类中,也就是它们之间并没有什么约束关系。

4. 类型变量的限定

前面我们已经知道了什么是类型变量,我们看一段代码:

public class Pair<T> {
    public static <T> int myCompare(T a,T b){
        return a.compareTo(b);//此处编译不通过
    }
}

我们知道,如果想要使用compareTo方法,就要实现Comparable接口,或者继承实现了此接口的类。此处想要使得程序正确,有两种办法。第一种:使类继承Comparable接口并且实现compareTo方法。第二种:就是使用类型变量限定。如下:

/*限定变量类型*/
public class Pair<T> {
    public static <T extends Comparable> int myCompare(T a,T b){
        return a.compareTo(b);
    }
}

细心的同学可能已经发现,相比于原来的方法,就是使类型变量继承与Comparable接口。原来的变成了,表示:原来的T可以是任意类型的,而现在的T被限制必须实现了Comparable 接口,就是说,凡是使用此泛型的类都是直接或者间接继承了Comparable 接口并实现其中方法的。所以,一旦我们将T限定了,就不用考虑实现Comparable 接口的事情了,程序的封装性更强了。
对类型变量的限定可以由多个限定,它们之间使用&分隔,而使用逗号分隔类型变量。看个例子:

<T extends Comparable>    //一个类型变量的一个类型限定

<T extends Comparable & Serializable> //一个类型变量的两个类型限定

<T extends Comparable,U extends Serializable>//两个类型变量的类型限定

5.泛型实现的基本原理

讨论了这么多的泛型方法,泛型类以及各种使用技巧,接下来,我们一起看看虚拟机实际执行时是怎么对待我们的泛型的。我们都知道java中有编译器和虚拟机,但实际上我们的泛型在这两者看来是不一样的,也就是说,虚拟机是不认识泛型的,而只有我们强大的编译器是认识泛型的。那他们是怎么实现统一的呢?接下来我们详细来看。
在java中,无论何时定义了一个泛型,它都会自动生成一个相应的原始类型。我们叫这个过程为:类型擦除。例如下面的代码:

/*这是一段泛型类的代码*/
public class Pair<T> {
    private T a;
    private T b;

    public T getA(){
        return this.a;
    }
    
    public T getB(){
        return this.b;
    }
}

经过类型擦除之后生成原始类型:

public class Pair{
    private Object a;
    private Object b;

    public Object getA(){
        return this.a;
    }
    
    public Object getB(){
        return this.b;
    }
}

经过对比,我们可以得出结论:去除了泛型的标志性符号<>并且所有的T类型都被替换成Object类型了。难道我们的类型擦除就是将所有的未知类型转换为Object类型吗?当然不是,类型擦除是有规则的而不是一味的将未知类型T转换成Object类型的。
对于有限定的类型变量就将用类型变量的第一个限定类型替换。如:Pair<T extends Comparable & Serializable>,就会选择用Comparable替换所有的T并去除修饰在类后面的泛型符号,生成原始类型。
对于没有限定类型的类型变量就默认使用Object替换类型变量。例如:Pair就会使用Object替换所有的T类型变量。
小结一下,类型擦除针对是否有类型限定类型,根据不同的状况进行替换生成相应的原始类型供jvm调用。

6. 何为通配符

/*我们声明了一个泛型方法*/
public class Pair<T> {
    public static <T extends Double> void show(myArray<T> a){   
    }
}
 上文声明了一个泛型方法,但是实在是太过啰嗦,于是我们可以使用通配符来简化这种泛型方法的声明形式。有时我们也称通配符为更加简洁的类型变量的限定。接下来我们看看使用通配符如何声明一个泛型方法。
public class Pair<T> {
    public static void show(myArray<extends Double> a){   
    }
}

对比两段代码,我们可以看出来,类型限定也就是主要用于类型参数的定义,声明在访问修饰符后面函数返回值之前;而通配符也就是<? extends Double>主要用于实例化类型参数,声明在类型变量之前,简化了泛型方法的声明。所以,凡是可以由通配符实现的都可以使用类型限定来实现,因为通配符是简化了的类型限定。

7.通配符的只读性

以上是这个通配符的一个特性,通配符的只读性是其第二个重要特性,我们知道:Child类继承于Base类是不能推出Pair继承于Pair的,所以:

public static void print(Pair<Base>){

}

Pair是不能作为参数来使用上述方法了,因为Pair和Pair是完全不同的两种类型。但是我们可以使用通配符完成两者的联系。

public static void print(Pair<? extends Base>){

}

这样,Base及其子类都是可以调用此方法的。说明了,Pair和Pair都是Pair<? extends Base>的子类型。下面我们看看为什么通配符要具有只读性,不允许修改属性值。

/*定义泛型类*/
public class Pair<T> {
    private T a;
    public Pair(T a){
        this.a = a;
    }
    public void setA(T a){
        this.a = a;
    }
    public T getA(){
        return this.a;
    }
}
/*main方法*/
public class Test2 {
    public static void main(String[] args){     
        Pair<Integer> p = new Pair<>(0);
        p.setA(10);
        System.out.println(p.getA());
    }
}
输出结果:10

没有使用通配符之前,一切安好;下面使用通配符:

/*定义泛型类*/
public class Pair<T> {
    private T a;
    public Pair(T a){
        this.a = a;
    }
    public void setA(T a){
        this.a = a;
    }
    public T getA(){
        return this.a;
    }
}
/*main方法*/
public class Test2 {
    public static void main(String[] args){     
        Pair<? extends Number> p = new Pair<Integer>(0);
        Pair<Double> d = new Pair<>(1.5);
        p.setA(d);//编译错误
        System.out.println(p.getA());
    }
}

下面我们一起看看java中为什么不允许这种机制的存在,我们不妨现假设java中不存在这种机制,也就是可以对通配符类型进行修改设置其值,语句:Pair<? extends Number> p = new Pair(0);,p中装的是Pair<? extends Number>的子类型,d实际上也是Pair<? extends Number>的子类型,所以可以调用set函数,将Double数值赋值给实际类型Integer,必然出错,所以,java中为了避免这种问题,直接不允许更改通配符修饰的类型。

8.超类通配符

超类通配符从某种角度上来说,和上述的普通的通配符作用上是相反的。其主要的形式为:<? super E>,是否和上述的<? extends E>形式上相似?接下来我们一起看看这种通配符。一段代码:

Class Base implements Comparable<Base>{
    private int x;
    public Base(int x){
        this.x = x;
    }
    //实现compareTo方法
    public int compareTo(Base b){
        .......
    }
}

Class Child extends Base{
    public Child(int x){
        super(x);
    }
}

public static void main(String[] args){
    //定义一个容器类
    DynamicArray<Child> childs = new DynamicArray<Child>();
    //添加元素到容器中
    childs.add(new child(10));
    childs.add(new child(20));
    Child maxA = max(childs);
}

此段程序编译时出错,原因在于,max方法推断传入的参数类型为Child类,调用comparaeTo方法时,发现类型不匹配,拒绝执行,父类Base实现了接口Comparable,而max方法需要Comparable方法,由于子类child并没有重新实现此接口而虽然Child继承与Base,但是Comparable与Comparable却毫无关系。
解决办法,使用我们的通配符

/*将max方法修改如下*/
public static <T extends Comarable<? super T>> T max(DynamicArray<T> a){
    //这样child和Base都可以被匹配到
} 

超类通配符主要用于灵活的写入和比较而<? extends E>主要用于读,不能写入或者修改。

8. 不能创建泛型数组

Pair<Integer>[] p = new Pair<Integer>[10];
Object[] obj = p;
obj[0] = "dfa";
/*编译器不会报错,但是在jvm运行时肯定抛异常*/

上面的这段代码是可以正常编译的,但在运行这段代码的时候会报异常java.lang.ArrayStoreException

这是因为数组在创建的时候就确定了元素的类型,并且会记住该类型,每次向数组中添加值的时候,都会做类型检查,类型不匹配时就会抛异常java.lang.ArrayStoreException
知道了这个数组类型检查机制后,我们来看看创建泛型类的数组会有什么问题,首先我们创建一个泛型类:

public class A<T>{
    private T value;
    public A(T value) {
        this.value = value;
    }
    // 省略 get 和 set 方法
    // ...
}
A<String>[] arr = new A<String>[1]; // 编译错误  

上面的这行代码根本不能通过编译,也就是不允许我们创建泛型数组

为了解释为什么不能创建泛型数组,这样会带来什么问题,我们假如上面的代码可以通过编译.
A[] arr = new A[1];
Object[] objArr = arr;
objArr[0] = new A();
类似我们前面说的那样,我们把 arr 赋给一个 Object[],然后我们往数组中加入 A 类型的值,这样做是可以的,在编译和运行期间都不会报错。因为泛型类型会被擦除,A 和 A 的类型其实是一样的:

System.out.println(new A<String>().getClass() == new A<Integer>().getClass());
// 输出:true

这样就逃过了数组的类型检查机制,在我们使用中就会造成类型转换错误。

所以,Java 中不允许创建泛型的数组。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值