一、背景:为什么需要泛型
我们测试下面这段代码,会有运行的异常抛出:“Integer cannot be cast to String”
public class FanXing {
public static void main(String[] args) {
List list =new ArrayList();
list.add("helloworld");
list.add(100);
for (int i =0; i < list.size(); i++) {
//问题出在这里,list中第二个元素强转会失败,因为不知道容器中装的是什么类型
String name = (String) list.get(i);
}
}
}
在上述代码中,我们发现主要存在两个问题:
1.我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,这个对象的编译类型变成了Object类型,但其运行时类型仍然为原类型。
2.取出集合元素时需要强制类型转化到目标类型,很容易出现“java.lang.ClassCastException”异常。
为了解决以上的问题,泛型就出现了。
泛型可以使集合能够记住集合内元素各类型,达到只要编译时不出现问题,运行时就不会出现“java.lang.ClassCastException”异常。
二、概念:什么是泛型
泛型,即“参数化类型”
1、定义的时候,将类型参数化,类似于方法中的变量参数。
2、调用的时候,传入具体的类型。
参考案例:List接口
//Java中List的定义,定义的类型参数E
public interface Listextends Collection {
boolean add(E e);
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
ListsubList(int fromIndex, int toIndex);
}
使用List,增加了泛型之后,发现不匹配就直接编译报错了,必须修改之后才能继续编译运行。
image三、基础使用:自定义泛型类、接口、方法
泛型可以作为类型参数定义在类、接口和方法上,举例说明:
//在类定义中加入泛型
class Box {
private T data;
public Box() {
}
public Box(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
Box nameBox =new Box<>("helloworld");
Box ageBox =new Box<>(32);
//输出 name class:class com.example.fanxing.Box
System.out.println("name class:" + nameBox.getClass());
//输出 age class:class com.example.fanxing.Box
System.out.println("age class:" + ageBox.getClass());
//输出 true
System.out.println(nameBox.getClass() == ageBox.getClass());
发现一个情况:不同泛型参数类型的数据最后getClass()得到的类型是一样的
究其原因,在于Java中提出泛型概念的目的,让其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,在运行前会将泛型的相关信息擦出。也就是说,成功编译过后的class文件中是不包含任何泛型信息的,泛型信息不会进入到运行时阶段。具体会在第五节的泛型擦除详细说明。
四、高级特性:类型通配符
看这一段编译有问题的代码:
public static void main(String[] args) {
Box nameBox =new Box<>("helloworld");
Box<Integer> ageBox =new Box<>(32);
getData(nameBox); //类型匹配,可以编译通过
getData(ageBox); //类型不匹配,编译不通过
}
public static void getData(Box data) {
System.out.println("data :" + data.getData());
}
这样就只能在写一个getData方法,然后传入Box<Integer>这样的类型对象,比较麻烦,也不符合多态的形式。基于这个问题,Java提出了类型通配符的概念。
类型通配符一般是使用 ? 代替具体的类型实参。注意,此处是类型实参,而不是类型形参!
public static void getData(Box data) {
System.out.println("data :" + data.getData());
}
按照上述的定义,就可以解决问题了。
在代码逻辑上 Box在逻辑上是Box、Box...等所有Box<具体类型实参>的父类。
另外,Java还提出了类型通配符上限和下限的概念,比如:
类型通配符上限通过形如Box<? extends String>形式定义
类型通配符下限为Box<? super String>形式定义,含义与上限正好相反
五、高级特性:泛型类型擦除
使用泛型的时候加上的类型参数,会被编译器在编译之后去掉,这个过程就称为类型擦除。
很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:
1、泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。
2、静态变量是被泛型类的所有实例所共享的。对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。
3、泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException<String>和MyException<Integer>的。对于JVM来说,它们都是 MyException类型的。也就无法执行与异常对应的catch语句。
利用泛型类型擦除的特性干“坏事儿”
ArrayList l=new ArrayList();
l.add("abc");
try {
Method method = l.getClass().getDeclaredMethod("add",Object.class);
method.invoke(l,"test");
method.invoke(l,100.f);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("list的大小是:"+l.size());
for ( Object o: l){
System.out.println(o);
}
运行结果是:
list的大小是:3
abc test 100.0
我们可以看见100.0 成功地插入到ArrayList 中了,所以利用类型擦除的原理并结合反射的手段就绕过了正常开发中编译器不允许的操作限制。
网友评论