什么是泛型
泛型是JDK1.5之后引入的新特性,本质上就是将类型参数化。那么什么叫做类型参数化呢?
原本在Java中定义集合时,存入集合中的对象具有很大的不确定性,可能我存入对象引用是一个自定义类,也可能是一个已知类,如String。如设计者编写ArrayList底层的add方法时,它无法确定ArrayList对象调用add方法传入的参数是是什么,
arraylist.add(“不确定类型的参数”)
此时可能首先会想到的是默认参数类型为所有类的超类Object,理论上这样做对于add方法来说是可行的,不会产生什么较大的影响,但当我们要从集合中get元素时,又会产生新的问题。ArrayList的get方法获取的是指定位置的指定元素,那么get方法又该返回什么呢?首先会想到的又是Object对象,而当我们在检索对象并试图对它进行类型转换时才会发现定义返回值为Object可能会出现的问题。例如add时我们向集合中添加的是Employee自定义类对象,而在get方法后进行强制类型转换时,我们可能试图将自定义类Employee对象转换为Integer对象或者String对象,此时编译器就会抛出异常。而导致这一异常产生的原因就是在add操作时编译器无法检测到添加到集合中的元素类型是否是我们所需要的类型。
综上所述,类型不确定情况下不使用泛型会产生两个问题:
- 如果元素类型固定化,例如集合类只可以存储String类型或者Integer类型,对代码的复用能力会造成毁灭性打击;
- 如果元素类型定义为超类Object,对集合又会产生一定的危险性;
由此唯一能想到的方法就是将类型像方法形参一样,所操作的数据类型被指定为一个可变参数。在实际应用中,泛型是普遍存在的,例如我们在写一个排序方法时,需要能够对整型数组、字符串数组甚至其它任何类型的数组进行排序时,我们首先会想到就是将排序方法泛型化。
泛型的应用
泛型的应用主要体现在三个方面:泛型方法、泛型类以及类型通配符。
1. 泛型方法
泛型方法可以接受不同类型的参数,根据传递给泛型的参数类型,编译器适当的处理每一个方法的调用。
泛型方法定义规则如下:
- 泛型方法声明需要在方法返回类型前有一个类型参数声明,可有多个,用逗号分隔开来。如:
public <E,T> void test(E element1 , T element2){ }
- 类型参数只能代表引用类型,而不能代表原始类型(如int、double等基本类型);
- 类型参数能被用来声明返回值类型,前提是返回类型的类型参数需要与泛型类保持一致,且返回值必须是泛型类的泛型属性。如
public class Demo<T> {
private T t;
public T test(){
return t;
}
}
2. 泛型类
泛型类的声明是在类名后面添加类型参数声明部分,也可以有多个泛型参数,同样中间使用逗号分隔开。如:
public class Demo<T,E> {
private T t;
public T test(){
return t;
}
public void setT(T t) {
this.t = t;
}
public T getT() {
return t;
}
}
3. 类型通配符
类通配符一般使用“ ?”来代替具体的参数,它的职责相当于Object类,理论上可以称之为所有类型的父类。例:
public class Demo {
public static void test(ArrayList<?> data) {
//参数可以传入任何引用类型,如String、Integer等
System.out.println("data: " + data.get(0));
}
public static void main(String[] args) {
ArrayList<String> a1 = new ArrayList<>();
ArrayList<Integer> a2 = new ArrayList<>();
a1.add("chen");
a2.add(22);
Demo.test(a1);
Demo.test(a2);
}
}
输出结果:
data: chen
data: 22
我们也可以为类通配符设置参数匹配上限。可能在几个类的继承关系中,为了更加安全,我们所需要的泛型匹配类型为此父类以及其子类类型,没有必要无上限匹配类型,此时我们可以让占位符“ ?”继承最高类型即可,如下所示:
public class Father {
}
public class Son extends Father{
}
public class Demo {
public static void test(ArrayList<? extends Father> data) {
}
public static void main(String[] args) {
ArrayList<Father> a1 = new ArrayList<>();
ArrayList<Son> a2 = new ArrayList<>();
Demo.test(a1);
Demo.test(a2);
}
}
我们可以设置上限同样也可以设置下限,extends是在继承中表示的是从上向下的关系,所以我们使用了extends来设置上限,而super表示向上调用,所以采用super关键字来设置泛型下线,语法与泛型上限相同。
泛型擦除
泛型信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦出掉,称之为泛型擦除。简单的来说,不存在泛型上限的泛型最终都会被替换为Object类型,存在泛型上限的泛型最终会被替换为泛型上限,如<? extends String>最终会被替换为String类型。如下所示:
public class Demo<T> {
T object;
public Demo(T object) {
this.object = object;
}
public static void main(String[] args) {
Demo<String> demo = new Demo<String>(" ");
Class demo_class = demo.getClass();
Field[] fs = demo_class.getDeclaredFields();
for (Field f : fs) {
System.out.println("Field Name : " + f.getName());
System.out.println("Field type : " + f.getType().getName());
}
}
}
输出结果:
Field Name : object
Field type : java.lang.Object
泛型擦出解决了泛型产生之后的一个重要问题,也就是怎样适应JVM,与之前的Java版本代码兼容,但同样也会带来一些隐患。泛型擦除最终会将无上限设置的泛型转换为Object,而Object类是所有父类的超类。对反射了解的人完全可以使用泛型擦除后的方法来进行操作,从而绕过了泛型类型检查。如:
public class Demo<T> {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
try {
Method method = arrayList.getClass().getDeclaredMethod("add",Object.class);
method.invoke(arrayList, 123);
System.out.println((Object) arrayList.get(0));
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
输出结果:
123
从上述代码我们可以看出来,ArrayList被定义为String类型的集合,如果正常添加元素时,很明显执行arraylist.add(123)在编译的时候都会报错,而由于泛型擦除的原因,我们完全可以利用发射来避免了泛型检查,从而将一个Integer对象存入String类集合之中,从而带来数据上的不安全问题。