写在前面
你们好,我是小庄。很高兴能和你们一起学习Java。如果您对Java感兴趣的话可关注我的动态.
写博文是一种习惯,在这过程中能够梳理和巩固知识。
从浅到深的泛型解读篇
一、泛型的魅力
1、什么是泛型
泛型的意思是适用于许许多多的类型
泛型能够实现参数变化
使用泛型后,会更具有灵活性,不用指定类型
类型使用<>
尖括号包含起来,例如ArrayList<String>
2、泛型有哪些
- 泛型类:ArrayList,HashSet,HashMap 等
- 泛型方法:Collections.binarySearch ,Arrays.short 等
- 泛型接口:Callable,Iterator,List 等
上面所列举的内容都是我们前面所学过的,泛型类和接口在Java集合框架和工具类这篇文章提到,Callable在Java多线程并发实战这一篇文章的线程池Executor使用到
3、泛型如何使用
以ArrayList为例,指定泛型类型是String
方式一:
ArrayList<String> list=new ArrayList<String>();
方式二:
ArrayList<String> list=new ArrayList<>();
注意: <>
里面不能放基本类型,只能放包装类或对象,比如int
要用
它的包装类Integer
4、如果不用泛型会遇到什么情况
我们知道了泛型的诸多好处,我们不妨想想如果不用泛型会有什么情况。
情况一:假设我们在ArrayList不使用泛型,同时存储整型和字符串,然后我们遍历的使用进行类型转换编译没有报错,但运行报错,我们来看截图
我们可以存放多种类型的数据在集合中,但在遍历取出的同时进行类型强制转换的时候,编译并没有提示错误,但是运行时,我们发现报异常了
我们使用泛型时,有一定的限定条件,能够避免转型错误(不需要考虑转型)
如以下代码
import java.util.ArrayList;
public class GenericDemo {
public static void main(String[] args) {
//指定String类型
ArrayList<String> list=new ArrayList<>();
//添加元素
list.add("123");
list.add("abc");
for(String s:list) {
System.out.println(s);
}
}
}
我们给类做了限定,指定是String类型,不管存数据还是取数据都只能是String类型,这样我们就不需要考虑转型了
情况二:我们定义一个类、方法、接口如果想支持多个类型,就需要写多个类,方法、接口,代码的复用性不高,我们来进行讨论。
如果我们有这个需求:根据参数类型,返回不同数据
public class Test {
public int get(int i) {
return i;
}
public String get(String s) {
return s;
}
}
我们需要定义两个甚至多个方法,但如果使用泛型方法,则只需要一个
public class Test {
public <T> T get(T t) {
return t;
}
}
实际上我们用不到上面代码"自己获取自己"的方法,但通过这个例子我们可以看出泛型的好处之一
二、自定义简单泛型
1、自定义泛型类
我们通过查看Java的API文档中的ArrayList类的定义
public class ArrayList<E>
比我们普通定义类多了<E>
Java中有自定义泛型的约定俗成,自定义的泛型使用T
字母表示,与源码使用的E
进行区分,这样便于其他编程人员阅读代码,如下
public class MyArrayList<T>
2、自定义泛型方法
在第一章中,我们有介绍泛型方法
public class Test{
public <T> T get(T t) {
return t;
}
}
<T>
表示这是泛型方法,自动根据参数类型识别返回类型,具体怎么实现我们先不管,T指的是返回类型,这个可以在普通类中使用的泛型方法,如果是在泛型类中使用方法,如下:
public class Test<T>{
//这里我并不需要<T>,因为我处于泛型类中,已经知道T的意义
public T get(T t) {
return t;
}
}
通过上面两个代码我们看出了区别了吧
3、自定义泛型接口
泛型接口和泛型类的定义很相似,只需要把class改成interface就可以了
public interface Test<T>{
//泛型接口的方法
public T get();
}
通过上面的介绍,我们来练习一下吧
4、练习
定义一个泛型接口
public interface TestImp<T> {
public void set(T left,T right);
public T getLeft();
}
定义一个泛型类,实现泛型接口
public class MyTest<T> implements TestImp<T>{
private T left;
private T right;
@Override
public void set(T left, T right) {
this.left=left;
this.right=right;
}
@Override
public T getLeft() {
return left;
}
}
我们通过主类来使用
public class MyTestDemo {
public static void main(String[] args) {
//定义类的同时要指定泛型的类型
MyTest<Integer> t=new MyTest<>();
//调用泛型方法
t.set(100, 666);
System.out.println(t.getLeft());
}
}
看完上面的内容是不是自信心起来了,原来泛型这么简单。这么简单肯定不能满足我们需求的,我们需要深入学习它,耐心把下面内容看完吧
三、元组类库
Java本身是不支持元组的,但是我们可以通过泛型来实现它
什么是元组
官方定义是: 将一组对象
直接打包存储于其中的一个单一对象,这个容器允许读取其中的元素,但是不允许向其中放新的对象
仅一次方法调用就可以返回多个对象
为什么要使用元组
在未使用泛型之前,一个return语句只允许返回单个对象,因此,解决办法就是创建一个对象,用它来持有想要返回的多个对象。但是每次需要的时候,都要专门创建好一个类来完成这样的任务,这会给服务器造成很大的负担
如何使用元组
元组的长度是固定的,所以我们需要定义多个不同长度的元组,其中是通过继承(extends)的方式实现的,如下
//定义二维元组,有两个参数,对应两个对象
public class TwoTuple<A,B>{
//通过final限定一旦定义则不可修改
public final A first;//第一个对象
//第二个对象
public final B second;
//有参构造,在使用元组时返回传入的值到指定对象中
public TwoTuple(A a,B b){
this.first=a;
this.second=b;
}
}
我们通过继承的方式实现长度更长的元组,如下
//三维元组有三个参数,分别对应三个对象
public class ThreeTuple<A,B,C> extends TwoTuple<A,B>{
//只需要定义第三个对象,其他两个对象通过继承得到
public final C third;
public ThreeTuple(A a,B b,C c){
//通过super给父类的有参构造传值
super(a,b);
//给本类传值
this.third=c;
}
}
//请新建一个FourTuple类
//同样,四维元组有四个参数,分别对应四个对象,也通过继承扩展
public class FourTuple<A,B,C,D> extends ThreeTuple<A,B,C>{
//我们只需要定义第四个对象即可
public final D fourth;
//有参构造
public FourTuple(A a,B b,C c,D d){
super(a,b,c);
this.fourth=d;
}
}
使用元组,我们只需要定义一个长度合适的元组,然后在方法中通过return 创建并返回该元组。我们来看代码
//我们定义了一个元组的工具类
public class TupleDemo{
//通过泛型方法,定义二维元组,其他元组类似定义
public static <A,B> TwoTuple<A,B> tuple(A a,B b){
return new TwoTuple<A,B>(a,b);
}
//定义三维元组
public static <A,B,C> ThreeTuple<A,B,C> tuple(A a,B b,C c){
return new ThreeTuple<A,B,C>(a,b,c);
}
//定义四维元组
public static <A,B,C,D> FourTuple<A,B,C,D> tuple(A a,B b,C c,D d){
return new FourTuple<A,B,C,D>(a,b,c,d);
}
}
我们来进行测试,一个方法返回多个对象
public class TupleTest {
public static void main(String[] args) {
//声明工具类
TupleDemo demo=new TupleDemo();
//一个方法能够返回多个对象
demo.tuple("您好", "欢迎关注");
demo.tuple("谢谢你的支持", "小编会努力的", "加油");
}
}
小结:
1、元组的长度是固定的
2、元组隐含地保持了其中的元素的次序,比如A,B,C的位置对应
3、制作一个元组工具类,能通过一个方法返回多个对象,框架中常见
四、泛型的限定
为什么叫泛型的限定呢,比如我们指定这个泛型是继承某个类或实现某个接口,这就是限定的含义
1、extends
这个关键字我们在学习类的继承的时候已经用过,但请注意: 这里的extends不单单表示继承的意思,它也表示实现,替代implements
。单听概念我们无法理解,我们来看代码
List
类是Java的API文档的类
//T 继承了List 使用extends
public class Generics<T extends List>
Comparable
是Java的API文档接口类
//T 实现了 Comparable接口,但是同样使用extends
public class Generics<T extends Comparable>
2、&分隔符
实现多个接口,使用&
隔开,如以下代码
Collection
同样是Java的API文档接口类
//T 实现了 Collection接口和Comparable,使用&分开
public class Generics<T extends Comparable & Collection>
值得注意的是: 保持Java的继承规则,一个泛型类只能继承一个类,可以实现多个接口。如果有类,类必须
放在第一个位置,例如
public class Generics<T extends List & Comparable & Collection>
3、逗号分割符
如果我们使用多个参数的时候,我们需要用逗号分割开。在第三章聊元组的时候我们可以发现 public class TwoTuple<A,B>
。我们知道A和B是使用逗号分割开的。同样,我们限定的时候也这样,当多个参数的时候就是使用逗号进行分隔开来,如以下代码:
public class Generics<T extends List ,U extends Comparable & Collection>
小结:
1、使用extends关键字继承类或实现接口
2、一个泛型参数最多只能继承一个类,可以实现多个接口
3、当继承类同时实现接口时,继承的类必须放在第一位
4、继承类或实现多个接口时,使用&
进行分开
5、使用逗号对泛型参数进行分割开
五、泛型的继承原则
1、泛型之间的继承关系
首先,根据上图,我们知道Dog和Cat继承于Animal类,它们之间是继承关系
假设我们定义了一个泛型类Generics<T>
注意1: Generics<Dog>
和Generics<Animal>
没有关系,也就是说Generics<A>和Generics<B>
没有继承关系,无论A和B是否是继承关系,举例如下:
我们知道ArrayList和List是有继承关系的,泛型类可以扩展或实现其他的类
注意2: ArrayList<T>
和List<T>
有继承关系,保留了原有的继承关系
注意1和注意2中我们发现,同一个泛型类,尽管参数A和参数B之间有关系,但是泛型类并没有关系,如果是不同的泛型类,并且参数一致,会保留原本类的关系
如图总结
3、泛型的通配符类型
无界通配符 ?
也叫无限定通配符,表示任意类型
具体使用:Test<?>
无界通配符:<?>
,我们会觉得这个和<T>
好像没有什么区别,我们都知道<T>
经过类型擦除后是原生类型Object。<?>
却不一样,<?>
是在声明:”我是想用Java泛型来编写这段代码,我在这里并不是要用原生类型,我表示任意类型“
?和 T的区别
T 通常用于泛型类和泛型方法的定义
?通常用于泛型方法的调用代码和形参,不能定义类和泛型方法
无界通配符只能读取数据,不能插入数据
错误提示:
The method add(capture#2-of ?) in the type ArrayList<capture#2-of ?> is not applicable for the arguments (Object)
Object的原生类型都不能传进去,这验证了不能插入数据。因为并不知道到底是什么类型。只能通过它读取。如果去掉第9行代码,运行同样会报错,报错原因是ArrayList 没有数据,越界异常,但这并不是我们这里要讨论的
上限界定符(协变)
public class Test<? extends List>
上面代码中,我们使用到? extends
表示只能接收List类以及它的子类
只能读取数据,不能写入数据,因为父类并不知道子类的类型,所以父类不能对子类进行写数据操作
Test<? extends List>
代表Test<List>
、Test<ArrayList>
等等
下限界定符(逆变)
public class Test<? super ArrayList>
我们使用了? super
表示 只能接收List类以及它的超类(父类
)
只能写数据,不能读取数据,编译器只能保证放进去的是自身类或超类,并不能保证出来的是自身类还是超类的数据,子类可以把数据转为父类
Test<? super ArrayList>
代表Test<ArrayList>
、Test<List>
、Test<Object>
等
泛型PECS原则
也就是我们对以上的上下限界定符的总结
- 要想从泛型类
读取数据
,并且不能写入
,就使用?extends
通配符 - 要想向泛型类
写入数据
,并且不能读取
,就使用? super
通配符 - 既
想写入又想读取
数据时,就不要使用
通配符
六、泛型的类型擦除
前面我们知道,泛型很好用,到底是怎么实现的呢?
我们在泛型限定那里提到如果继承类和实现多个接口时,必须把类放在第一位。为什么会有这样的规定?
泛型采用的是类型擦除技术,它具体是怎么实现的呢?
带着这些疑问,我们来看看下面的介绍
1、泛型的实现原理
我们知道Object是所有类的超类,当进行类型擦除的时候,会进行替换泛型变量。代码如下:
泛型的代码(原始类型)
public class Test<T>{
private T frist;
public Test(T t){
this.frist=t;
}
}
编译时进行类型擦除后代码
public class Test{
//泛型变量T变成了Object
private Object frist;
public Test(Object t){
this.frist=t;
}
}
2、泛型限定时类必须放在第一位的原因
我们先来看代码
泛型的代码(原始类型)
public class Test<T extends ArrayList & Comparable>{
private T frist;
public Test(T t){
this.frist=t;
}
}
编译时进行类型擦除后代码
public class Test implements Comparable{
private ArrayList frist;
public Test(ArrayList t){
this.frist=t;
}
}
注意: 在类型擦除的时候,会把泛型变量T替换为泛型限定extends
关键字后的第一个类或接口,这也是为什么一定要把类放在第一位
我们通过代码来验证一下效果
案例实现:
import java.lang.reflect.Field;
//定义接口A
interface A {}
//定义接口B
interface B {}
//定义类C
class C {}
//定义类D继承类C,并实现接口A和接口B
class D extends C implements A,B{}
//定义一个泛型类
public class Generic<T extends C & A & B> {
//定义一个成员变量,观察类型的变化
private T typeName;
public static void main(String [] args) {
//声明泛型类
Generic<D> generic=new Generic<>();
//通过反射获取类的成员变量
Field[] fs=generic.getClass().getDeclaredFields();
//遍历字段组
for(Field f:fs) {
//获取字段的类型名
System.out.println(f.getType().getName());
}
}
}
运行后的结果为:C
,表示typeName的类型是C类
如果把Generic<T extends C & A & B>
改成Generic<T extends A & B>
运行后结果为A
,此时表示typeName的类型是A类
3、转换泛型的底层实现
编写泛型方法调用时,如果擦除了返回类型,编译器会自动
插入强制类型转换。
注意:是编译器自动,请看下面代码
Pair<Employee> pair=new Pair<>();
Employee employee=pair.getFirst();
我们知道getFirst()
方法的返回类型经过类型擦除后的返回类型是Object
编译器会自动,讲Object转为Employee 类型
插入强制类型转换
//编译器自动执行的代码
Object o=pair.getFirst();
Employee employee=(Employee)o
类型擦除与多态发生冲突,编译器自动生成桥方法。
合成桥的方法保持多态
class People <T>{
public void say(T empty) {
System.out.println("I am People "+empty);
}
}
public class Man extends People{
public void say(Object empty) {
System.out.println("I am Man "+empty);
}
public static void main(String[] args) {
//声明Man
Man man=new Man();
//People引用的对象是Man类型,所以会调用Man的方法
People<String> people=man;
people.say("欢迎关注我");
}
}
上面代码我们看到,调用的是String类型,但是在类型擦除的时候是Object类型,底层会自动通过以下的代码实现
//虚拟机底层进行桥方法
public void say(Object empty) {
say((String)empty);
}
需要知道以下的内容
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型参数都会替换为它们的限定类型
- 会合成桥方法保持多态
- 为保持类型安全性,必要时会插入强制类型转换
七、泛型的约束
1、任何基本类型都不能作为类型参数
2、运行时类型查询只适用于原生类型
3、不能创建参数化类型数组
4、可变参数警告
5、不能实例化类型变量
6、不能构造泛型数组
7、泛型类的静态上下文无效
8、不能抛出或捕获泛型类的异常实例
9、可以消除对受查异常(checked exception)的检查
10、类型擦除后引发的方法冲突
以上就是泛型约束的十大内容,这里就不展开聊,感兴趣的可以去查看相关书籍。如果点赞的人数超过两百的话并且有这个需求的话,我会专门写一篇博文对这十大约束进行详细解读
原创不易,喜欢的话请支持一下