【一文搞懂泛型】
3.3泛型
3.3.1泛型出现的背景
泛型出现的背景有两点:
- 第一点是在集合容器中,如果没有指定对应类型的话,那么底层的元素就是object,要对容器中的元素进行存取的时候,取出来的同时需要进行类型转换,如果有的类型不支持强制类型转换,这个时候就会报错,因此泛型的出现能够在一开始的时候就指定相应的类型,这就不会造成出错
- 第二点是为了实现代码的复用,比如有一个做加法的函数,加法既可以做int数据的加法,也可以做long数据的加法,但是如果没有泛型,固定写死的话,那就需要去根据不同的数据类型去创建对应的方法,有了泛型之后就可以在调用方法时,直接指定对应的类型就可以
3.3.2泛型的基本使用
- 泛型类
/*** 泛型类* @param <T>*/
class Obj<T>{T var;public T getObj(){return this.var;}
}
/*** 多元泛型类* @param <T,E>*/
class MutliObj<T,E>{T var1;E var2;public T getVar1(){return this.var1;}public T getvar2(){return this.var1;}}
- 泛型接口
/*** 泛型接口* @param <T>*/
interface Info<T>{/*** 在泛型接口中定义方法* @param info* @return*/public T getInfo(T info);
}/*** 实现泛型接口的类*/
class InfomationImpl implements Info<String>{String info;public void setInfo(String info){this.info = info;}@Overridepublic String getInfo(String info) {return info;}
}
- 泛型方法
/*** 定义泛型方法的类*/
class FxMethod {/*** 泛型方法* @param var1* @param var2* @param <T>* @param <E>* @return*/public <T,E> T method(T var1, E var2){if (var2!=null){System.out.println(var2);}return var1;}
}
- 泛型的上下限
- 泛型的上限,在做为参数的时候,使用?extends Father,表示,当前传入的参数只能是Father或者Father的子类才行
class Info<T extends Number>{ // 此处泛型只能是数字类型private T var ; // 定义泛型变量public void setVar(T var){this.var = var ;}public T getVar(){return this.var ;}public String toString(){ // 直接打印return this.var.toString() ;}
}
public class demo1{public static void main(String args[]){Info<Integer> i1 = new Info<Integer>() ; // 声明Integer的泛型对象}
}
- 泛型的下限,在声明泛型的时候,使用<? super Son>,表示,当前传入的参数只能是Son或者Son的父类才行
class Info<T>{private T var ; // 定义泛型变量public void setVar(T var){this.var = var ;}public T getVar(){return this.var ;}public String toString(){ // 直接打印return this.var.toString() ;}
}
public class GenericsDemo21{public static void main(String args[]){Info<String> i1 = new Info<String>() ; // 声明String的泛型对象Info<Object> i2 = new Info<Object>() ; // 声明Object的泛型对象i1.setVar("hello") ;i2.setVar(new Object()) ;fun(i1) ;fun(i2) ;}public static void fun(Info<? super String> temp){ // 只能接收String或Object类型的泛型,String类的父类只有Object类System.out.print(temp + ", ") ;}
}
- 泛型数组,常用的集合如List,Set,Queue等,在定义时都会指定对应的类型,从而创建泛型数组
3.3.3泛型擦除
-
泛型其实是Java中的一个语法糖,为的就是解决上面所说的问题,而在编译成字节码的时候,会将<>里面的泛型都替换为确切的类,这个过程就是泛型擦除
-
泛型擦除包含了三种类型:
- 第一种是没有指定上下限的,在编译的时候就会将所有泛型都转换成Object类
- 第二种是指定了上限的,那么在编译的时候就会将所有泛型转换成上限这个类,比如,那在编译的时候就会转换成Number类
- 第三种是指定了下限的,在编译的时候会将所有泛型转换成指定下限的父类,比如<? super Number>,编译时会替换成Object
-
泛型擦除会有什么问题呢?
- 第一个问题
- 数据的继承性问题:在Java中,数据是具备继承性的,比如Integer 继承 Number,在数组中,如果定义Object[] objArr = new Object[3]; objArr[0]=“abc”;objArr[1]=1;这在编译的时候是没有问题的;
- 但是如果定义了ArrayList list = new ArrayList();这就会报错;因为在编译的时候,泛型会进行擦除,擦除之后的语句变成ArrayList list = new ArrayList();这样虽然看起来没什么问题,但是编译器对于左右两边的类型就无法判断是不是兼容。
- 再细致点讲,比如如下代码
- 第一个问题
List<Object> list = new ArrayList<>();list.add(123);list.add("abc"); List<String> newList = new ArrayList<>();newList = list;
上面的代码中,list和newList的类型是不一样的,对其进行赋值(相当于上面的操作),如果能赋值成功,那么久会导致newList中的元素既有int类型,又有String类型,这样是会造成错乱的,所以编译器不允许这种形式的存在。
第二个问题:
- 同样是因为数据具备继承性,比如我创建一个方法 method(List list);我希望我传入的是List类型的时候,这个方法也能调用,其实这个过程就转换成上面的第一问题了,我想传入子类参数,实际上就是赋值操作,让List list = new List,这显然是不可以的
- 这时候想到了,由于一个是Object类型的list,一个是String类型的list,那么我可不可以对方法进行重载呢,只要将参数设置成不同类型即可,就像下面的代码所示这样
public class Cmower {public static void method(Arraylist<Object> list) {System.out.println("Arraylist<Object> list");}public static void method(Arraylist<String> list) {System.out.println("Arraylist<String> list");}}
-
看似这样定义方法能解决上面的问题,但是实际上解决不了,同样是在编译的时候会进行泛型擦除,上面两个形参在编译时,他们的形参都会转成Arraylist list,这其实就变成了同一种方法,所以这种方式并不能解决上面的问题
-
泛型擦除可以怎么验证呢?
获得两个泛型的Class对象,让他们进行==操作,得到的结果是true
//泛型擦除ArrayList<String> arrlist1 = new ArrayList<>();ArrayList<Integer> arrlist2 = new ArrayList<>();System.out.println(arrlist1.getClass()==arrlist2.getClass());
3.3.4泛型通配符
-
在上面的泛型擦除问题中,讲到的两个问题,都导致了我们在使用泛型的时候,没办法用到数据的继承性,所以这个时候就出现了泛型通配符,为的就是解决上面的问题
-
泛型通配符的使用:
- <? extends Father>,表示这时候可以赋值Father及他的子类,如下面的代码
ArrayList<? extends Number> list = new ArrayList<Integer>();
我们来分析一下他为什么能解决上面的泛型擦除的问题,因为指定了当前ArrayList的上限为Number,所以在编译的时候,就知道无论如何,list中都是存放Number的子类对象的,就不用担心左右两边出现类型不确定的问题,如下面的代码
- <? super Son>,表示这时候可以赋值Son及他的父类,如下面的代码
ArrayList<? super Integer> list = new ArrayList<Number>();
原因其实和上面extends的一样
- <?>,只有通配符的话就表示没有限定类型
- <? extends Father>,表示这时候可以赋值Father及他的子类,如下面的代码
-
泛型通配符的使用场景:
-
上面的做法虽然解决了泛型擦除的问题,但是在实际使用中要注意使用的场景,这里有两条原则,就是用了<? extends Father>的集合不能调用add()方法;用了<? super Son>的集合不能调用get()方法,具体原因如下
- <? extends Father>的集合不能调用add()方法:
如果现在有一个集合ArrayList<? extends Number> list = new ArrayList<>(),同时允许去调用add()方法,那么既可以往里面加入int,也可以加入float,这样再去做赋值操作的话,比如list = new ArrayList();就会出现问题,因为new ArrayList()限定了类型只能为Integer,但是list里面确可能包含不止Integer一种类型的元素。把这个过程变成下面的代码
ArrayList<? extends Number> list = new ArrayList<>(); list.add(123); list.add(2.3); ArrayList<Integer> newList = new ArrayList<>(); //newList限定了类型,但是list中的元素包含不止Integer的类型 newList = list;
而<? super Son>的集合确可以调用add()方法,原因是,无论后面赋值的是什么类型的集合,都必须是Son或者Son的父类,因此就不会导致加入的元素和定义的类型不同的问题,比如下面的代码
ArrayList<? super Integer> list = new ArrayList<>(); list.add(123); ArrayList<Number> newList = new ArrayList<>(); newList.add(3.14); list = newList;
- <? super Son>无法调用get()方法:
因为如果可以调用get()方法的话,这个list里面不仅包括Son,还可能包括Son的父类元素,比如下面的代码
ArrayList<? super Integer> list = new ArrayList<>(); list.add(123); ArrayList<Number> newList = new ArrayList<>(); newList.add(3.14); list = newList;
由于list可以被newList赋值,所以list里面并不一定只存放了Integer,还可能是Number,所以这是后去get(),就不确定是什么类型了。
而<?extends Father>是可以调用get()方法的,原因就在于,赋值的时候,必须是Father的子类,所以无论传入的是Father还是Father的子类,其实都是Father类,所以就不担心get的时候会搞不清是什么类型,如下面的代码
ArrayList<Integer> newList2 = new ArrayList<>(); newList2.add(123); ArrayList<Double> newList3 = new ArrayList<>(); newList3.add(2.3); list2=newList2; list2.get(0); list2 = newList3; list2.get(0);
- <? extends Father>的集合不能调用add()方法:
-