Java 泛型(1):基本原理

1.1      擦除

15.7例子

Java的泛型是同伙擦除来实现的,在泛型代码内部,无法获得任何有关泛型参数类型的信息(这一点有别于C++等实现),这意味着你在使用泛型时,无法知道任何类型信息,只知道你在使用一个对象,因此List<String>和List<Integer>在运行时事实上是相同的类型。

1.1.1        C++的实现

15.7.1例子

C++的实现与Java实现不同,当你实例化模板时,C++编译器会进行检查,模板代码知道参数类型,所以如果调用了类型的某些方法,将由编译器在编译时进行检查

1.1.2        为何采用擦除

泛型不是Java出现时就有的组成部分,而是中途加入的,为了向下兼容许多现有的代码和类文件,并且保持其原有的含义,所以采用了擦除这一解决方案,允许非泛型和泛型代码共存。

       必须意识到,擦除并不是实现泛型最好的技术,如果泛型出现在Java 1.0,那么将不会使用擦除来实现,而是使用具体化,这样你就可以在类型参数上执行基于类型的操作和反射,擦除减少了泛型的泛化性,使得Java的泛型没有本来设想的那么有用,但是泛型在Java中仍然是一个很有用的特性。

1.1.3        擦除引入的问题

由于擦除的原因,Java的泛型不能用于显示地引用运行时类型的操作,例如转型、instanceof、new等操作。因为所有的参数类型都丢失了,在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有了参数类型信息,例如:

Foo<Cat> f = new Foo<Cat>();

我们通常会认为,Foo的代码应该知道操作的对象是Cat,而且从语法上也给人这样的暗示:在Foo类中,所有的地方类型T都被替换成了Cat。但是事实却完全不是这样,你必须要知道,它只是一个Object。

1.2      边界

边界就是在泛型的参数类型上设置限制条件,比如<T extends Shape>等,使用边界让你可以强制规定泛型可以应用的类型。

    因为擦除了类型信息,导致我们只能调用那些Object的方法,但是,我们可以通过将这个参数限制为某个类型的子集,那么你就可以用这些类型子集的方法。我们可以通过继承来消除边界书写上的冗余,如下例所示,通过继承,在每个层次上添加边界限制,在不断的继承中,T拥有了越来越多的成员。

public interface IGetName {

    public String getName();

}

public interface Weight {

    public int getWeight();

}

public class Shape {

    int x;

    int y;

    int z;

}

public class Cat<T> {

    T item;

    Cat(T item) {

        this.item = item;

    }

}

public class MyCat<T extends IGetName> extends Cat<T>{

    MyCat(T item) {

        super(item);

    }

    public String getMyCatName() {

        return item.getName();

    }

}

public class MyCats<T extends Shape & IGetName & Weight> extends MyCat<T> {

    MyCats(T item) {

        super(item);

        // TODO Auto-generated constructor stub

    }

    int getCatWeight() {

        return item.getWeight();

    }

    int getShape() {

        return item.x + item.y + item.z;

    }

}

 

注意:在继承中,T的作用域必须越来越窄,不能越来越宽,例如:IgetName接口在MyCat的类中已经作了限制,如果在MyCats的继承中将IGetName去掉(class MyCats<T extends Shape & Weight> extends MyCat<T>),将会出现The type T is not a valid substitute for the bounded parameter <T extends IGetName> of the type MyCat<T>的错误,原因就是T的作用域变宽了(父类的T必须是继承IGetName,而子类没有这一限制)。

    在类型边界限制中,不管是类还是接口都是使用extends,中间使用&分隔,同时需要注意的时,如果有类继承,那么类必须写在第一个,接口写在第二个到第n个,<T extends 类 & 接口1 & 接口2 & 接口3 ...>。

1.3      泛型数组

1.3.1        为什么Java不能创建泛型数组

擦除会移除参数类型信息,而数组必须知道他们所持有的确切类型,以强制保障类型安全

在Java中,Object[]数组可以是任何数组的父类,或者说,任何一个数组都可以向上转型成它在定义时指定元素类型的父类的数组,这个时候如果我们往里面放不同于原始数据类型 但是满足后来使用的父类类型的话,编译不会有问题,但是在运行时会检查加入数组的对象的类型,于是会抛ArrayStoreException:

String[]strArray=newString[20];

Object[]objArray=strArray;

objArray[0]=newInteger(1);// throws ArrayStoreException at runtime

