半路出家的我做安卓开发已经很久了,但是自己贪玩的原因再加上待的公司都不怎么注重项目质量,也就各种摸鱼混吃等死的状态熬了几年,现在回头看不仅新技术没有多少,以前的好多基础也丢的一干二净了,真是那个悔啊。最近不知咋地醒悟了,决定好好专研一下技术,重新把以前的丢的知识捡回来,再学习新的知识。写博客也为了督促自己学习吧,这篇算是我学习之路的第一篇,有缘看到的小伙伴一起加油吧~
泛型
对于泛型这玩意我本身用的不是特别多(技术落后的原因占比很大),可是看着源码里面一套一套的,必须完全搞明白,不然源码看的更懵逼。
1.为啥要使用泛型
举个例子,需要打印一个int变量的值,一般都是以下写法:
public static void main(String[] args) {
show(10);
}
private static void show(int a){
System.out.println("a = " + a);
}
但是当你又想去打印一个String变量的值呢,如果直接在main函数里面写show("aaaa"),这样编译器肯定会报错的,因为类型改变了。这时候要打印String就会去写一个show的重载方法,把参数由int改成String
private static void show(String a){
System.out.println("a = " + a);
}
这样的确能满足需求,可是代码就会很麻烦了,以后想打印float或者其他什么类型,是不是又要加一个重载?所以现在有了泛型这种东西,就可以让多种数据类型执行相同的代码,减少这种不必要的重载代码量。
private static <T> void show(T a){
System.out.println("a = " + a);
}
再举个例子,List集合本身是支持泛型的,但是假如让List不使用泛型会怎么样呢?我们写一段代码,在List里面插入String和int两种类型并循环取值打印。
List list = new ArrayList();
list.add("1");
list.add("2");
list.add(3);
for (int i=0;i<list.size();i++){
String item = (String) list.get(i);
System.out.println("item = " + item);
}
可以发现,因为类型不固定,取值的时候也不知道是取的String还是int,假如用String去接收就必须进行强转,类似上面的样子,可是强转时假如遇到了int类型就GG了,会报类转换异常(当然instanceof先去判断能解决这个问题)。因此JDK才给集合类使用了泛型的功能,也建议不要在同一集合存储不同的数据类型。这样的好处是可以把运行中可能会出现的异常在编译阶段就给暴露出来,降低代码出现BUG的可能性。
2.什么是泛型
我讲讲我目前理解的泛型吧,首先,在任意一个存在一个入参的方法中,比如 show(int a),那么这个入参a的类型就是被这个int限定死了,你只能传这一个类型,也就是形参,而这个a具体是什么值,要等真正使用的时候才知道传进来的是什么值,也就是实参。而泛型,例如show(T a),就是你连这个参数的形参类型是啥都不知道,只能等后面实现的时候才知道这个形参类型是啥,也就相当于泛型是当前参数形参类型的形参,而泛型的实参就是这个方法真正实现时候这个参数的形参类型,假如这个show方法是用int最终实现的,那么这个T就是形参,int就是实参。道理有点绕,就相当于一个指数级的概念,一个是三维世界一个是二维世界(二向箔警告)。
3.泛型怎么用
泛型的使用分为类或者接口定义泛型,和方法中定义泛型
a.泛型类和接口
在类或者接口中定义写法上是一样的,类名接口名后加尖括号定义一个或多个泛型变量。
class C1<T>{
T t;
void setT(T t){
this.t = t;
}
T getT(){
return t;
}
}
interface I1<T>{
void show(T t);
}
并且在实现和继承上泛型是可以选择实现或者继续向上传递的,就是说可以选择在继承的任意环节中实例化泛型,把泛型的实参确定下来(此时复写父类方法或者实现接口的方法时,使用的参数就是确定下来的了)。
class c2<T> extends C1<T>{
}
class c3 extends c2<String>{
}
b.泛型方法
泛型方法和类和接口是完全两个空间的东西了,写法是在返回值前面加尖括号定义一个或多个泛型变量。
private <T> void show(T t){
}
需要注意的是,只要不是按照这个格式,在返回值前面没有加尖括号定义的都是不是泛型方法。例如上面泛型类中的setT方法,虽然是有泛型的定义,但那个是与类相关的泛型,与泛型方法没有任何关系。
如果泛型类和泛型方法同时使用,会出现冲突,
public class Test1<T>{
public <T>void show(T t){
}
}
class Test2 extends Test1<String>{
@Override
public <T> void show(T t) {
super.show(t);
}
}
重写父类方法并设置泛型类的实参为String时,复写父类方法依旧是展示的泛型T,由此可见,show方法中的T和Test1中的T就不是一个东西,所以上面才说泛型方法和泛型类是两个空间的东西,而且在两者冲突时,泛型类中的泛型方法只由它本身定义的泛型决定,不受泛型类的影响,泛型类定义的泛型只能影响当前类中的普通方法使用。
4.泛型的限定
在使用泛型时假如不做限定,就意味着使用的泛型实现时没有任何要求,可以为任意类型,这样就会有个问题,当你的业务当中使用泛型的入参需要做一个指定的事,就会因为泛型类型太过宽泛,导致编译器不知道哪个是能完成这件事的类型,就会报错。比如看电影这一件事,电脑能去看,手机能去看,但图书不能去看,用一个泛型类型来处理,的话太过广泛,编译器无法指定哪些能看电影哪些不能,于是在看电影这个事件上就会报错,因此可以让泛型限定为电子产品,电子产品具有看电影这个功能,实现泛型为电脑手机的时候就能正常使用,但图书不是电子产品,就会报错。
public class Test1<T extends Electron>{
}
class Test2 extends Test1<Computer>{
}
interface Electron{
void movie();
}
class Computer implements Electron{
@Override
public void movie() {
}
}
class Phone implements Electron{
@Override
public void movie() {
}
}
class Book{
}
在泛型限定当中无论是继承父类还是实现接口都是用extends,而且可以用&来限定多个限定要求,由于java语法单继承多实现,只能限定一个继承父类和多个实现接口,且父类需要放置在第一个。
public class Test1<T extends Computer&Electron>{
}
5.泛型的局限性
1.不能用基本类型实例化泛型的类型,基本类型实例化时必须使用它封包后的引用对象(如下面的int类型)
class Test extends Test1<Integer>{
}
2.泛型类当中不能在静态域或者静态普通方法中使用泛型的类型变量,当然,泛型方法与泛型类所处的的空间都不同,是可以定义静态的泛型方法的。
public class Test1<T>{
static T t;
public static T getT(){
return t;
}
}
上面这段编译器就无法通过的,因为创建Test1对象时静态变量t是最先创建的,此时虚拟机完全不知道这个T是什么东西,所以静态必须指明明确的变量类型。
3.不能实例化类型变量,即没有new T()这样的写法。
4.可以申明泛型类型的数组,但是不能创建泛型类型的数组
List<String>[] a;//可以使用
List<String>[] b = new List<String>[10];//报错
5.instanceof不适用于查泛型类型
List<String> list = new ArrayList<>();
if (list instanceof List<String>){//报错
}
6.不能捕获泛型类型的示例,即泛型extends不能为Exception/Throwable(这一块用的少,没有深究)
6.泛型类型的继承规则与泛型通配符
class A{
}
class B extends A{
}
如上面代码中B继承于A,分别写两个LIst设置其泛型为A和B,即:
List<A> list1 = new ArrayList<>();
List<B> list2 = new ArrayList<>();
虽然A和B有继承关系,但此时的list1和list2是没有任何继承关系的,因为他们的对象都是List,也就是说,使用泛型的类并不会由于泛型的实际类型是继承关系而变成继承关系。
如果有一个方法如下:
private void showList(List<B> list){
}
传list2是肯定没问题的,但是要想传list1也能执行该方法中的代码,怎么办呢?除了利用麻烦的重载以外,泛型有一个通配符的概念,即改造方法为以下两种
//extends 表示类型的上界,此处即传入的泛型类型最高不得超过A类型
private void showList(List<? extends A> list){
}
//super 表示类型的下界,此处即传入的泛型类型最低不得低于B类型
private void showList(List<? super B> list){
}
extends关键字是类型上界,相当于<=此类型,super相当于下界,相当于>=此类型。也说不上来为啥,就当成语法硬记住就好了,还有就是这个和泛型限定中extends是不一样的,限定是用于泛型的定义时,这里是用于方法接收什么样的泛型类型(泛型是已经定义好了的)。
在这里还要说的是接收以后的List对象(任意泛型类都行,本文都是以List举例)的add和get方法的一些使用规则(错误使用编译器会报错)。
首先我们扩展几个class对象:
class BB extends A{
}
class C extends B{
}
即BB继承于A,C继承于B,那么目前的类型关系就为:
那么我们先在有extends的方法中使用List的add和get方法去操作对象:
//extends 表示类型的上界,此处即传入的泛型类型最高不得超过A类型
private void showList(List<? extends A> list){
list.add(new A());//报错
list.add(new B());//报错
list.add(new BB());//报错
list.add(new C());//报错
Object o = list.get(0);//不报错
A a = list.get(0);//不报错
B b = list.get(0);//报错
BB bb = list.get(0);//报错
C c = list.get(0);//报错
}
这里可以总结一个规律,当使用extends通配符接收泛型类时,泛型类无法去设置该泛型的任意参数类型的实例(因为传进去的只知道最大是A,具体是A的哪个子类并不知道,所有无法改变这个泛型参数),此时泛型类实例去接收这个泛型变量时,只能接收该泛型变量为上界类型以及上界类型以上的父类。
再来看看有super的方法中使用List的add和get方法去操作对象:
//super 表示类型的下界,此处即传入的泛型类型最低不得低于B类型
private void showList(List<? super B> list){
list.add(new A());//报错
list.add(new B());//不报错
list.add(new BB());//报错
list.add(new C());//不报错
Object o = list.get(0);//不报错
A a = list.get(0);//报错
B b = list.get(0);//报错
BB bb = list.get(0);//报错
C c = list.get(0);//报错
}
这里也可以总结一个规律,当使用super通配符接收泛型类时,泛型类只能设置该泛型变量为下界及下界类型以下的子类,接收则只能用Object去接收。
这两个总结说实话真的绕,我表述的也不那么好,而且深挖原因也不知怎么挖,只能看做泛型的固定语法了。把这个例子以及结果记下来就好了。
对了,还有一个是无限通配符,就是只写一个<?>,个人感觉不到有啥用,就不研究了,真要看设置存取得规则的话用上面的例子试一下即可。
泛型的基本用法也就这些了,写博客确实累的很,不过真能帮自己把学到的知识加深一层映象。各位小伙伴们一起加油吧!