解析java泛型(一)

解析java泛型(一)

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

  • 泛型的定义及为什么要使用泛型
  • 定义一个简单的泛型类
  • 定义一个简单的泛型方法
  • 类型参数的限定
  • 泛型内部实现的基本原理
  • 泛型通配符(难点)
  • 泛型的其他实现细节

一、何谓泛型

	泛型程序设计意味着编写的代码可以被不同中类型的对象重用。例如:MyList<T>,MyList<Integer>是一
种类型,MyList<String>也是一种类型,但是使用的代码块都是MyList<T>,这也就是java中引入泛型的一种
原因:可以增强代码的复用性,当然这种限定死类型的方式也会使得代码的安全性和可读性更高。

二、一个简单的泛型类

先看一个完整的泛型类:

/*一个简单的泛型类的声明如下*/
public class Pair<T> {
    private T a;
    private T b;
    public Pair(T a, T b){
        this.a = a;
        this.b = b;
    }
}
	由此可以看出来,泛型类型和普通类型的区别主要在于:类名之后多了个<T>,并且实例域类型可以
不是具体的类型而是不确定的T类型。其中,我们管T叫做类型变量,类型变量一般使用大写字母表示并且很
短(在java中使用E表示集合的元素类型,K和V分别表示关键字和值的类型)。
	使用具体的类型来替换类型变量的过程我们叫做实例化泛型类型。例如:Pair<Integer>,<Double>等。这
其中需要注意的是:java中的8中基本类型是不能作为类型变量的,也就是:Pair<int>,是会不允许的。当然
声明一个泛型类时,不局限于一个类型变量,可以由多个类型变量,例如:
/*声明两个类型变量也是可以的*/
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);
//创建泛型类实例变量的时候,可以省略类型变量,编译器可以推测出来

三、一个简单的泛型方法

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

/*泛型类中定义了一个泛型方法*/
public class Pair<T> {
    private T c;

    public Pair(T c){
        this.c=c;
    }
    
    //声明一个泛型方法
    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);
        //调用泛型方法
        System.out.println(p.<Integer>getA(10));
    }
}
	我们可以看到,声明一个泛型方法:public <T > T getA(T c),<T >放在返回值前面,修饰符后面,
T表示返回类型。泛型方法的调用:p.<Integer>getA(10),在方法名前面放置类型变量,当然也可以选择
省略,泛型的解析是需要消耗资源的,它为你提供的只是安全检验,如果你的程序能保证安全性,就可
以不使用泛型,相比之下,是有一点优势效率的,当编译器没有足够的信息推测出来时就会报错,那时你
再添加也不迟(但是,如果你能减轻计算机的工作的话,想必是可以提高效率的)

四、类型变量的限定

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

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 extends Comparable>,表示:原来的T可以是任意类型的,而现在的T被限制必须实现了Comparable
接口,就是说,凡是使用此泛型的类都是直接或者间接继承了Comparable 接口并实现其中方法的。所以,一旦
我们将T限定了,就不用考虑实现Comparable 接口的事情了,程序的封装性更强了。

对类型变量的限定可以由多个限定,它们之间使用&分隔,而使用逗号分隔类型变量。看个例子:

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

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

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

五、泛型实现的基本原理

讨论了这么多的泛型方法,泛型类以及各种使用技巧,接下来,我们一起看看虚拟机实际执行时是怎么对待我们的泛型的。我们都知道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;
    }
}

看下面的代码:

public class Test {
	public static void main(String[] args) {
		ArrayList<Integer> list1 = new ArrayList<Integer>();
		ArrayList<String> list2 = new ArrayList<String>();
		System.out.println(list1.getClass() == list2.getClass());
	}
}
输出:
true

为什么两个list对应的Class对象是一致的呢?因为在jvm层面,他只看到List,没有里面的类型。
也就是,泛型里面的类型信息被擦掉了,这就是所谓的类型擦除。

public static void main(String[] args) {
		ArrayList<Integer> list = new ArrayList<Integer>();
		HashMap<String, String> map = new HashMap<String, String>();
		HashSet<String> set = new HashSet<String>();
		System.out
				.println(Arrays.toString(list.getClass().getTypeParameters()));
		System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
		System.out.println(Arrays.toString(set.getClass().getTypeParameters()));
	}
输出:
[E]
[K, V]
[E]

从上面的输出,我们只是看见泛型的标记,但是完全不问清楚里面的类型信息,这是因为代码经过编译器后,类型信息被擦除了。

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

猜你喜欢

转载自blog.csdn.net/m0_37746551/article/details/87701174