Java 泛型
1.概述
泛型,即“参数化类型”,就是将类型由原来的具体的类型参数化。类似于方法中的形参,类型也可以定义成参数形式,然后在声明时指定具体的类型。泛型是为了在不创建新的类的情况下,通过泛型指定不同类型来控制类中的具体类型。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
2.泛型解决的问题
参数安全:引入泛型,能够在编译阶段找出代码的问题,而不是在运行阶段。泛型要求在声明时指定实际数据类型,Java 编译器在编译时会对泛型代码做强类型检查,并在代码违反类型安全时发出告警。
类型转换:不需要显式使用强制类型转换
重复编码:通过使用泛型,可以实现通用编码
3.泛型的使用
3.1 泛型类
在创建类时,在类名后面的<>中定义范型。
public class GenericUtils<T> {
private T var;
public GenericUtils(){}
public GenericUtils(T var){
this.var = var;
}
public T getVar(){
return var;
}
public void setVar(T var){
this.var = var;
}
}
在创建类的对象时,若指定了泛型,则编译器会将泛型替换成所指定的类型,并会进行类型检查。
GenericUtils<String> genericUtils = new GenericUtils<>();
genericUtils.setVar(1); //java: 不兼容的类型: int无法转换为java.lang.String
如果不指定泛型,那么编译器会将类型解释为Object,并用Object进行类型检查。
GenericUtils genericUtils = new GenericUtils();
genericUtils.setVar(1); //编译通过
genericUtils.setVar("1"); //编译通过
声明变量时,要么在<>中指定具体类型,要么raw type(即什么都不写),不可以只写<>
GenericUtils<String> genericUtils; //编译通过,T被替换为String
GenericUtils genericUtils; //编译通过,T被替换为Object
GenericUtils<> genericUtils; //编译不通过
创建对象时,可以只写<>
new GenericUtils<String>();//编译通过,T被替换为String
new GenericUtils();//编译通过,T被替换为Object
new GenericUtils<>();//编译通过,T被替换为Object,这是个语法糖
同样的,类上可以定义多个泛型。
public class GenericUtils<T,E> {
T var1;
E var2;
public GenericUtils(T var1, E var2){
this.var1 = var1;
this.var2 = var2;
}
}
若类上定义了多个泛型,则声明变量时,要么都指定,要么raw type
GenericUtils<String,Integer> genericUtils = new GenericUtils<>();
GenericUtils<String> genericUtils = new GenericUtils<>(); //编译不通过
3.2 泛型接口
在创建接口时,在接口名后面的<>中定义范型。
public interface GenericInf<T> {
T get();
}
实现接口时,如果声明接口为raw type。则重写方法时,T被替换成Object
public class GenericImpl implements GenericInf {
@Override
public Object get() {
return null;
}
}
如果给接口泛型指定具体类型,则重写方法时,T被替换为所指定的具体类型
public class GenericImpl implements GenericInf<String> {
@Override
public String get() {
return null;
}
}
如果让接口仍保持其泛型形式,则实现类也要定义相同的泛型。
public class GenericImpl<T> implements GenericInf<T> {
@Override
public T get() {
return null;
}
}
3.3 泛型方法
普通类中可以定义泛型方法,在泛型类中也可定义泛型方法。在方法返回值类型前面的<>中定义范型。
public class GenericUtils<T> {
//是泛型方法,其定义的泛型T与类上的T不一定是同一个类型。在泛型方法中,Java会采用类型推断,其中的T用方法参数的类型替换
public <T> T getA(T var){
return var;
}
//是泛型方法
public <T,E> void getB(T var1, E var2){
return null;
}
//不是泛型方法,T与类上的T是同一个类型
public T getC(T var){
return var;
}
}
使用泛型方法的时候,通常不必指明类型参数,因为编译器会为我们找出具体的类型。这称为类型参数推断(type argument inference)。
4.泛型上边界
在使用泛型的时候,还可以为泛型的类型进行上边界的限制
<T extends Number> 限制上边界为Number
5.泛型实现方式
如果自己来实现泛型,会怎么实现呢?
5.1 Object类
我们想到Java中Object是所有class的父类。
public class GenericUtils {
private Objcet var;
public Objcet getVar(){
return var;
}
public void setVar(Object var){
this.var = var;
}
public Objcet getVar(){
return var;
}
}
这样容器的类型丢失了,在get的时候不得不在输出的地方加上类型转换。
缺点:
【1】程序员并不知道这个var具体是什么类型,而且编译器也不知道(编译器只知道是Object),因此程序员需要去看前段代码所set的值判断类型。
【2】在进行类型强转时,由于是Object类型,编译器并不能检查出类型转换是否错误,直到运行时的Cast Error才能判断出来。
所谓的类型检查,就是在边界(对象进入和离开的地方)处,检查类型是否符合某种约束,简单的来说包括:
【1】赋值语句的左右两边类型必须兼容。
【2】函数调用的实参与其形参类型必须兼容。
【3】return的表达式类型与函数定义的返回值类型必须兼容。
【4】多态类型检查,即向上转型可以直接通过,但是向下转型必须强制类型转换(前提是有继承关系)
编译器只会检查继承关系是否符合。强转本身如果有问题,在运行时才会发现。所以下面这行代码在运行期才会抛异常。
Number n = new Integer(1);
Double d = (Double)n; // 编译通过,但运行时报错java.lang.Integer cannot be cast to java.lang.Double
为什么 Java 编译器选择只认为n是Number类型,而不和运行时一样认为n是Integer类型呢?看上面第一行代码,如果编译器采用静态联编,认为n就是Integer类型,那么就检查出第二行类型转换错误了呀,从而上述的两个缺点就可以不成为问题,因为编译器能够判断Object具体指什么类型了。
上面这段代码确实可以通过上下文分析确定n的实际类型的,但是更多时候编译器并不知道n具体指向哪个引用。
比如下面的伪代码:
Number n;
if (i > 0){
n = new Integer(1);
}else {
n = new Double(1);
}
n.???(调用其中的方法)
此时编译器无法根据上下文分析出n的实际类型。现实中出现更多的情况是用配置文件控制创建不同的子类对象来改变程序行为,这种情况编译器更是没法分析的。 那么可不可以在设计编译器的时候对上下文分情况讨论,遇到这种无法确定的情况就认为是Number类型,而可以确定的就认为是具体类型呢?
那当然是可以的啦!但是太吃力不讨好了,因为程序中不单单只有if...else...需要进行这种判断,还有许多其他用法,文法分析会变得异常复杂。只能说一句未来可期。
5.2 Template模板
即C++的泛型实现方式。 C++的模板又称为,编译时多态技术 。当我们在C++中定义一个模板类之后。可以分别去声明不同类型的模板实现。 C++的模板能够实现泛型是因为它帮你最终生成了不同类型的代码。
缺点:编译后会生成大量代码,编译后的文件会非常大。但是现在的编译器应该做了优化处理了。
那么Java可不可以仿照这个来实现泛型呢?的确是可以的。但是确实太臃肿了,本来Java文件大就是诟病了。
5.3 JIT即时编译
那么Java可不可以给泛型类加上tag,在运行时用tag替换泛型形成新的代码呢?当然是可以的,这就是C#的泛型实现方式。
首先在编译时,c#会将泛型编译成元数据,即生成.net的IL Assembly代码。并在CLR运行时,通过JIT(即时编译), 将IL代码即时编译成相应类型的特化代码。
那么Java为什么没有用这种方法呢?因为这样就要修改JVM搞JIT了,当时的JAVA开发人员想偷懒就没做。况且做即时编译让代码在运行时重构,一定程序上也影响了运行效率,本来Java运行就很慢了。
5.4 类型擦除
JAVA开发人员想了一个绝妙的办法。泛型信息只在编译时有效,编译完成后,泛型信息将被擦除为Object(泛型有上界会被擦除为上界) ,而在对象进入和离开方法的边界处添加类型检查和类型转换。
比较下面的两段代码:
//1.5版本以前使用List
List list = new ArrayList();
list.add(1);
Integer i = (Integer) list.get(0);
//1.5版本之后使用List
List<Integer> list = new ArrayList<>();
list.add(1);
Integer i = list.get(0);
其实1.5版本后编译器会帮你把代码编译成1.5版本之前的样子。这样不仅没动JVM,也实现了泛型,也解决了4.1中使用Object类型的两个缺点。
当然这样也带来了很多问题:
【1】基本类型无法作为泛型具体类型
基本类型并不是Object的子类,因此在类型擦除的实现下,泛型不能接收基本类型为具体类型。这就非常影响运行效率。
【2】实例化对象问题
实例化一个对象的时候,比如T t = new T()
它的发生时机是在运行阶段,而在运行阶段,T 已经被擦除为Object,因此这段代码实际上是Objcet t = new Object()
,也就是说并不能创建一个T所具体指定的类型的对象。同时,编译器也无法验证 T 是否具有默认(无参)构造函数。
Java 中的解决方案是引入类型标签,在泛型类中添加Class类型的变量,在创建对象时传入T所指定的具体类型的class,并使用该class创建对象。
【3】不允许创建泛型数组
List<Integer>[] list
是不被允许的。
查看如下代码:
//假如允许泛型数组
List<String>[] list = new List<String>[10];
List<Integer> list1 = new ArrayList<>();
list1.add(1);
Object[] o = list;
o[0] = list1;
String s = list[0].get(0); //编译期不会发现问题,但运行时ClassCastException
【4】异常中不能使用泛型
以下代码编译无法通过
public class Problem<T> extends Exception{......}
假如上述代码可以通过编译,那么在类型擦除的情况下,所有T所指定的具体类型都会被擦除为Object,因此在try...catch...中异常类型并不能进行区分。
在泛型方法中也不能使用泛型变量类型的异常
查看如下代码:
public static <T extends Throwable> void doWork(Class<T> t){
try{
...
}catch(T e){ //编译错误
...
}catch(IndexOutOfBounds e){
...
}
}
如果允许通过,在运行时T被擦除为Throwable,会catch所有异常。
但是运用Java在泛型方法中的类型推断,确定具体类型,可以解决上述问题,比如下面的代码是合法的。
public static<T extends Throwable> void doWork(T t) throws T{
try{
...
}catch(Throwable realCause){
t.initCause(realCause);
throw t;
}
}
【5】方法重载问题
public static void add(List<Integer> list){
System.out.println(1);
}
public static void add(List<Number> list){
System.out.println(2);
}
这样的overload是不被允许的,因为实际上两个方法的签名是一样的,都是List类型。
【6】类型判断问题
List<Integer> list1 = new ArrayList<>();
List<Number> list2 = new ArrayList<>();
list1.getClass() //class java.util.ArrayList
list2.getClass() //class java.util.ArrayList
//Inconvertible types; cannot cast 'java.util.List<java.lang.Integer>' to 'java.util.List<java.lang.Number>'
if (list1 instanceof List<Number>){
System.out.println(1);
}
//Illegal generic type for instanceof
if (list2 instanceof List<Number>){
System.out.println(1);
}
//这样是可以的
if (list1 instanceof List){
System.out.println(1);
}
这个问题也可以通过引入类型标签并使用动态的isInstance()解决。
public class GenericUtils <T> {
public boolean f(Object o) {
// illegal generic type for instanceof
// 这里是在运行时判断,而T已经被擦除为Object,所以无论如何都是正确的,因此是不安全的,不被编译通过
if (o instanceof T) {
return true;
}
return false;
}
public void add(List<Number> list){
System.out.println(1);
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();、
//add (java.util.List<java.lang.Number>) in GenericUtils cannot be applied to (java.util.List<java.lang.Integer>)
new GenericUtils<>().add(list);
}
}
很难想象List<Integer>
居然不是List<Number>
的子类,毕竟List
的实现是用的数组,而Integer[]
确实是Number[]
的子类。
为了解决这个问题,JAVA引入了通配符。
6 通配符 ?
泛型是参数化类型,必须先指定具体类型后使用。而通配符可看作是具体类型,这是Java定义的一种特殊类型,它可以代表任何一种类型,比Object
更特殊。比如List<Object>
和List<String>
是没有父子关系的,但是List<?>
是 List<String>
的父类。
6.1 通配符的使用
?表示了任何的一种类型,那List<?>
岂不是可以同时包含 String 和 Integer?这又和Java的类型系统矛盾了,即List
里面只能放一种类型。因此,对于 List<?> list
,是不可以进行写操作的,除了可以 list.add(null)
,只可以进行读操作,并且返回的是Object类型 。
6.2 通配符上边界
和泛型一样,通配符也可以限制上边界。
< ? extends Number> 限制上边界为Number
List< ? extends Number>
不可以向里面添加元素(除了null)。因为编译器并不知道 ? 的具体类型,只知道上界。因此编译器并不能知道添加的元素是否合法。那可不可将添加的元素上转型为上界呢?
查看如下代码:
List<Integer> list1 = new ArrayList<>();
List<? extends Number> list = list1;
list.add(1.0); //如果可以上转型为Number,那这里就是被允许的,但很明显,List<Integer>不应允许Double类型添加。
List< ? extends Number>
可以从中读取元素,返回值类型为上界。
泛型可以在定义时限定为同时满足继承某类和实现某些接口的,例如 List<T extends Number & Interface1 & Interface2>
。这种情况下,在进行类型转换时会采用第一个边界的类型。通配符 ?不可以这样使用。
6.3 通配符下边界
通配符可以限制下边界,而泛型不可以。
< ? super Number> 限制下边界为Number
List< ? super Number>
,可以向其中添加Number及Number子类的对象。因为虽然编译器虽然不知道 ? 的具体类型,但它总是Number的父类,因此向其中添加任何Number及Number的子类对象总是可以上转型为 ? 的。
List< ? super Number>
,可以从中读取元素,返回值类型为Object。
7 反射获取泛型信息
在实践中我们会发现,通过反射我们是可以获取到泛型信息的。
查看如下代码:
public class GenericUtils<T> {
ArrayList<Integer> list;
T var;
public static void main(String[] args) throws NoSuchFieldException {
GenericUtils<String> genericUtils = new GenericUtils();
Class clazz = genericUtils.getClass();
System.out.println(clazz.getDeclaredField("list").getGenericType());
//java.util.ArrayList<java.lang.Integer>
System.out.println(clazz.getDeclaredField("var").getGenericType());
//T
System.out.println(new ArrayList<Integer>().getClass().getGenericSuperclass());
//java.util.AbstractList<E>
System.out.println(new ArrayList<Integer>(){}.getClass().getGenericSuperclass());
//java.util.ArrayList<java.lang.Integer>
}
}
Java泛型信息是否擦除有如下两种情况:
【1】声明侧泛型信息保留
【2】使用侧泛型信息擦除
在JDK1.5后Signature属性被增加到了Class文件规范中,它是一个可选的定长属性,可以出现在类、字段表和方法表结构的属性表中。在JDK1.5中大幅度增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。Signature属性就是为了弥补擦除法的缺陷而增设的,Java可以通过反射获得泛型类型,最终的数据来源也就是这个属性。
但为什么Java不使用这个Signature来实现真泛型,而采用类型擦除来实现假泛型呢。因为类型擦除避免了JVM的大修改,也保证了程序的运行效率。