泛型即参数化类型。其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦除,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
定义简单的泛型类
一个泛型类就是具有一个或者多个类型变量的类。例子:
publc class Pair<T>{
private T first;
private T second;
public Pair(){first = null; second = null;}
public Pair(T first, T second){this.first = first; this.second = second;}
public T getFirst(){return first;}
pbulic T getSecond(){return second;}
public void setFirst(T newValue){first = newValue;}
public void setSecond(T newSecond){second = newValue}
}
Pair类引入了一个类型变量T,用<>括起来,并放在类名后面。这就是泛型类的简单的定义方式。当然,泛型可以有多个类型变量,例如<K,V>。类中定义的泛型指定了使用这些泛型的方法的返回值及局部变量的类型。
Java库中,使用E表示集合的元素类型,K和V表示表的关键字及值的类型。T(需要时还可以使用临近的字母U和S)表示任意类型。
使用具体的类型替换类型变量就可以实例化泛型类。例如:
Pair<String>();
就可以将类型变量替换为String类型。
泛型方法
将泛型利用到方法上就可以定义一个泛型方法:
class ArrayAlg{
public static <T> T getMiddle(T... a){
return a[a.length / 2];
}
}
注意将类型变量放到修饰符的后面,返回类型的前面。当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型。当然,如果参数不存在类型转换问题,编译器有足够的信息可以推断出参数类型,则可以省略。
类型变量的限定
<T extends BoundingType>
表示T应该是绑定类型或其子类型。T和绑定类型可以是类,也可以是接口。一个类型或通配符可以有多个限定,限定类型用&分隔,而逗号用来分隔类型变量。例如
<T extends Comparable & Serializable>
限定中至多有一个类,且必须是限定表中的第一个。
泛型代码和虚拟机
虚拟机没有泛型类型的对象--所有对象都属于普通类。
类型擦除
无论何时定义一个泛型,都自动提供一个原始类型。原始类型的名字就是删去类型参数后的泛型类名。例如Pair<String>或Pair<Integer>擦除类型后就变成原始的Pair类型了。
类型变量用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。例如,Pair<T>中的类型没有显示限定,就是用Obejct替换T。如果声明了一个不同类型,如<T extends Comparable $ Serializable>就用Comparable替换T。
翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器会强制类型转换。例如下面的语句:
Pair<Employee> buddies = ...;
Employee emp = buddies.getFirst();
擦除getFirst的返回类型后会返回Object类型。编译器会把这个方法调用翻译为两条虚拟机指令:
1、对原始方法Pair.getFirst的调用
2、将返回的Object类型强制转换为Employee类型。
同理,在存取一个泛型域的时候也要插入强制类型转换。
桥方法
泛型擦除也会出现在泛型方法中。但是,泛型擦除后,有可能会出现奇怪的现象。例如:
假设有一个超类:
public class Parent<T>
{
public void sayHi(T value)
{
System.out.println(" Parent Class, value : " + value);
}
}
有一个子类:
public class Child extends Parent<String>
{
public void sayHi(String value)
{
System.out.println(" Child class, value : " + value);
}
}
最后有以下测试代码,企图实现多态:
public class TestDemo
{
public static void main(String[] args)
{
Child child = new Child();
Parent<String> parent = child;
parent.sayHi("hi");
}
}
运行的时候,会对Child类的方法表进行搜索,先分析一下Child类的方法表里有哪些东西:
1. sayHi(Object value) : 从超类中继承过来
2. sayHi(String value) : 自己新增的方法
......
按理来说,这段测试代码应该不能通过编译,因为要实现多态的话,所调用的方法必须在子类中重写,但是在这里Child类并没有重写Parent类中的sayHello(Object value)方法,只是单纯的继承而已,并且新加了一个参数不同的同名方法。
但是结果是可以正常运行。
原因是编译器在Child类中自动生成了一个桥方法:
public void sayHi(Object value)
{
sayHi((String) value);
}
可以看出,这个桥方法实际上就是对超类中sayHi(Obejct)的重写。这样做的原因是,当程序员在子类中写下以下这段代码的时候,本意是对超类中的同名方法进行重写,但因为超类发生了类型擦除,所以实际上并没有重写成功,因此加入了桥方法的机制来避免类型擦除与多态发生冲突。
public class Child extends Parent<String>
{
public void sayHi(String value)
{
System.out.println("Child class, value : " + value);
}
}
桥方法并不需要自己手动生成,一切都是编译器自动完成的。
还有一个问题需要注意,我们看一下下面的例子:
public class Pair<T>{
private T first;
private T second;
public Pair(T first,T second){
this.first = first;
this.second = second;
}
}
在这个时候,当我们实例化一个Pair类的时候,一定要注意传入的两个参数要类型统一。如果类型不统一的话,会出现T因为第一个参数已经确定了类型,而第二个参数因为类型不一致会报错(其实这个问题细心点不会出现,可是刚开始学Java的时候因为本人大意,在这个问题上耗了半天,所以写这一段,免得大家跟我掉一个坑里😓)。
总之,需要记住有关Java泛型转换的事实:
• 虚拟机中没有泛型,只有普通类和方法;
• 所有的类型参数都用他们的第一限定类型替换,没有就默认Object;
• 桥方法被编译器自动生成来保持多态;
• 为了保持类型安全,必要时插入强制类型转换。
约束与局限性
不能用基本类型实例化参数类型
Pair<T>泛型擦除后含有Object类型的域,而Object不能存储基本类型
运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
不能创建参数化的类型数组
泛型擦除后,数组类型会是原始类型。插入其他类型的元素会报错。
不能实例化类型变量
不能使用像new T(),new T[] 或者T.class这样的表达式中的类型变量。因为类型擦除后T改变成Object。在Java SE 8 之后,最好的解决办法就是让调用者提供一个构造器表达式。
不能构造泛型数组
泛型擦除。
泛型类的静态上下文中的类型变量无效
泛型擦除
不能抛出或捕获泛型类的实域
可以消除对检查异常的检查
注意擦除后的冲突
泛型类型的继承规则
我们看一段代码:
Pair<Child> child = new Pair<>();
Pair<Parent> parent = child;//报错
虽然Child是Parent的子类,但是Pair<T>之间没有继承关系。举个栗子🌰:
List<Object> obj = new ArrayList<>();
List<String> str = new ArrayList<>();
obj.add(123)是正确的,如果obj可以转换成str(obj = str),那么str.get(0)应该也没问题,可是却报错了。因为obj添加Integer类型的值进入了str的集合中,但是str是String类型,所以会报错。因此,泛型不存在此种继承关系
2、可以将参数化类型转换成一个原始类型:
List<String> str = new ArrayList<>();
List obj = str;
obj.add(123);入String中
显然是错误的;
代码中obj是原始的数据类型,所以obj.add(123)没有问题。但是这个操作同上面的结果一样,都是将一个Integer类型的数据存入String中,所以,也是不对的。
3、泛型类可以扩展或实现其他的泛型类:
//泛型接口
interface MyTestImpl<E>{
}
//实现了泛型接口的泛型类
class MyTest<U, E> implements MyTestImpl <E>{
}
//泛型类
class Father<T>{
}
//继承了其他泛型类的泛型类
class Son<U, E> extends List3<E>{
}
MyTestImpl <Parent> parent = new MyTest <Child, Parent>();//因为MyTest实现了MyTestImpl,所以可以通过MyTest实例化一个MyTestImpl的对象
Father< Parent > father = new Son < Child, Parent >();//Son继承了Father,所以父类引用可以指向子类对象。
通配符类型
通配符概念
通配符类型中,允许类型参数化。例如,通配符类型
Pair<? extends Employee>
表示任何泛型Pair类型,它的类型参数是Employee的子类。
需要注意的是:
/*
* 使用通配符的上限的问题:
* ? extends Father getFirst();
* void setFirst(? extends Father);
* 当c2= c1时:
* c2.setFirst(Father father);时,会将Father对象添加到Son对象内存中,这是不好的
* 所以使用extends上限时,不能使用setFirst(? extends Father),add(? extends Father)* 等方法。
* 但可以使用getFirst();方法
*/
c2.setFirst( new Father(); );//error
通配符的超类型限定
通配符限定与类型变量限定十分相似,但还有一个附加的能力,即可以指定一个超类型限定
? super XXX
这个通配符限制为XXX的所有超类型即父类。这个和上面的相反,就是可以为方法提供参数,但是不能使用返回值。
//另一种超类型限定的写法
Pair<T extends Comparable<? super T>> c4;
无限定通配符
还可以使用无限定的通配符,例如,Pair<?>。
Pair<Child> child = new Pair<>();
Pair<String> str = new Pair<>();
child = str;//报错,因为他们不是同一种类型
Pair<?> obj = new Pair<>();
obj = str//Pair<?>是所有的Pair泛型类的父类
Pair<?>和Pair的本质不同在于:可以用任意的Object对象调用原始的Pair类的setObject()方法。
通配符捕获
//交换First,Second变量值
public static void swap(Pair<?> p){
? t = p.getFirst();//error,因为通配符(?)不是类型变量,所以不能直接将?写入代码中,利用通配符的捕获来解决这个问题。
p.setFirst(p.getSecond());
p.setSecond(t);
}
//交换First,Second变量值
public static void swap(Pair<?> p){
swapHelper(p);//在调用下面的方法时,类型参数就被捕获了。
}
//利用通配符的捕获来解决该问题
public static <T> void swapHelper(Pair<T> p){
T t = p.getFirst();//T是具体的某个类型。
p.setFirst(p.getSecond());
p.setSecond(t);
}
注意:通配符的捕获只有在许多限制的情况下才是合法的,编译器必须能够确信通配符表达的是单个,确定的类型。
反射和泛型
反射允许在运行时分析任意的对象。如果对象是泛型类的实例,关于泛型类型的参数则得不到太多的信息,因为他们会被擦除。
泛型Class类
现在Class类是泛型的。例如String.class实际上是一个Class<String>类的对象(事实上,是唯一的对象)。
类型参数使得Class<T>方法的返回类型更加有针对性。下面Class<T>中的方法就使用了类型参数:
T newInstance()
T casr(Object obj)
T[] getEnumConstants()
Class<? super T> getSuperclass()
Constructor<T> getConstructor(Class...parameterTypes)
Constructor<T> getDeclaredConstrucor(Class...parameterTypes)
newInstance返回一个实例,这个实例所属的类有默认的构造器获得。他的返回类型为被声明为T,其类型与Class<T>描述的类相同,这样就免除了类型转换。
如果给定的类型确实是 T 的一个子类型,cast 方法就会返回一个现在声明为类型T的对象, 否则,抛出一个 BadCastException 异常。
如果这个类不是 enum 类或类型 T 的枚举值的数组, getEnumConstants 方法将返回 null。
最后, getConstructor 与 getdeclaredConstructor 方 法 返 回 一 个 Constructor<T> 对象。Constructor 类也已经变成泛型, 以便 newlnstance 方法有一个正确的返回类型。
使用Class<T>参数进行类型匹配
有时, 匹配泛型方法中的 Class<I> 参数的类型变量很有实用价值。下面是一 标准的示例:
public static <T> Pai r<T> makePair(Class<T> c) throws InstantiationException,
IllegalAccessException
{
return new Pair(c.newInstance(), c.newInstanceO » c.newInstance());
}
如果调用
makePair(Employee.class)
Employee.class 是类型 Class<Employee> 的一个对象。makePair 方法的类型参数 T 同 Employee匹配, 并且编译器可以推断出这个方法将返回一个 Pair<Employee>。
虚拟机中的泛型类型信息
Java泛型擦除后,擦除的类仍保留一些泛型祖先的信息。例如原始的Pair类知道源于泛型类Pair<T>。类似的看一下方法
public static Comparable min(Comparable[] a)
这是一个泛型方法的擦除:
public static <T extend Comparable<? super T>> T min(T[] a)
可以使用反射API来确定:
这个泛型方法有一个叫T的类型参数;
这个类型参数有一个子类型限定,其自身又是一个泛型类型;
这个限定类型有一个通配符参数;
这个通配符参数有一个超类型限定;
这个泛型方法有一个泛型数组参数。
换句话说,需要重新构造实现者声明的泛型类型以及方法中的所有内容。但是,不会知道对于特定的对象或方法的调用,如何解释类型参数。
为了表达泛型类型声明,使用java.lang.reflect包中提供的接口Type。这个接口包含下列子类型:
Class类,描述具体类型;
TypeVariable接口,描述类型变量(如 T extends Comparable<? super T>);
WildcardType接口,描述通配符(如 ? super T);
ParameterizedType接口,描述泛型类或接口类型(如 Comparable<? super T>);
GenericArrayType接口,描述泛型数组(如 T[])。
最后4个子类型是接口,虚拟机将实例化实现这些接口的适当的类