泛型是java中一个很重要的概念,虽然我们平时可能很少用上,但不代表就不需要学习。其实很多牛掰的框架模块,里面都经常使用泛型,随便点开几个源码就能看到了。
1.什么是泛型?
平时我们很少会用到泛型,但是它是无处不在的,例如随便打一个List,就能看到使用了泛型的类。简单来说我们时不时看到的那些尖括号包裹的,单独一个大写字母代替了具体类型的地方,就是泛型。
泛型,即“参数化类型”,是在JDK5的时候推出的,就是将具体的类型参数化(类型形参),在使用和调用时才知道它的具体类型(类型实参)。这种参数类型可以用在类、接口和方法中,分别称为泛型类、泛型接口、泛型方法。
2.为什么需要泛型?
说起为什么会需要泛型,就必须举一个集合的例子了
首先看这段代码:
List list = new ArrayList();
//一个List集合中添加了字符串和整数两种不同类型的数据
list.add("qqyumidi");
list.add(100);
for (int i = 0; i < list.size(); i++) {
String name = (String) list.get(i);
System.out.println("name:" + name);
}
结果会抛出ClassCastException: java.lang.Integer cannot be cast to java.lang.String异常,因为类型转换失败了嘛,但是在编译时不会察觉到有任何问题(当然现在牛逼的IDEA已经有一定的提示了,但是终究只是提示,无法在编译前标红报错)。
没有泛型的情况下,我们将一个对象加入集合中,集合不会记住它的类型,也就是统一用Object存储的。所以在取出时,我们不得不一个一个的进行类型转换。而一旦进行了类型转换,如果整个集合的数据都是同一个类型还好,万一加入了一个不一样的类型,立马就是报错ClassCastException。
为了让这种问题能在编译时就暴露出来,在运行时不会发生ClassCastException异常,泛型应运而生。
使用泛型后的集合
既然泛型能解决这个问题,我们就看看泛型是如何解决的,还是这段代码,只是在初始化List时指定了类型
List<String> list = new ArrayList<>();
//添加了字符串和整数两种类型的数据
list.add("qqyumidi");
list.add(100);
for (int i = 0; i < list.size(); i++) {
String name = list.get(i);
System.out.println("name:" + name);
}
无需启动,在编译时就已经提示list.add(100)是标红报错的了,通过指定类型,直接限定了List集合中只能装String类型的元素。既然无法添加其他类型,自然在取出来的时候也不用类型转换了。
3.泛型的使用
泛型分三种使用方式,分别是泛型类、泛型接口、泛型方法
①泛型类
直接用例子来看会更快,先创建一个普通的泛型类:
package com.lzh;
public class Box<T> {
private T data;
public Box(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
就是个普通的类,唯一不同的是类名后面加了个**<T>**,并且类中的属性和方法,原本应该是具体类型的地方都用T代替了,这个T就是泛型。关于这个泛型有很多不同的字母,什么E、V、K、T,其实区别不大,具体我会放到后面再说。
然后对这个泛型类初始化不同的类似参数,看看它们的类名会不会有什么不同。
package com.lzh;
public class Test {
public static void main(String[] args) {
Box<String> stringBox = new Box<>("giao");
Box<Integer> integerBox = new Box<>(250);
System.out.println("stringBox class:" + stringBox.getClass());
System.out.println("integerBox class:" + integerBox.getClass());
System.out.println(stringBox.getClass() == integerBox.getClass());
}
}
结果是二者并没有任何区别,不管它泛型的实际参数填的是什么,它这个类都是Box类。并且我们发现,它明明只有一个构造方法,却可以传字符串或整数都可以成功构造对象。
这样我们可以看出,实际上这个泛型和我们平时普通的new对象一样,唯一的特别之处就是用尖括号传了一个“参数”<Integer>,并且将泛型类里所有的泛型位置(字母T那些)都替换成了传入的参数。
所以我们也无需把泛型类想的太复杂,就当是在new对象的同时还需要额外用尖括号传一个参数给它而已。
注意:另外泛型的类型只能是引用类型(也就是对象类型),不能是简单类型如int、long之类的。
②泛型接口
泛型接口与泛型类的定义和使用基本相同,还是看例子说话
创建一个十分简单的接口,只定义了一个简单方法。
package com.lzh;
public interface Generator<T> {
public T getInfo();
}
但是当类实现它时,就有了两种不同的情况:
情况1:在实现泛型接口时不传入泛型实参,那么这个类与泛型类的定义是相同的,还是相当于一个泛型类
package com.lzh;
public class WaterGenerator<T> implements Generator<T> {
@Override
public T getInfo() {
return null;
}
}
情况2:在实现泛型接口时传入泛型实参,那就只是实现了一个接口的普通类,在它的眼里这个接口中方法所有的T泛型都用传入的实参String代替了,所以类中实现的方法也是代替后的。可以说这个类就是个普通的类,和泛型没有关系了。
package com.lzh;
public class WaterGenerator implements Generator<String> {
@Override
public String getInfo() {
return null;
}
}
③泛型方法
这个泛型方法有个误区,有些人会认为在泛型类里面带T的就是泛型方法,其实并不是,包括前面我们在泛型类里的方法,那都不是泛型方法,只能说是泛型类里的方法。
不好区分,但是泛型方法也有个标识性的区别:public与返回值之间有个<T>。
这样看起来,尖括号才是泛型的代名词嘛,如果没有<T>,那么泛型类中的方法的T都会标红,因为没有叫T的类。可以认为<T>就是对泛型的声明,只有标注了<T>,才可以在里面使用T。
泛型方法也是,泛型方法并不依靠泛型类,那么声明的<T>就要加在方法上面,例如:
package com.lzh;
public class Person {
public <T> T getSelf(T param) {
return param;
}
}
这样我们就可以很容易的区分是泛型方法还是泛型类里的方法了。
另外,静态方法无法访问类上的泛型,所以静态方法如果要使用泛型,即便是在泛型类中,也要用泛型方法的方式。
④泛型通配符的区别
我们在定义泛型时,经常会看到不同的通配符,比如T,E,K,V等等,它们有什么区别呢?
其实它们都是通配符,对java来说并没有什么区别,甚至我们可以自己从A-Z中挑一个用都无所谓的,不会影响程序和泛型的使用。
这只是一种约定俗成的东西,就像驼峰命名变量一样,就算我们不遵守也不会对程序的运行产生影响,但是好的程序员都是会遵守这个约定的。
T:Type,表示具体的java类型
K V:Key Value,表示java键值对中的Key和Value
E:Element,表示集合中的元素
N:Number,表示数值类型(Integer和Long那些)
?:表示不确定的java类型
然后这里有个巨坑的地方,就是T和?的区别,因为A-Z和T是一样的,但是T和?却并非一种东西。
T是确定的java类型,?是不确定的java类型。
如果实在要说明一个区别,那么最大的区别就是,问号大多是用在泛型方法上的形参的,不能用于定义泛型类和泛型方法。
简单说明一下我自己对它们的理解:
T和?最大的区别就是一致性,T是已知的类型,在一个泛型类中,只要我们声明了T,那么在使用这个类时,它里面所有T的位置都会被我们传入的类型参数替换,不会出现不一致的情况。泛型方法也是如此,所以我们可以在方法内部使用T的类型,如果是集合类型,也可以单独取出每个对象赋给T类型:
public <T> T getSelf(T param) {
T t = param;
return t;
}
但是?就不行了,它表示未知的类型,所以我们无法在方法中定义?类型,只能操作它作为形参的对象。既然是未知的类型,那么它也就不能用在泛型类上声明泛型。
感觉理解的还有有些模糊,但我们只需要知道问号最常用的地方是下面的场景就好了
⑤通配符边界
上界:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
下界:用 super 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类,直至 Object
当我们需要用一个方法接收List集合的参数,而这个集合中的元素可以是一种类的子类或父类时,终于就可以用上?了。
先用例子说明一下场景,假设我们有一个泛型类,其中有这样一个方法:
public void doWord(List<Animal> animal)
它接收了List<Animal>的参数,我们先不管它做了什么,现在我们需要传List<Dog>进去调用,Dog是Animal的子类。
这时就会发现失败了,虽然List还是同一个List,但由于泛型不同,所以不能调用,尽管它们是父子类。
这时候就可以利用问号和边界关键字了:
public void doWord(List<? extends Animal> animal)
通过这种方式,我们就可以传入List<Dog>并执行方法了,super关键字用法也是类似,只是范围是指定类的父类。
⑥T和?的其他区别
T和?的区别这块我到最后都有点晕,只能尽可能整理它们的区别了。
(1)T没有super关键字
(2)T的extends XX类和? extends XX类效果是类似的,但是T需要提前定义好泛型,无论是泛型类还是泛型方法都要定义好,而?是随处都可以用的。
(3)一个方法中的T都是统一的,不管有几个T,在运行时都是同一个类,而多个?并不统一,可以相互不一样。
(4)T用于泛型类和泛型方法中的定义,?用于方法中声明或参数。(声明是声明变量)
*4.类型擦除
关于类型擦除,具体可以了解一下这篇,我写的也只是相当于读后感——
Java泛型类型擦除以及类型擦除带来的问题:https://www.cnblogs.com/wuqinglong/p/9456193.html
①java泛型的实现方法:类型擦除
java的泛型是伪泛型,因为java在编译期间,所有的泛型信息都会被擦除掉。
在代码里定义的List<Object>或List<String>类型,在编译后都会变成List。
如何证明这点呢?
前面我们有用过一个例子,new两个不同泛型的数组,然后getClass比较会发现它们是同样的类,这就是一种证明方式。证明泛型类型都被擦除了,只剩下原始类型。
另一种方式是定义一个ArrayList<Integer>数组,按理说它是不能添加字符串类型的元素的,但当我们用反射的方式添加时,会发现它可以存储字符串了,说明Integer泛型实例在编译后被擦除掉了。
②原始类型是什么
被擦除后的类变成了原始类型,而这个原始类型通常就是Object,可以理解为在编译时泛型类里面所有的T都用Object进行了替换,所以我们用反射的方式可以添加不同类型的数据。
而如果是有边界的泛型,例如Pair<T extends Comparable>,那么它的原始类型就是边界本身Comparable。
③类型擦除引起的问题和解决方法
因为一些原因,Java不能实现真正的泛型,只能用类型擦除来实现伪泛型,这样虽然不会有类型膨胀问题,但是也会引来一些新问题,所以SUN对这些问题做出了一些限制,避免我们发生错误。
例如java编译器在进行泛型擦除前,会先检查代码中泛型的类型,避免我们在ArrayList<Integer>中添加String元素。
通过这个例子可以更好的理解这个检查:
public class Test {
public static void main(String[] args) {
//普通的声明一个泛型ArrayList
ArrayList<String> list1 = new ArrayList();
list1.add("1"); //编译通过
list1.add(1); //编译错误
String str1 = list1.get(0); //返回类型就是String
//声明时不用泛型,new的时候才是有泛型的ArrayList
ArrayList list2 = new ArrayList<String>();
list2.add("1"); //编译通过
list2.add(1); //编译通过
Object object = list2.get(0); //返回类型是Object,和String毫无关联
new ArrayList<String>().add("11"); //编译通过
new ArrayList<String>().add(22); //编译错误
String str2 = new ArrayList<String>().get(0); //返回类型是String
}
}
通过上面的例子,我们可以明白,类型检查是针对变量类型的,如果变量声明时没有声明泛型,那么默认就是Object,不管它真正new出来的类型有没有声明泛型。
个人理解是java在编译时会把所有的T替换成Object,同时存储泛型的类型,在反射或取值出来时会通过存储的泛型类型进行强制转换,以便我们直接使用。
参考资料:
java 泛型详解:
https://www.cnblogs.com/coprince/p/8603492.html
聊一聊-JAVA 泛型中的通配符 T,E,K,V,?:
https://juejin.im/post/5d5789d26fb9a06ad0056bd9
Java泛型(一)类型擦除:
https://www.jianshu.com/p/2bfbe041e6b7