深入解读Java泛型

写在前面
  你们好,我是小庄。很高兴能和你们一起学习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原则

也就是我们对以上的上下限界定符的总结

  1. 要想从泛型类读取数据,并且不能写入,就使用 ?extends 通配符
  2. 要想向泛型类写入数据,并且不能读取,就使用 ? super 通配符
  3. 想写入又想读取数据时,就不要使用通配符

六、泛型的类型擦除

前面我们知道,泛型很好用,到底是怎么实现的呢?
我们在泛型限定那里提到如果继承类和实现多个接口时,必须把类放在第一位。为什么会有这样的规定?
泛型采用的是类型擦除技术,它具体是怎么实现的呢?
带着这些疑问,我们来看看下面的介绍

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、类型擦除后引发的方法冲突

以上就是泛型约束的十大内容,这里就不展开聊,感兴趣的可以去查看相关书籍。如果点赞的人数超过两百的话并且有这个需求的话,我会专门写一篇博文对这十大约束进行详细解读
原创不易,喜欢的话请支持一下

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值