1.泛型的由来
Java泛型是JDK5中引入的新特性,提供了一种编译时安全检测机制,允许在定义类/接口/方法的时候使用类型参数,声明的类型参数在使用时用具体的类型来替换,允许程序员在编译时检测到非法类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
2.泛型的意义
在没有泛型之前,是要做显式的强制类型转换。针对不同参数得写出几个对应的方法。使用泛型好处是让编译器保留参数类型信息,执行类型检查,执行类型转换操作,同时省掉了显式的强制类型转换,保证了这些类型转换的正确性,保证类型安全,并实现了更为通用的算法。在编译时就能发现插入类型的错误,也不需要手动转换类型。
3.泛型的种类
泛型类
泛型类声明和非泛型类的声明类似,除了在类名后面添加类型参数声明部分。泛型类的类型参数声明包含一个或多个类型参数,参数之间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符,因为他们接受一个或者多个参数,这些类被称为参数化的类或者参数化的类型。
public class GenericTest{
public static void main(String[] args) {
Box name = new Box("corn");
Box age = new Box(712);
System.out.println("name class:" + name.getClass());
System.out.println("age class:" + age.getClass());
System.out.println(name.getClass() == age.getClass());
}
static class Box {
private T data;
public Box() {}
public Box(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
}
在使用泛型类时候,虽然传入了不同的泛型实参,但没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存中只有一个,即为原来的最初类型,在逻辑中可以理解为多个不同的泛型类型。究其原因是泛型只是作用于代码编译阶段,在编译过程中正确检测出了泛型信息,会将泛型的相关信息擦除,即成功编译后的class文件不包含任何泛型信息,泛型信息不会进入运行时阶段。
泛型接口
在接口处使用泛型,使用比较方便
public class GenericTest{
public static void main(String args[]) {
generic i = null;
i = new genericImpl<>("it");
System.out.println("Length Of String : " + i.getVar().length());
}
}
interface generic {
T getVar();
}
class genericImpl implements generic {
private T var;
public genericImpl(T var) {
this.setVar(var);
}
public void setVar(T var) {
this.var = var;
}
public T getVar() {
return this.var;
}
}
泛型方法
一个基本原则是无论何时,只要你能做到,你就应该尽量使用泛型方法。
public class GenericTest{
public static void main(String args[]) {
out("findingsea");
out(123);
out(11.11);
out(true);
}
public static void out(T t) {
System.out.println(t);
}
}
可以看到方法的参数彻底泛化了,原来需要自己对类型进行判断和处理,现在交给编译器来做了。这样在定义方法的时候,大大增加了编程的灵活性。
4.类型擦除
类型擦除指的是通过类型参数合并,编译器只为泛型类型生成一份字节码,将泛型类型实例关联到同一份字节码上。在生成的Java字节码中不包含泛型类型信息的,类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且在必要的时候添加类型检查和类型转换的方法。具体过程为:
1.检查代码中的泛型类型
2.将所有泛型参数用它们的原始类型替换
3.编译代码
需要注意的是:泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的,但是类型信息已经被擦除了。泛型类并没有自己独有的Class类对象。静态变量是被泛型类的所有实例所共享的。
List list = new ArrayList();
Map map = new HashMap();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
/* Output
[E]
[K, V]
*/
这种代码的输出,我们期待
是得到泛型参数的类型,但是实际上我们只得到了一堆占位符。
public class Main {
public T[] makeArray() {
// error: Type parameter 'T' cannot be instantiated directly
return new T[5];
}
}
这种出现错误是因为我们无法在泛型内部创建一个T类型的数组,因为T仅仅是个占位符,并没有真正的类型信息,实际上除了new表达式外,instanceof操作和转型是在泛型内部无法使用的,而这个原因是对编译器对类型信息进行了擦除。
public class Main {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Main m = new Main();
m.set("findingsea");
String s = m.get();
System.out.println(s);
}
}
/* Output
findingsea
*/
虽然编译器无法知道T的类型信息,但是编译器可以保证的是:内部的一致性。
class GenericTest
public List fillList(T t, int size) {
List list = new ArrayList();
for (int i = 0; i < size; i++) {
list.add(t);
}
return list;
}
public static void main(String[] args) throws Exception{
GenericTest m = new GenericTest();
List list1 = m.fillList("findingsea", 5);
System.out.println(list1.toString());
}
}
输出为[findingsea, findingsea, findingsea, findingsea, findingsea],同样保证了一致性。
5.类型擦除带来的问题以及解决
public static void main(String[] args) {
ArrayList arrayList=new ArrayList();
arrayList.add("123");
arrayList.add(123);//编译错误
}
泛型变量Integer会被擦除为Object,为什么ArrayList不能存别的类型,在类型擦除的情况下,如何保证我们加入的是泛型变量指定的类型?因为Java编译器是先检查代码中泛型的类型,再进行类型擦除,然后进行编译。
泛型中的引用传递问题:
ArrayList arrayList1=new ArrayList();
arrayList1.add("1");//编译通过
arrayList1.add(1);//编译错误
String str1=arrayList1.get(0);//返回类型就是String
ArrayList arrayList2=new ArrayList();
arrayList2.add("1");//编译通过
arrayList2.add(1);//编译通过
Object object=arrayList2.get(0);//返回类型就是Object
类型检查是针对引用的,谁是引用,就对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
ArrayList
以上两种情况都会出错,因为泛型就是为了解决类型转换问题,但是这种强转违背了泛型设计的初衷,所以在Java中是禁止这种行为的。
6.通配符
类型通配符一般是使用?来代替具体的类型实参。这个地方是类型实参,而不是类型形参。通配符有三种。
无限定通配符:形式 >
public class GenericTest{
public static void main(String[] args) throws Exception{
List listInteger =new ArrayList();
List listString =new ArrayList();
printCollection(listInteger);
printCollection(listString);
}
public static void printCollection(Collection> collection){
for(Object obj:collection){
System.out.println(obj);
}
}
}
上述代码中,如果Collection>中是Object,会出现编译失败的情况。使用?通配符可以引用其他各种参数化的类型,通配符定义的变量的主要用作引用,可以调用与参数化无关的方法,比如Collection.size()。不能调用与参数化有关的方法,比如Collection.add(),因为程序调用这个方法时候传入的参数不知道是什么类型的。
通配符上限:
Vector extends 类型1> x = new Vector<类型2>();
类型1指定一个数据类型,那么类型2就只能是类型1或者是类型1的子类。例:Vector extends Number> x = new Vector
通配符下限:
Vector super 类型1> x = new Vector<类型2>();
类型1指定一个数据类型,那么类型2就只能是类型1或者是类型1的父类。例:Vector super Integer> x = new Vector
通配符使用总结:
如果你想从一个数据类型中获取数据,使用? extends,如果你想把对象写进一个数据结构里,使用? super通配符。
7.当泛型遇上重载
public static void method(List list) {
System.out.println("invoke method(List list)");
}
public static void method(List list) {
System.out.println("invoke method(List list)");
}
这段代码无法编译,因为类型擦除使得两个方法特征签名变得一模一样。
class Pair {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
//子类继承
class DateInter extends Pair {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
public static void main(String[] args) throws ClassNotFoundException {
DateInter dateInter=new DateInter();
dateInter.setValue(new Date());
dateInter.setValue(new Object());//编译错误
}
如果是重载,那么子类的两个重载方法,一个是Object类型,一个是Date类型。但是没有出现这样子类继承父类的Object类型参数的方法。所以这种情况下是重写而不是重载。
原因是:虚拟机不能将泛型类型变成Date,只能类型擦除,将类型变为原始类型Object。
8.JVM如何理解泛型类
class SonBox extends Box{
public void setData(String data){....}
}
JVM并不知道泛型,所有的泛型在编译阶段就已经处理成了普通类和方法。无论我们如何定义一个泛型类型,相应的都会有一个原始类型被自动提供。原始类型的名字就是擦除类型参数的泛型类型的名字。
如果泛型类型的类型变量没有限定(
如果有限定(
如果有多个限定(
继承泛型类的多态麻烦,本来想覆盖父类中setData这个方法,但是事实上Box
此时编译器会在调用一个桥方法,bridge method,而且桥方法调用的实际是子类字节setData(String)方法,即多态中方法覆盖是可以的。即JVM利用很巧妙的方法来避免了类型擦除和多态之间的冲突。
引入桥方法,是编译器自动生成,而不是需要程序员自己去写代码的。但是另一个问题是,在getData中自己定义的为String getData(),编译器的桥方法为Object getData(),这两个方法的签名是一样的,编译器需要怎么去区别?
方法签名只有方法名加上参数列表,我们绝对不能编写出方法签名相同的一个方法,JVM会用参数类型和返回类型来确定一个方法。 一旦编译器通过某种方式自己编译出方法签名一样的两个方法(只能编译器自己来创造这种奇迹,我们程序员却不能人为的编写这种代码)。JVM还是能够分清楚这些方法的,前提是需要返回类型不一样。
在JVM中不存在泛型,只有普通类和方法,而在编译阶段,所有泛型类的类型参数都会被Object或者限定的边界来替换。在继承泛型类型的时候,桥的生成是为了避免类型擦除带来的多态问题。
9.泛型类型中的方法冲突
public class Box{
public boolean equals(T value){
return (data.equals(value));
}
}
上述代码会报错,编译器显示方法冲突。子类方法要覆盖父类的方法必须要和父类方法有同样的签名,即方法名加上参数列表,必须保证子类访问权限大于父类访问权限,编译器发现equals方法,第一反应是没有覆盖父类Object的equals方法,如果编译器将这个方法覆盖的话,equals(T)变为equals(Object),基于开始没有确定覆写这个方法,编译器会出错。
10.注意事项
1.Java中没有泛型数组的说法;
Box[] stringBoxes=new Box[10];
Box[] intBoxes=new Box[10];
这种方法会指定编译器错误。假设泛型存在,就会有
Object[0]=stringBoxes[0]; Ok
Object[1]=intBoxes[0]; Ok
每次调用Object[]的元素都可能得到不同的结果,也许是字符串,也许是整型,这个是JVM无法预料的结果。即数组必须要牢记元素类型,但是泛型做不到这点。
还是因为类型擦除,对于泛型而言,擦除降低了效率。如果要收集参数化类型对象,可以直接使用ArrayList。
2.一个类不能实现同一个泛型接口的两种变体,因为类型擦除会使得两个变体变成相同接口。
3.泛型通配符和自定义T类型的区别:
4.当泛型中包含静态变量
public class StaticTest{
public static void main(String[] args){
GT gti = new GT();
gti.var=1;
GT gts = new GT();
gts.var=2;
System.out.println(gti.var);
}
}
class GT{
public static int var=0;
public void nothing(T x){}
}
这个答案是2,因为经过类型擦除,所有泛型实例都关联到一份字节码上,泛型类所有静态变量共享。
5.不能在catch中使用泛型变量
public static void doWork(Class t){
try{
...
}catch(T e){ //编译错误
...
}
}
因为泛型信息在编译的时候已经变成了原始类型,也就是说上面的T会变成原始的Throwable。
public static void doWork(Class t){
try{
...
}catch(T e){ //编译错误
...
}catch(IndexOutOfBounds e){
}
}
这种情况也会出错,因为类型捕获一定要子类在前,父类在后。Java为了避免异常捕获原则,禁止在catch字句中使用泛型变量。