因为Java的范型会在编译后将类型信息抹掉,这样如果Java允许我们使用类似Map<Integer,String>[]mapArray=newMap<Integer,String>[20];这样的语句的话,我们在随后的代码中可以把它转型为Object[]然后往里面放Map<Double, String>实例。这样做不但编译器不能发现类型错误,就连运行时的数组存储检查对它也无能为力,它能看到的是我们往里面放Map的对象,我们定义的<Integer, String>在这个时候已经被抹掉了,于是而对它而言,只要是Map,都是合法的。想想看,我们本来定义的是装Map<Integer, String>的数组,结果我们却可以往里面放任何Map,接下来如果有代码试图按原有的定义去取值,后果是什么不言自明。

       如上原因,Java中不能创建泛型数组,一般的解决方案是在你想创建泛型数组的地方使用ArrayList。

1.3.2        生成泛型数组的折中方法

       有趣的是,虽然Java中不能创建泛型数组,但是你却可以定义一个泛型数组的引用,而且编译器不会产生任何警告,但是你却永远不能创建这个数组。

Foo<Cat> []f = new Foo<Cat>[SIZE];        //error

Foo<Cat> [f];                                    //right

       既然无法创建泛型数组,那么泛型数组的引用拿来干什么呢,考虑以下用法:

              Foo<Cat> []f = (Foo<Cat>)new Object[SIZE];  //compile ok, run error

数组的每个元素应该持有相同的类型,那么通过创建一个Object的数组进行强转成泛型数组是否可行呢?事实上以上代码是可以编译的,但是当你运行的时候,却会抛出ClassCastException异常。原因是虽然我们将Object进行了强转(所以编译期不会报错),但是在运行时它仍然是Object,所以引发了类异常。

事实上,生成泛型数组的唯一方式是创建一个被擦除类型的新数组,然后对其转型,例如: T [] array;  array = (T[])new Object[SIZE]; 考虑以下例子,我们使用泛型数组来生成一个自己的简单的数组:

class MyArray<T> {

    private T[] array;

    public MyArray(int size) {

        array = (T[])new Object[size];

    }

   

    public void set(T item, int n) {

        array[n] = item;

    }

   

    public T get(int n) {

        return array[n];

    }

   

    public T[] getArray( ) {

        return array;

    }

   

    public static void prt(String str) {

        System.out.println(str);

    }

    public static void main(String []args) {

        MyArray<String> my = new MyArray(3);

        my.set("a", 0);

        my.set("b", 1);

        my.set("c", 2);

       

        prt(my.get(1));

        //String[] o = my.getArray();   //run error

        Object[] o = my.getArray();

        prt(Arrays.toString(o));

    }

}

//out

b

[a, b, c]

 

注意:我们仍然不能使用T[] array = new T[SIZE];只能使用创建对象数组,然后强转的方式,因为Object是任何类型的基类,所以我们可以将String类型的对象赋值到数组上,注意方法getArray(),它的返回值为T[],在main函数中,声明的是<String>,所以我们尝试将其赋予到String[]数组上,但是此时运行时发生了类型错误,这是因为本质上我们的数组还是Object[],所以在运行中试图将一个Object[]赋值到String[],自然会发生异常。

       事实上,更加稳妥的做法是将上述代码中的数组声明,更改成Object[] array,由于Java的擦除技术,所以我们运行的时候类型都会是Object,如果我们立刻将其转型成T[],那么我们编译期就会丢失数组的实际类型(Object),此时编译器可能错过一些潜在的错误检查。另外一点,使用Object[]声明可以随时提醒我们,这个数组运行时是Object,而不是T[]。

1.3.3        生成“真正的”泛型数组

仍然考虑上述的例子,如果我们将参数类型传入进去,那么我们可以生成一个“真正的”泛型数组,通过反射技术,生成了实际传入的参数类型(String)的数组,这样array的实际类型就是String[],而不是上例的Object[],所以在main()中getArray()直接返回给String[]类型的引用没有任何问题。

class MyArray<T> {

    private T[] array;

    public MyArray(Class<T> type, int size) {

        array = (T[])Array.newInstance(type, size);

    }

    public void set(T item, int n) {

        array[n] = item;

    }

    public T[] getArray( ) {

        return array;

    }

    public static void main(String []args) {

        MyArray<String> my = new MyArray(String.class, 3);

        my.set("a", 0);

        my.set("b", 1);

        my.set("c", 2);

        String[] o = my.getArray();

        System.out.println(Arrays.toString(o));

    }

}

 

猜你喜欢

转载自www.cnblogs.com/LemonPi/p/11040252.html