目录
一、泛型阐述
1、泛型是一种重用程序的设计手段
泛型,即参数化类型,目的是将具体类型参数化,在使用时需要传入具体类型进行替换。
参数分为实参和形参,泛型属于类型形参(好比抽象函数,是一种泛指,类似于数学函数中用 x,y,z 代表具体的值)。
2、为什么需要使用泛型?
(1)保证类型安全,进行编译期错误检查,使代码具有更好的安全性和可读性。
(2)不需要进行类型强制转换。
如下程序,如果不使用泛型前,强转结果很容易出错:
public class GenericDemo {
public static void main(String[] args) {
// 不使用泛型,集合中传入的值类型不会受到限制
// 存值没有限制,取值就容易出错,容器安全性得不到保障
List list = new ArrayList();
list.add("String");
list.add(10);
for(Object obj : list){
// 数据取出,需要进行强转,代码可读性和使用性降低
String str = (String) obj;
}
}
}
以上程序会抛出类型转换异常:
如果使用泛型,编译器会进行检查,从而规避已知的类型转换异常:
二、类型擦除,泛型只存在于编译期
1、什么是类型擦除?
Java 泛型的实现是在编译层,编译后生成的字节码中不包含泛型中的类型信息。所以使用泛型时,加上的类型参数,会在编译器编译的时候去掉,这个过程称为类型擦除。
下边示例程序中存放不同类型的集合,编译后只剩下相同的原始类型:
public class GenericDemo {
public static void main(String[] args) {
List<String> list1 = new ArrayList();
list1.add("String");
List<Integer> list2 = new ArrayList<>();
list2.add(10);
System.out.println("两者相同吗?:"+(list1.getClass()==list2.getClass()));
System.out.println("list1集合:"+list1.getClass());
System.out.println("list2集合:"+list2.getClass());
}
}
存放 String 和 Integer 类型 list 的集合,原始类型均为 java.util.ArrayList,并没有携带具体的存储数据类型
也可以使用反射调用 add() 方法,往只能添加字符串类型的集合中添加整型数据(同样也可以证实泛型类型信息确实在编译后的文件中是不存在的)。
2、擦除后只会保留原始类型
(1)什么是原始类型?
原始类型(raw type)就是擦除了泛型信息,最后在字节码中的类型,原始类型才是变量的真正类型。
无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除(crased),并使用其限定类型(无限定变量用 Object )替换。
(2)无限定类型的擦除,原始类使用 Object 替换
下边定义的是一个泛型类:
public class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
编译后,它是下面这样的,类型被擦除,并用原始类型替换:
public class Pair{
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
这是因为在Pair<T>中,T是一个无限定的类型变量,所以用 Object 替换。其结果就是一个普通的类,如同泛型加入 Java 变成语言之前已经实现的那样。在程序中可以包含不同类型的 Pair,如 Pair<Stirng>,Pair<Integer>,但是类型擦除后它们就变成原始的 Pair 类型了,原始类型都是Object。
(3)限定类型的擦除,原始类型用边界的类型替换
如果类型变量有限定,那么原始类型就用第一个边界的类型变量来替换。
比如 Pair<T> 这样申明:
public class Pair<T extends Comparable & Serializable>{}
那么原始类型就是 Comparable。但是,注意,如果 Pair 这样申明
public class Pair<T extends Serializable & Comparable>{}
那么原始类型就用 Serializable 替换,而编译器在必要时要向 Comparable 插入强制类型转换。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界限定列表的末尾。
(4)指定泛型和不指定泛型
调用方法时,可以指定泛型类型,也可以不指定泛型类型。
- 不指定泛型类型:泛型变量的类型为该方法中的几种类型的同一父类的最小级,直到Object。
- 指定泛型:该方法中的几种类型必须是该泛型实例类型或者其子类。
public class Demo<T>{
public static<T> T add(T x,T y){
return y;
}
public static void main(String[] args) {
/**
* 不指定泛型的时候
*/
Integer add = Demo.add(1, 2);// 这两个参数都是Integer,所以T为Integer类型
Number add1 = Demo.add(1, 1.2); // 这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Number
Object add2 = Demo.add(1, false); // 这两个参数一个是Integer,一个是boolean,所以取同一父类的最小级,为Object
/**
* 指定泛型的时候
*/
Demo.<Integer>add(1,2);// 指定类型为Integer,所以只能为Integer类型或其子类
// Demo.<Integer>add(1,1.2);// 编译错误,指定了Integer类型,不能为Float
Demo.<Number>add(1,1.2);// 编译通过,指定为Number,所以可以为Integer和Float
}
}
三、类型擦除带来的问题
1、引用传递问题(传入)——确保使用正确的限定类型
问题描述:
类型信息被擦除,怎么能保证只使用泛型变量的限定类型?编译后 String是Object,Integer 也是 Object,怎么能确定使用哪一个呢?
(1)检查针对引用,而非引用对象
什么是引用?
比如 A a = new A();
此时变量a指向了一个A对象,a被称为引用变量,也可以说a是A对象的一个引用。我们通过操纵引用变量a来操作A对象。变量a的值为它所引用对象的地址。
为确保正确的使用类型,java的实现顺序是这样的:
先检查泛型类型(针对引用)——类型擦除——编译
代码示例:list1 使用泛型,list2 没有使用泛型
类型检查就是编译时完成的。new ArrayList()只是在内存中开辟了一个存储空间,可以存储任何的类型对象。而真正涉及类型检查的是它的引用,因为我们是使用它的引用来调用它的方法,比如说list1调用add()方法,它做了泛型限定,所以list1引用能完成泛型类型的检查。而引用list2没有使用泛型,所以没有进行类型检查。
通过上边的例子,我们可以明白,类型检查就是针对引用的。谁是一个引用,用这个引用调用泛型方法,就会针对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
(2)泛型中参数化类型不考虑继承关系
下边情况的引用传递是不被允许的——集合不存在类型之间的继承关系
第一种情况:
父类型集合强转为子类型集合,java不允许这样的引用传递。因为在父类型元素强转为子类型元素时,存在很大的类型转换安全隐患,因为你不知道ArrayList<Object>集合里边存的到底是String还是Integer或者是其他的值,这也是泛型出现的原因——类型转换安全。
public static void main(String[] args) {
ArrayList<Object> objecList = new ArrayList<Object>(); // 编译通过
objecList.add(new Object());
// Object类型转String类型,会抛出类型转换错误
ArrayList<String> list1 = objecList; // 编译错误
}
第二种情况:
子类型集合强转为父类型集合,违背泛型设计初衷,编译报错。这个原因是,list2也可以存放数据,它不一定就是String类型的,因此取值时无法确定具体的类型。另外,使用了泛型以后,StringList取值的时候还是要进行强转,这样泛型的存在便没有任何意义了。
public static void main(String[] args) {
// 集合之间不存在继承关系
ArrayList<String> StringList = new ArrayList<String>(); // 编译通过
StringList.add(new String());
ArrayList<Object> list2 = StringList;// 编译错误,无继承关系
}
2、自动类型转换问题(取出)
当类型替换为原始类型,我们在获取的时候,为什么不需要进行类型的强制转换呢?
使用ArrayList和get方法作示例:源码中,数据在return之前会根据泛型变量进行强转。
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
3、类型擦除与多态冲突
现在给定一个泛型类:
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
然后通过一个子类去继承它:
class DateInter extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
那么问题就来了,泛型在经过编译,类型信息被擦除,实际上泛型类Pair经过编译后是这种样子的:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
但是我们在子类继承父类的时候,为泛型父类指定了具体的类型,我们重写的的方法是这样的:
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
很快我们就可以发现子类重写的方法,在父类中根本就没有,这样分析起来,子类倒像是写了两个重载的方法。
补充重载和重写知识点:
重写:返回值和形参都不能改变,外壳不变,核心重写。
重载:方法名字相同,而参数不同,返回类型可以相同也可以不同。
问题浮现:
原本是子类对父类方法进行重写,实现多态。但类型擦除以后,就变成了重载(形参、反参不一致),这样类型擦除就和多态有了冲突。
诡异的桥方法——之所以诡异是因为你看不到,它是由编译器生成的,它的作用就是用来解决泛型和多态的冲突。
桥方法的做法是这样的:
子类A重写了泛型父类的方法:setValue(Date date) —— 指定了具体类型Date
泛型父类B的桥方法:setValue(Object obj) ——>在方法中调用子类重写的方法setValue(Date date)
这时候这个setValue(Object obj)就是桥方法了,它唯一的功能就是去调用子类生成的setValue(Date date)方法,满足子类重写的需要。这是因为,泛型在没有传入具体类型前,它也不知道自己是什么样的(抽象存在),现在子类传入了具体类型,那泛型就按照子类要求的具体类型来实现,父随子变。
这种做法其实理解起来像是在复制,子类有什么,泛型父类就生成一个桥方法调用什么(复制,当然也是有原则的复制),使得子类重写的方法,在父类中是存在的,不破坏代码的多态特性。
4、泛型类型变量不能是基本数据类型
不能用类型参数替换基本类型。就比如,没有ArraryList<double>,只有ArraryList<Double>。因为当类型擦除后,ArraryList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。
5、运行时类型检查异常——instanceof
例如:
ArrayList<String> arrayList = new ArrayList<String>();
因为类型擦除后,ArrayList<String>只剩下原始类型,泛型信息String不存在了。
所以你不能这样去判断:
if(arrayList instanceof ArrayList<String>){}
正确的做法是通过通配符的方式:
if(arrayList instanceof ArrayList<?>){}
6、泛型和异常捕获
(1)不能抛出也不能捕获泛型类的对象
原因:异常都是在运行时捕获和抛出的,编译后,泛型类型信息被擦除,会导致catch两个一模一样的普通异常,这个是不允许的,因此编译会报错。
try{
}catch(Problem<Integer> e1){
// 代码省略
}catch(Problem<Number> e2){
...
}
编译后是这样的:catch了两个一模一样的异常
try{
}catch(Problem<Object> e1){
// 代码省略
}catch(Problem<Object> e2){
...
(2)不能在catch句子中使用泛型变量
示例如下:
public static <T extends Throwable> void doWork(Class<T> t){
try{
...
}catch(T e){ //编译错误
...
}catch(IndexOutOfBounds e){
}
}
上述示例违背了异常捕获的原则,异常捕获一定是子类在前,父类在后,而使用泛型T,编译后类型擦除会变成Throwable,与异常捕获原则冲突。
7、不能创建泛型类型数组
不允许使用参数化类型数组,下边的代码是错误的:
List<String>[] stringLists=new List<String>[1]; // 编译报错
原因:编译器使用类型擦除,参数类型被替换为Object,用户可以向数组中添加任何类型对象,因此下边红色部分是不能限定死类型的。
List<Object>[] stringLists=new List<String>[1]; 这样是不对等的
List<Object>[] stringLists=new List[1]; // 这样是对等的
数组是协变的(关于协变,在通配符中有介绍),不过,有时候错误的使用数组协变特性,还是会带来安全隐患,示例如下:
List<String>[] stringLists = new List[1]; // List<String>类型数组
List<Integer> intList = Arrays.asList(40); // 创建一个整型List元素
Object[] objects = stringLists; // 数组协变这是可以的,万物皆Object,出错的关键在这里
objects[0]= intList; // 将intList放入objects数组中
String s= stringLists[0].get(0); // 强转错误
8、不能实例化泛型类型
下边的代码是不能通过编译的:new 无法为不确定的类型分配内存空间
public static <T> void add(Box<T> box) {
T item = new T(); // 编译不能通过,T没有具体类型
box.add(item);
}
想实例化参数化类型,可以借助反射:
public static <T> void add(Box<T> box, Class<T> clazz)
throws InstantiationException, IllegalAccessException{
// 因为T是在运行时通过反射才能知道是什么类型
T item = clazz.newInstance(); // 通过反射使用字节码
box.add(item);
}
9、类型擦除后的冲突
1、不能使用泛型创建与父类方法名重名的方法
public class Pair<T> {
// 泛型擦除后,与父类方法产生冲突,两个同名,同参数的方法
public boolean equals(T value) {// 编译报错
return null;
}
public static void main(String[] args) {
Object obj = new Object();
obj.equals("object已经有equals方法了");
}
}
2、要支持擦除的转化,需要强制一个类或者类型变量不能同时成为两个接口的子类,而这两个子类是同一接口的不同参数化。
如下边这种情况是不被允许的:
父类实现了Comparable接口,并限定了参数类型为Pair
public class Pair implements Comparable<Pair> {
...
}
子类继承了父类Pair,同时自己又去实现了Comparable接口,也限定了类型
public class PairChild extends Pair implements Comparable<PairChild> {
... // 编译报错
}
这种情况使得PairChild类同时实现了Comparable<Pair>和Comparable<PairChild>接口,这是同一接口的不同参数化实现。
但是,去除泛型后,这样是可以的:
public class Pair implements Comparable {
@Override
public int compareTo(Object o) {
// 父类的具体实现逻辑
return 0;
}
}
public class PairChild extends Pair implements Comparable {}// 编译不会报错
这种情况下Comparable的具体实现都在父类Pair里边,不过这种实现是只能是Object类型的,因为Pair和PairChild归根结底都是Object.
10、不能使用静态域
泛型类中的静态方法和静态变量不可以使用泛型类所申明的泛型类型参数。
原因:因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用,对象都没有创建,所以不能确定这个泛型参数是何种类型,因此编译的时候会报错。
举例说明:
public class Pair<T> {
public static T one; //编译错误
public static T show(T one){ //编译错误
return null;
}
}
但是下边这种情况是正确的
public class Pair<T> {
// 这是一个泛型方法,对象调用的时候需要传入具体的类型参数
public static <T>T show(T one){
return null;
}
}
上述是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的T,而不是泛型类中的T。