现在网上讲泛型的一大堆,但是很多人要么讲一下语法,要么讲几个注意点,读者无法深入了解泛型的本质!所以这篇文章从泛型的起源,本质上,以通俗易懂的方式讲解Java的泛型!
泛型是在java1.5时从c++中借鉴的!在1.5之前是没有泛型这个概念的!为什么要引入泛型呢?因为当时有以下几个问题:
那个时候的集合类是这样的:
ArrayList list = new ArrayList();
list.add(new String("我是字符串!"));
list.add(new Integer(10));
我们可以往一个ArrayList中添加任何类型对象,为什么是这样设计的呢?
因为,我们这个世界有无数的对象,如果为每个对象都单独设置一个集合类,那是根本不可能的!所以集合类中,巧妙的运用了java多态,设置了一个Object类,用来接收所有的对象!我们来阅读一下ArrayList类的源码:
原来是设置了一个Object类空数组,当我们调用这个无参构造器的时候,会创建一个名为elementData的变量,这个变量会引用之前的静态Object
所以,我们是可以往ArrayList中添加任何我们想要添加的对象的!
读者自己想一下这样做有什么问题?(以上面创建的list对象为例)
首先,list中取出来的所有对象,他本质上是String类型,但是其表现出来的是Object类型,这样我们是无法直接使用String去接收的!见下图:(若对于这一点不懂,建议去看一下java多态的知识)
所以,我们在取出list中元素去使用的时候,必须要进行强转!见下图:
那如果list中不止String呢?那我们每使用一个,就必须对其进行强转!见下图:
如果我们对每个元素都进行强转,那是不是太麻烦了呢?
所以,为了解决这个问题,java引入了泛型!
集合中的元素,一般都要求能够进行比较和排序,那么既然涉及到了比较,你想想,不同类型的对象能够比较、排序吗?人对象和猪对象能比较吗?(如果你觉得能,当我没说。)那就很难比较了!所以,为了使集合中的元素都能够比较和排序,规定一个集合对象中必须存入同一种类型!这是引入泛型的第二个原因!
假如你现在要设计一个 " 点 " 类,即有点的横坐标、纵坐标字段,且要求支持Integer类型、String类型、Double类型。难道我们要为这三种类型每个都单独设计一个点类吗?有人说用Object做接受,那不就回到了上面说的第一个原因吗?
这个时候。为了解决这个问题,java的泛型就来了!
泛型有两种,一种是泛型类,一种是泛型方法!
这里就不再说那让人看了也不懂的百度百科式官方定义了,我们直接上代码吧!
首先是泛型类:
class TestClass{
//这就是泛型类最基本的定义方式了
}
我们在类名的后面,加上一个<>,然后在里面加上一个标识符就行了!不一定非要是T、Y或者K,只要符合标识符命名规则就OK了。
那加上
意思就是,在我这个TestClas类中,T代表了一种类型,但是这个类型是什么,是不确定的,然后我们在这个类中,就可以把这个T当成一个类名去使用,例如:
谁需要用这个类,就在调用的时候,把这个<>中的标识符换成你实际想要用的类型!例如:
这个时候,test对象中所有的T都变成了String类型了!
这种写法叫做 “ 菱形语法 ”。
当然了,T除了可以定义变量外,还可以当成方法的返回值,或者形式参数,例如:
当然了,T是不能new的!例如:
下面我们回到问题一。
有了泛型类这个设计之后,我们就可以在集合类中使用泛型了,于是在java1.5中,ArrayList中是这样的:
于是,我们就可以这样了:
这样,list里面就只能添加(add)String类型了!
如果加别的类型,就会提示 String类型不适用于Interger类型
也就是说,现在ArrayList中所有的E,都变成了String类型了!所以只能用String类型了!
看一下往ArrayList中添加元素的方法 add(E e);
也就是说这里的E现在变成String了,只能接受String了!
到这里,问题一就被解决了!
当然,问题二也被解决了!因为现在存入集合中的都是同一种类型,所以现在集合中的元素是不是就可以比较了!
问题三也解决了,因为我们可以设计一个类:class 点
我们都知道,静态方法是在类加载进jvm后,就加载进方法区,所以,静态方法是不能使用类名后的T的!
所以为了解决这个问题,又引入了静态方法,静态方法的定义如下图所示:
在方法返回值类型前面,加上
那么亲爱的读者,你发现没有,以上这种写法是没有丝毫意义的!为什么呢?
因为这里定义的K ,无法从外界给他设置类型!那如何才能让外界调用者设置K的类型呢,只能在方法参数中设置了,所以,一个标准的泛型方法是这样的:
泛型方法只有从参数中设置类型,才是有意义的!
同时,我们要注意的是,如果一个泛型类定义为class A
那么方法中的T会将类中的T隐藏,也就是说,在这个方法中是以这个方法的T为准,引用这个类时定义的T,不会影响到方法中的T!
解决了老问题,就会出现新的问题,现在假设我们只想往泛型中添加一个制定了范围类型,或者要求其必须实现某个接口!例如,我规定,这个类的泛型只能是Number的子类,那就可以将泛型限制一个范围,具体做法如下:
使用extends,将这个T限制成只能是Number或者Number的子类!注意是包含Number的!
当然T也是可以划分多范围的,只需要在中间使用 & 将类名或者接口名隔开就行,如下所示:
在这里,A和B都是接口,注意,限制 T 的范围,只能有一个类,接口的数量不限制,因为java中类是 单继承多实现 的!
还有就是,必须把这个类放在第一位!
将类放在后面是不行的!
-----------------------------------------------------------------------华丽的分割线-------------------------------------------------------------------------
泛型如此的好用,但是他到底是怎么实现的呢?其实泛型是一个编译时期的语法,是一个语法糖,意思就是他在编译后就会被删除掉,真正运行的时候,是不存在泛型的。
什么意思呢?废话不多说,直接上代码!
//首先我们创建一个泛型类
class Gp{
T t1;
T t2;
public void test_one(T t){
}
public T test_two(){
T t = null;
return t;
}
public static void test_three(K k){
//随便做点啥
}
}
public class TestClass{
public static void main(String[] args){
//创建一个Gp对象
Gp gp = new Gp<>();
//xianzai
String str = gp.t1;
System.out.println(str);
}
}
创建的gp对象,我们将 K 变为 String类型,其实,这个K也好,String也好,都是语法糖,也就是说只是给我们程序员看的,是一种编译时期的语法,编译之后就不存在了,编译后使用的依然是强转!啥意思呢,我们将这个代码生成的class文件反编译之后看一下,你就会豁然开朗!
以上代码编译后的class文件反编译后的代码如下(每一个类都会生成一个class文件,所以有两个):
假设现在有一个水果类 Fruit ,还有一个苹果类Apple,橘子类Orange,Apple和Orange继承Fruit!
现在有如下代码:
ArrayList list = new ArrayList<>();
list.add(new Fruit()); //绝对没问题
list.add(new Apple()); //绝对没问题
list.add(new Orange()); //绝对没问题
由于多态,所以这个地方是不会报错的!!!
但是我们再看下面的一段代码:
//首先有一个方法,就叫他s吧
pulic static void s(ArrayList list){
list.add(new Fruit());
list.add(new Orange());
}
//以下是main中的片段
ArrayList list = new ArrayList<>();
//把这个list放进s方法中
s(list); //报错
这是为什么呢?方法参数类型不是ArrayList
要明确一个问题,Fruit是Apple的父类不假,但是不代表ArrayList
为什么要这样设计呢?这是因为,如果这样不报错,就破坏了原来的ArrayList中只能放Apple的约定了! 因为一旦ArrayList
再来一张图:
于是现在又有了一个问题,如何修改上面的s方法,使他能够接收ArrayList
于是,又引入了一个东西,叫做通配符,即 “ ?” ,于是上面的代码就变成了以下这样,现在传参的时候就不会报错了:
pulic static void s(ArrayList extends Fruit> list){
list.add(new Fruit()); //报错
list.add(new Orange()); //报错
}
//以下是main中的片段
ArrayList list = new ArrayList<>();
//把这个list放进s方法中
s(list); // 不会报错
? extends Fruit 代表的是 Fruit类 及其子类,这个叫做通配符的上界。
现在这个传参的问题就解决了!但是突然上面的add方法又报错了,这是为什么呢?
由于s方法中的参数是未知的,当我们传入的是 Fruit 时,可以add Fruit 或者 Apple 或者 Orange,但是如果传入的是Apple,那么只能add Apple及其子类,但是现在编译器不知道日后你要传入什么类型,所以为了保持类型的一致性,是不允许add的,我们只能去操作,不能添加!
有上界就有下界,? super Apple,这个就是上界!只允许传入 Apple类 或者 其父类。
pulic static void s(ArrayList super Fruit> list){
list.add(new Fruit()); //不会报错
list.add(new Orange()); //不会报错
list.add(new Apple()); //不会报错
}
现在我们使用add又不会报错了,这个又是为什么呢?
因为我们现在知道传入的参数中的泛型,肯定最起码是Fruit,所以只要是Fruit或者其子类,我们都是可以添加的!
在这里,他们的父类处于一种无法确认状态!那么我们该如何去遍历他们呢?
只需要使用Object类接收就行了!
pulic static void s(ArrayList super Fruit> list){
list.add(new Fruit()); //不会报错
list.add(new Orange()); //不会报错
list.add(new Apple()); //不会报错
//由于不知道list的父类,所以我们只能使用Object类去做接收
for (Object object : list) {
//。。。doWork。。。
}
}
以上说的 ? extends Fruit 和 ? super Apple 都是属于有节通配符,如果是单独的 ? ,那么就叫做无界通配符!
如 List> ,代表未知的,这个东西,我们通常用在两个地方:
① 当一个方法是使用了Object类型作为参数时,例如:
public static void printList(List
这个时候,我们可以把以上方法改成使用 ?的,如下所示:
public static void printList(List> list) {
for (Object elem: list)
System.out.print(elem + "");
System.out.println();
}
需要注意的是,此时依然是不能使用add的!除了null;
但是这有什么用呢?用处就是 :可以兼容更多的输出,而不单纯是List
List li = Arrays.asList(1, 2, 3);
List ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
② 在定义的方法体的业务逻辑与泛型类型无关的时候
什么意思?就是说,我现在要有一个方法要接受一个ArrayList参数,但是呢,我这个方法里面实现的功能是和你ArrayList里面装什么东西是没有关系的,所以可以使用 “ ?”。
其实通配符 ?和泛型一样,也是一个处于编译期的语法,编译后就不存在什么 ?,底层使用的依然是强转;可以通过反编译得出结论!
反编译后为: