泛型是jdk1.5开始引入的特性。泛型既是参数化类型,理解为将参数的类型作为参数。泛型可以作用在类、接口和方法上。分别称为泛型类、泛型接口和泛型方法。使用泛型的好处有:1、适用于多种数据类型执行相同的代码;2、获得编译期的类型安全,以及运行时更小的抛出ClassCsstException的可能。
“适用于多种数据类型”,即是参数类型指定为泛型,在实际使用中根据需要传入具体的类型。例如下面代码:
// 定义一个泛型方法,接收T类型参数,T既是泛型
public void method (T param1, T param2) {
// ...
}
public void test() {
method(0.1f, 0.2f); // 参数是float类型
method("aaa", "bbb"); // 参数是double类型
method(1, 2); // 参数是int类型
}
关于“ “获得编译时的类型安全””。在Java语言处于还没有出现泛型时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。只有程序员和程序运行期才知道这个Object到底是什么类型。导致异常的风险转嫁到运行期。
例如定义一个List集合,向其中分别加入Interger类型和String类型。在使用时,可能会由于忘记之前加入的值的类型,导致使用类型错误,而在运行期抛出“ClassCastException”。使用泛型,则可以在编译期就能确定是否正确,从而获得运行期的安全。
// 定义一个存放String类型的List
private List myList = new ArrayList<>();
// 该List只能存放String类型,如果存放其他类型,在编译器会报错
public void set() {
myList.add("aa");
myList.add("bb");
}
// 从容器中取出来的一定是String类型,无需进行强制类型转换
public void get(int index) {
String str = myList.get(index);
}
泛型类的定义,引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等),用<>括起来,放在类名的后面。泛型类允许有多个类型变量。
public class Generator {
private T data;
private E value;
public void method(T data, E value) {
this.data = data;
this.value = value;
}
}
泛型接口与泛型类定义类似。泛型接口的实现类可以选择指定具体类型也可以选择不指定。
// 定义泛型接口
public interface IGenerator {
public T method();
}
// 实现类实现泛型接口,并指定泛型类型
// 这种方式,在创建该类型对象时和普通类没有区别
public class Generator1 implements IGenerator {
@Override
public String method() {
return null;
}
}
Generator1 generator1 = new Generator1();
// 实现类实现泛型接口,但不指定泛型类型
// 这种方式在创建具体对象时,需要指定具体的类型
public class Generator2 implements IGenerator {
@Override
public T method() {
return null;
}
}
Generator2 generator2 = new Generator2<>();
泛型方法的定义,是将类型变量放在方法的访问修饰符和返回类型之间。泛型方法可以定义在普通类或者泛型类中。它是在调用方法的时候指明具体的类型。
在泛型类中用类的泛型作为参数或返回值的方法,并不是泛型方法,认为是普通方法。只有按照泛型方法定义的才是泛型方法。泛型方法的类型变量也允许有多个。
public class Generic {
// 这只是泛型类的普通方法
public void gMethod(T data) {
this.data = data;
}
}
// 这才是一个泛型方法,可以在普通类和泛型类中定义
public void gereratorMethod(T param) {
// ...
}
// 调用泛型方法,在调用方法时明确具体的类型
public void test1() {
gereratorMethod("hello");
gereratorMethod(123);
}
限定类型变量,通常用于需要对类型变量进行约束的情形。如,泛型方法中,泛型类对象需要具有比较的功能。则将泛型T限制为需要实现带有比较方法接口的类。
public void test(T a, T b) {
if (a.compareTo(b) > 0) {
// ...
}
}
public interface Comparable {
public int compareTo(T o);
}
限定类型变量需要用extends关键字实现。“T extends Comparable”,T表示应该绑定类型的子类型,Comparable表示绑定类型,子类型和绑定类型可以是类也可以是接口。经过限定类型修饰后,我们如果试图传入一个没有实现接口Comparable的类的实例,就会发生编译错误。
子类型和绑定类型(即在extends左右两边)都允许有多个,例如“T,V extends Comparable & Serializable”。在extends右边,即绑定类型,最多只能有一个类类型,并且有类型类的话,必须放在第一个位置,如“T extends MyClass & MyInterface1 & MyInterface2 ”。
通配符,适用于用存在继承关系的类具现化泛型的情况。如用存在继承关系的两个类对象,分别实例化泛型,则产生的泛型对象是完全不同的类,如下面的代码。如果需要在下面funtion函数中,也可以接收子类具现话的泛型,则要用到通配符。
// 定义泛型类
public class Generator {}
// Apple和Fruite存在继承关系
public class Apple extends Fruit {}
// 需要一个Generator类型的参数
public void function(Generator gFruit) {}
public void method() {
// 具现化泛型的类存在继承关系
// 而Generator 和 Generator是完全不同的类型
Generator fruit = new Generator<>();
Generator apple = new Generator<>();
// function方法可以接收fruit而不能接收apple
function(fruit);
}
上面代码function函数做一下修改,增加接收通配符类型的参数“? extends Fruit”。 则现在它也可以接收由子类具现化泛型的实例。
public void method() {
Generator fruit = new Generator<>();
Generator apple = new Generator<>();
// function方法可以接收fruit和apple
function(apple);
function(fruit);
}
public void function(Generator extends Fruit> gFruit) {}
"? extends X" 是上界通配符,表示类型的上界,传递的类型参数必须是X的子类或本身;同时还有下界通配符“? super X”,表示类型的下界,传递的类型参数必须是X的超类或本身。下界通配符使用和上界通配符类似。
上界通配符,通常用于安全的访问数据,即获取元素时使用,因为编译器可以确定获取到的基类类型;而下界通配符适用于安全的写入数据,即设置元素时使用,因为写入的数据是X或者是X的超类,X可以安全的转换为其超类。
无限通配符,只用一个“?”表示,认为对类型没有限制,可以看成所有类型的父类。如“ArrayList> al=new ArrayList>()”表示集合元素可以是任意类型。这样修饰的容器没有实际的意义,获取元素只能是Object类型;无法设置元素,Object类型的元素也不行。
1、不能用基本数据类型实例化泛型,如“Generator
2、不能创建泛型数组,如“Generator
3、泛型类型变量不能直接被实例化,如 "T t = new T()"是不允许的;
4、不能直接捕获泛型类型,如“try { } catch (T t) { }”是不允许的;
5、不能用于运行时类型查询,如“if(apple instanceof Generator
6、静态域或方法中不能引用泛型类型(泛型方法可以是泛型方法)。这是因为泛型是要在对象创建的时候才能确定类型,而静态的内容在对象创建之前确定,此时虚拟机无法识别。
public class Generic {
// 静态域中不能用泛型
// private static T data;
// 静态方法中不能用泛型
// public static void method(T data) {}
// 可以定义泛型静态方法
public static void sMethod(E data) {}
}
泛型思想早在C++语言的模板(Template)中出现。但是泛型技术在C++和Java在实现上有着本质的不同。
C++的泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言),或是运行期的CLR中,都是切实存在的。如,List<int>与List<String>就是两个不同的类型,它们在系统运行期生成,有自己的类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。
Java语言中的泛型只存在程序源码中,在编译的字节码文件中,就已经替换为了原生类型(Raw Type,也称为裸类型),并在相应的位置插入了强制转型的代码。在运行期,ArrayList<int>与ArrayList<String>就是相同的类型 。Java语言的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
由于Java引入泛型,需要在多种应用场景(如,反射)下的使用,而产生新的需求。如,在泛型类中获取传入的参数化类型。因此,JCP组织针对泛型修改了虚拟机规范,引入了如Signature、LocalVariableTypeTable等属性用于解决泛型带来的参数类型识别问题。如Signature用来存储方法在字节码层面的特征签名,该属性中保存了泛型的信息。新的虚拟机规范要求能够识别49.0以上版本的Class文件的虚拟机都能够支持Signature参数。
因此,从Signature属性上说,java泛型擦除,是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的依据。