在前面的章节中我们介绍了擦除机制.它运用于泛型,泛型对编程有很多限制,在第一章中也有所介绍.
借助Thinking in Java这本书,我们将考虑java代码遇到这些限制时,应做出何种补偿.或者说,在一般情况下,java开发者是如何实现泛型中不允许实现的操作的?
在Java核心技术卷1中,对于泛型的限制和局限性,书中提到这两点:
运行时类型查询只适用于原始类型
不能实例化类型变量
而在Thinking in Java中,直接给出了这样一个例子说明这一点.
package 泛型演示.擦除特性;
public class Erased<T> {
private final int SIZE = 100;
public Class<T> kind;
public Erased(Class<T> kind){
this.kind = kind;
}
public void f(Object arg){
if(arg instanceof T){
System.out.println("good");//Error
}
T var = new T();//Error
T[] array = new T[SIZE];//Error
T[] array2 = (T)new Object[SIZE];//Error
}
public static void main(String[] args) {
Erased erased = new Erased(String.class);
Object obj = new Object();
erased.f(obj);
}
}
声明一个Erased类(擦除),则上面注释的这几行全都会出现编译失败的问题.这便是我们错误地对泛型类型使用了类型查询以及错误地实例化一个泛型类型的对象.
对泛型变量使用类型判断
当我们需要判断传入的对象类型是否是某个类型的时候,通常我们将声明一个类型变量在一个类中,并将这个类型变量传入这个类的构造方法.判断一个对象arg是否是调用者对象的类型的方法是下面的f():
package 泛型演示.擦除特性;
class Building{}
class House extends Building{}
public class ClassTypeCapture<T> {
Class<T> kind;
public ClassTypeCapture(Class<T> kind){
this.kind = kind;
}
public boolean f(Object arg){
return kind.isInstance(arg);
//return arg instanceof kind; Error
}
public static void main(String[] args) {
ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);
System.out.println(ctt1.f(new Building()));
System.out.println(ctt1.f(new House()));
ClassTypeCapture<House> ctt2 = new ClassTypeCapture<>(House.class);
System.out.println(ctt2.f(new Building()));
System.out.println(ctt2.f(new House()));
}
}
注意到我们用Class类的isInstance()代替运算符instanceof.instanceof的用法是对象 instanceof 类名.这对于类名是泛型类型T是没有用的,因为instanceof要求类名是一个确切的类型.
但是isInstance()却可以解决这个问题,这是一个动态方法,类是一个变量,编译不会出问题是因为这个方法允许调用者和方法内的参数都是变量,而在运行的时候,由于传入了实参,也没有收到泛型擦除的影响.
擦除的只是
<T>
部分,jvm将它们替换成Object了,这时候对于f()方法,并没有任何影响,以
System.out.println(ctt1.f(new Building()));
为例,jvm会返回Building.class.isInstance(new Building())
,这显然没有受到任何擦除的影响.这不是偶然的,我们将涉及泛型变量T的部分限制在成员方法以外.
创建类型实例
接下来我们考虑如何在java中取代创建类型实例.
在Erased.java中对创建一个new T()的尝试将无法实现,部分原因是因为擦除,而另一部分原因是因为编译器不能验证T具有默认(无参)构造器.Java的解决方案是传递一个工厂对象,并使用它来创建新的实例.来看下面的这个例子:
package 泛型演示.擦除特性.创建泛型实例;
//虽然会在运行期间抛出异常,但是编译期间无法捕获
public class InstantiateGenericType {
public static void main(String[] args) {
ClassAsFactory<Employee> fe = new ClassAsFactory<>(Employee.class);
System.out.println("ClassAsFactory<Employee> succeeded");
try{
ClassAsFactory<Integer> fi = new ClassAsFactory<>(Integer.class);
}catch(Exception e){
System.out.println("ClassAsFactory<Integer> failed");
}
}
}
class ClassAsFactory<T>{
T x;
public ClassAsFactory(Class<T> kind){
try{
x = kind.newInstance();//用来创建一个T类型对象
}catch(Exception e){
throw new RuntimeException(e);
}
}
}
class Employee{}
newInstance()作用和new关键字一样,都是用来创建对象不过局限性很大,它只能创建无参对象,并且它是类对象的方法,需要一个类对象去调用,而new关键字不需要.
我们注意到主方法中先是实例化了一个Employee类型的ClassAsFactory对象.而事实上是构造了一个Employee对象.因为Employee默认有无参构造,所以能够构造出来.所以它会输出
ClassAsFactory< Employee > succeeded
字符串.但是后面的实例化就有问题了,因为Integer类没有无参构造,所以是不会通过newInstance()构造出来的,所以会抛出异常,而这里捕获了这个异常.它的输出如下:
ClassAsFactory<Employee> succeeded
ClassAsFactory<Integer> failed
可以看出这个工厂类是一个包装,它封装了泛型对象实例化的过程,将要真正创建的泛型对象作为参数传递进去.同时避免使用了new T()的写法.但是,正如所见,newInstance()方法的局限性很大.并且这种异常只在运行的时候才能捕获,所以还并不出色.
如果我们将工厂类抽象成一个接口,定义一个创建泛型实例的方法.当我们想要构造一个具体类型的对象时,声明一个具体类型的工厂子类实现工厂类.同时,为了弥补instance()不能构造有参对象的缺点,改用new关键字构造之.
package 泛型演示.擦除特性.创建泛型实例;
public class FactoryConstraint {
public static void main(String[] args) {
new Foo2<Integer>(new IntegerFactory());
new Foo2<Widget>(new Widget.Factory());
// new Foo2<String>(new StringFactory());//编译器不通过,因为类型不对
}
}
interface FactoryI<T>{
T create();
}
class Foo2<T>{
private T x;
public <F extends FactoryI<T>> Foo2(F factory){
x = factory.create();
}
}
class IntegerFactory implements FactoryI<Integer>{
public Integer create(){
return new Integer(0);
}
}
class StringFactory{
public String create(){
return new String("douge");
}
}
class Widget{
public static class Factory implements FactoryI<Widget>{
public Widget create(){
return new Widget();
}
}
}
注意比较FactoryConstraint.java和InstantiateGenericType.java的差别.首先便是虽然它们都是通过实例化一个工厂类对象间接实例化实参类型的对象的.但不同的是前者传递了一个具体的对象,而后者传递了一个Class对象,或者说类型.前者是静态的,后者是动态的.所以后者可以通过newInstance()方法构造而不会报错.但对前者却不行,new要求后面的必须得是一个具体的类名().虽然前者的方式对于后者来说,是麻烦了些,因为你需要实现知道要构造哪些对象,并一一声明对应对象的工厂类,但是却能提前捕获异常,并且也能构造有参对象.
泛型数组
正如你在Erased.java中所见,不能创建泛型数组.接下来讨论如果我们想要构造一个具有泛型类型的数组,java有什么方法.
考虑我们有这样一个类.
class Generic<T>{}
如果我们要实例化一个Generic 的数组,比如
static Generic<Integer>[] gia;
编译器将接受这个程序,而不会产生任何警告.但是,永远都不能创建这个确切类型的数组.来看下面的例子:
public class ArrayOfGeneric {
static final int SIZE = 100;
static Generic<Integer>[] gia;
@SuppressWarnings("unchecked")
public static void main(String[] args) {
gia = (Generic<Integer>[]) new Object[SIZE];
System.out.println(gia.getClass().getSimpleName());
gia[0] = new Generic<Integer>();
gia[1] = new Object();
gia[2] = new Generic<Double>();
}
}
class Generic<T>{}
看起来我似乎可以new一个Object类型的数组,再做强制类型转换,使得这个数组只能存放Generic <Integer>
类型的数据.
从编译情况来看,似乎情况一切都好,并且第11行和第12行编译不通过也是正常的.但事实上,在运行时(先注释掉编译不通过的行再运行),仍会抛出异常ClassCastException:
图片中的第9行对应上面代码的第6行,也就是
gia = (Generic<Integer>[]) new Object[SIZE];
为什么出现这样的情况?事实上,在创建数组Object[]时,即使它已经被转型为Generic<Integer>[]
,但是这个信息只存在于编译期,并且如果没有注解@SuppressWarning,你将得到有关这个转型的警告.在运行时,它仍旧是Object数组,而这将引发问题.
成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对它转型.
所以上例可以修改如下:
public class ArrayOfGeneric {
static final int SIZE = 100;
static Generic<Integer>[] gia;
@SuppressWarnings("unchecked")
public static void main(String[] args) {
gia = (Generic<Integer>[]) new Generic[SIZE];
System.out.println(gia.getClass().getSimpleName());
gia[0] = new Generic<Integer>();
// gia[1] = new Object();
// gia[2] = new Generic<Double>();
}
}
class Generic<T>{}
这个时候运行是正常的.会出被擦除类型的Generic[]数组.
接下来再看一个例子,对一个泛型数组包装器GenericArray,实现它的put,get和rep方法.
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int sz){
array = (T[])new Object[sz];
}
public void put(int index, T item){
array[index] = item;
}
public T get(int index){
return array[index];
}
public T[] rep(){
return array;
}
}
因为并不能声明T[] array = new T[sz],所以我们创建了一个对象数组,然后将其转型.
在主方法中,实例化一个GenericArray方法并传入具体类型Integer,我们发现下面的语句编译不通过
public static void main(String[] args){
GenericArray<Integer> gai = new GenericArray<Integer>(10);
Integer oa = gai.rep();//编译不通过
}
}
这是因为第一句.虽然在编译期所创建的对象被认为是GenericArray<Integer>
类型的,但在运行时还是原始类型,即GenericArray[].当调用rep()方法时,GenericArray将返回泛型类型的数组array,即Object[].用Integer类型的引用指向它就会引发ClassCastException.
因为有了擦除,数组的运行时类型就只能是Object[].如果我们立即将它转型为T[], 那么在编译期该数组的实际类型就将丢失,而编译器可能会错过某些潜在的错误检查.正因为这样,最好是在集合内部使用Object[],然后当你使用数组元素时,添加一个对T的转型.
package 泛型演示.泛型数组;
import java.util.Arrays;
public class GenericArray2<T> {
private Object[] array;
public GenericArray2(int sz){
array = new Object[sz];
}
public void put(int index, T item){
array[index] = item;
}
@SuppressWarnings("unchecked")
public T get(int index){return (T)array[index];}
@SuppressWarnings("unchecked")
public T[] rep(){
return (T[])array;
}
public static void main(String[] args) {
GenericArray2<Integer> gai = new GenericArray2<>(10);
System.out.println(Arrays.toString(gai.getClass().getTypeParameters()));
for(int i = 0; i < 10; i++){
gai.put(i, i);
}
for(int i = 0; i < 10; i++){
System.out.print(gai.get(i)+" ");//为什么这里运行正常?
// System.out.println(gai.get(i).getClass());
}
System.out.println();
System.out.println(gai.get(0).getClass());
// Integer[] ia = gai.rep();//为什么这里运行异常?
}
}
未完待续.