Java编程思想—泛型(2)

Java编程思想—泛型(2)

类型擦除

先上例子:

public class ErasedTypeEquivalence {

    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();

        Class c2 = new ArrayList<Integer>().getClass();
        
        System.out.println(c1 == c2);
    }


}


ArrayList< String >与ArrayList< Integer >是不同的类型,但是上面的程序输出true,也就是上面的程序认为它们是一样的类型。

再看接下来这个例子:

public class Apple {
}

public class Orange {
}

public class Pear<P> {
}


public class Watermelon<POSITION,MOMENTUM> {

}



public class LostInformation {


    public static void main(String[] args) {


        List<Apple> apples = new ArrayList<>();

        Map<Apple,Orange> map = new HashMap<>();

        Pear<Apple> pear = new Pear<>();

        Watermelon<Long,String> watermelon = new Watermelon<>();

        System.out.println(Arrays.toString(
                apples.getClass().getTypeParameters()
        ));

        System.out.println("______________________________________");

        System.out.println(Arrays.toString(
                map.getClass().getTypeParameters()
        ));

        System.out.println("______________________________________");

        System.out.println(Arrays.toString(
                pear.getClass().getTypeParameters()
        ));

        System.out.println("______________________________________");

        System.out.println(Arrays.toString(
                watermelon.getClass().getTypeParameters()
        ));

    }
}

Class.getTypeParameters()将返回一个TypeVariable对象数组,表示有泛型声明所声明的类型参数。

但是最终的输出结果确实一堆用作参数占位符的标识符,不是我们所想的具体的参数类型的信息。

最终的事实就是,在Java中,在泛型代码内部,你无法获得任何有关泛型参数类型的信息

Java泛型是用擦除来实现的,意味着你使用泛型时,任何具体的类型信息都背擦除了,你唯一知道的就是你在使用一个对象。

因此List< String >和List< Integer >在运行时事实上是相同的类型,这两种形式都被擦除成它们的“原生”类型,即List。

继续深入

还是先上例子:

public class HasF {

    public void f(){
        System.out.println("HasF.f");
    }
}

public class Manipulator<T> {

    private T obj;

    public Manipulator(T obj) {
        this.obj = obj;
    }

//    ERROR
    public void manipulator(){
//        obj.f();
    }
}

public class Manipulation {

    public static void main(String[] args) {

        HasF hasF = new HasF();

        Manipulator<HasF> manipulator =
                new Manipulator<>(hasF);

        manipulator.manipulator();



    }
}

因为擦除,Java编译器无法将manipulator()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实。

为了解决问题,我们必须给定泛型类的边界,告知编译器只能接受遵循这个边界的类型。


public class Manipulator2<T extends HasF> {

    private T obj;

    public Manipulator2(T obj) {
        this.obj = obj;
    }

    public void  manipulator2(){
        obj.f();
    }
}


边界< T extends HasF >声明T必须具有类型HasF或者从HasF导出的类型。

泛型类型参数将擦除到它的第一个边界(也许有有多个边界)。还有类型参数的擦除,编译器实际上会把类型参数替换为它的擦除,就像上面,T擦除到了HasF,就好像在类的声明中用HasF替换了T一样。

看第三个版本的Manipulator,我们可以自己去擦除:

public class Manipulator3 {

    private HasF obj;

    public Manipulator3(HasF obj) {
        this.obj = obj;
    }

    public void manipulator3(){
        obj.f();
    }
}

擦除带来的问题

擦除的代价很明显,泛型不能用于显式的引用运行时类型的操作之中,比如转型,instanceof操作,new表达式,因为所有关于参数的类型信息都丢失了。

所以如果接下来你写了这段代码:


public class Car {
}

public class Fon<T> {

    T var;
}

public class T1 {

    public static void main(String[] args) {

        Fon<Car> fon = new Fon<>();



    }
}

class Fon的代码应该工作与Car之上,泛型语法也在提醒我们,但事实并非如此,你应该意识到,这只是一个Object、

边界

用泛型表示没有任何意义的事物

public class ArrayMake<T> {

    private Class<T> kind;

    public ArrayMake(Class<T> kind) {
        this.kind = kind;
    }

    @SuppressWarnings("unchecked")
    T[] create(int size){
        return (T[]) Array.newInstance(kind,size);
    }

    public static void main(String[] args) {

        ArrayMake<String> stringArrayMake =
                new ArrayMake<>(String.class);

        String[] strings = stringArrayMake.create(10);

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

    }

}


kind即使被存储了Class< T >,擦除也意味着它实际被存储未Class,无任何参数,所以当你用Array.newInstance创建的时候,其实并未拥有任何kind的类型信息。

如果换成容器呢?

public class ListMake<T> {

    List<T> create(){
        return new ArrayList<T>();
    }

    public static void main(String[] args) {


        ListMake<String> stringListMake = new ListMake<>();

        List<String> stringList = stringListMake.create();

        

    }
}


编译器没有给出警告。

再看这个例子:

public class FilledListMake<T> {

    List<T> create(T t,int n){
        List<T> res = new ArrayList<>();

        for (int i = 0; i < n ; i++) {

            res.add(t);
        }

        return res;

    }

    public static void main(String[] args) {

        FilledListMake<String> stringFilledListMake =
                new FilledListMake<>();

        List<String> list = stringFilledListMake.create("World",20);

        System.out.println(list);

    }

}

即使擦除在方法或类内部移除了有关实际类型的信息,编译器依旧可以确保在方法或类中使用的类型的内部的一致性。

补偿擦除

因为擦除,任何在运行时需要知道的确切类型信息的操作都无法执行:

public class Erase<T> {

    private final int siez = 100;

    public static void f(Object o){


//        ERROR
//        if (o instanceof T) {
//
//        }


//        error
//        T var = new T();

//        error
//        T[] array = new T[siez];




    }
}


如果引入类别标签,那么就可以使用动态的isInstance():

public class ClassTypeCapture<T> {

    Class<T> kind;

    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }

    public boolean f(Object arg){
        return kind.isInstance(arg);
    }

    public static void main(String[] args) {

        ClassTypeCapture<Fruit> classTypeCapture =
                new ClassTypeCapture<>(Fruit.class);


        System.out.println(classTypeCapture.f(new Fruit()));

        System.out.println("____________________________________");

        System.out.println(classTypeCapture.f(new Apple()));

        System.out.println("____________________________________");

        ClassTypeCapture<Apple> classTypeCapture1 =
                new ClassTypeCapture<>(Apple.class);

        System.out.println(classTypeCapture1.f(new Fruit()));

        System.out.println("____________________________________");

        System.out.println(classTypeCapture1.f(new Apple()));


    }
}

使用类型标签的话,你就可以使用newInstance()来创建这个类型的新对象。

public class ClassFactory<T> {

    T x;

    public ClassFactory(Class<T> kind) {
        try {
            x = kind.newInstance();
        } catch (Exception e) {
            throw new RuntimeException();
        }

    }
}

public class Apple {
}


public class InstantiateGenericType {

    public static void main(String[] args) {

        ClassFactory<Apple> classFactory =
                new ClassFactory<>(Apple.class);

        System.out.println("succeed");


//        报错
        ClassFactory<Integer> classFactory1 =
                new ClassFactory<>(Integer.class);



    }
}


最后的语句会报错,因为Integer没有任何默认构造函数。

泛型数组

正如之前的例子可以看到,不能创建泛型数组,所以一般的思路就是用Arraylist

public class ListOfGenerics<T> {

    private List<T> array = new ArrayList<>();

    public void add(T item){
        array.add(item);
    }

    public T get(int index) {
        return array.get(index);
    }


}

假如你还是想创建泛型类型数组,你可以按照编译器喜欢的方式定义一个引用。

public class Generic<T> {
}

public class ArrayOfGenericReference {

    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];

        gia = new Generic[SIZE];

        System.out.println(gia.getClass().getSimpleName());

        gia[0] = new Generic<Integer>();

//        gia[1] = new Object();

//        gia[2] = new Generic<Double>();



    }


}


来看一个泛型数组包装器:

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;
    }

    public static void main(String[] args) {

        GenericArray<Integer> gai =
                new GenericArray<>(10);


//        error
//        Integer[] integers = gai.rep();

        Object[] objects = gai.rep();
        
    }
    
}


我们不能声明T[] array = new T[sz],所以需要创建一个对象数组,然后将其转型

因为擦除,数组运行时类型就只能时Object[],假设我们立即将其转型为T[ ],那么编译期间数组的实际类型就会丢失。

看接下来的例子:

public class GenericArray2<T> {

    private Object[] array;

    public GenericArray2(int size){
        array = new Object[size];
    }

    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> genericArray2 =
                new GenericArray2<>(20);

        for (int i = 0; i < 20; i++) {
            genericArray2.put(i,i);

        }

        for (int i = 0; i < 20 ; i++) {
            System.out.println(genericArray2.get(i) + "\n");

        }

//        error
//        Integer[] integers = genericArray2.rep();

    }


}


现在内部表示是Object[]而不是T[ ],方法get()会将对象转型为T,这是正确的类型。

当然传递一个类型标记也是可以尝试的:


public class GenericArrayWithTypeToken<T> {

    private T[] array;

    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class<T> type,int siez){
        array = (T[]) Array.newInstance(type,siez);
    }

    public void put(int index,T item){
        array[index] = item;
    }

    public T get(int index){
        return array[index];
    }

    public T[] rep(){
        return array;
    }

    public static void main(String[] args) {

        GenericArrayWithTypeToken genericArrayWithTypeToken =
                new GenericArrayWithTypeToken(Integer.class,30);

//        error
//        Integer[] ia = genericArrayWithTypeToken.rep();


    }

}


类型标记Class< T >被传递到构造函数中,以便从擦除中恢复,使得我们可以创建需要的实际类型的数组。

深入边界

边界使得你可以在用于泛型的参数类型上设置限制条件。

因为擦除移除了类型信息,所以可以用无界泛型参数调用的方法只是那些可以用Object调用的方法,但是假设能将这个参数限制为某个类型的子集,那么就可以用这些类型子集来调用方法。

看下面这个例子,体现边界的基本要素:

public interface HasColor {

    java.awt.Color getColor();
}

public class Colored<T extends HasColor> {

    T item;

    public Colored(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

    java.awt.Color color(){
        return item.getColor();
    }

}

public class Dimension {

    public int x,y,z;
}



public class ColoredDimension<T extends Dimension & HasColor> {

    T item;

    public ColoredDimension(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

    java.awt.Color color(){
        return item.getColor();
    }

    int getX(){
        return item.x;
    }
}


public interface Weight {
    int weight();
}


public class Solid<T extends Dimension & HasColor & Weight> {

    T item;

    public Solid(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

    java.awt.Color color(){
        return item.getColor();
    }

    int getX(){
        return item.x;
    }

    int weight(){
        return item.weight();
    }
}


public class Bounded extends Dimension implements HasColor,Weight{

    @Override
    public Color getColor() {
        return null;
    }

    @Override
    public int weight() {
        return 0;
    }
}



public class BasicBounds {

    public static void main(String[] args) {


        Solid<Bounded> solid =
                new Solid<>(new Bounded());

        solid.color();

        solid.getX();

        solid.weight();

    }
}


接下来看看如何在继承的每个层次上添加边界限制:


public class HoldItem<T> {

    T item;

    public HoldItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}


public class Color2<T extends HasColor> extends HoldItem<T> {

    public Color2(T item) {
        super(item);
    }

    java.awt.Color color(){
        return item.getColor();
    }


}

public class ColoredDimension2<T extends Dimension & HasColor> extends Color2<T>{

    public ColoredDimension2(T item) {
        super(item);
    }

    int getX(){
        return item.x;
    }
}


public class Solid2<T extends Dimension & HasColor & Weight> extends ColoredDimension2<T>{

    public Solid2(T item) {
        super(item);
    }

    int weight(){
        return item.weight();
    }
}


public class InheritBounds {

    public static void main(String[] args) {


        Solid2<Bounded> solid2 = new
                Solid2<>(new Bounded());

        solid2.color();

        solid2.getX();

        solid2.weight();
    }
}


HoldItem直接持有一个对象,因此这种行为也继承到了Color2中,也要求其参数与HasColor一致。

通配符

来考虑数组的一种特殊行为,向导出类型的数组赋予基类型的数组引用。

public class Fruit {
}

public class Apple extends Fruit{
}


public class Jonathan extends Apple{
}


public class Orange extends Fruit{
}


public class CovarianArrays {

    public static void main(String[] args) {

//      创建了一个Apple数组,并将其赋值给了一个Fruit数组引用,这是有意义的
//        因为Apple也是一种Fruit。所以Apple数组也是一个Fruit数组
        Fruit[] fruits = new Apple[10];

        fruits[0] = new Apple();

        fruits[1] = new Jonathan();

//        error
//        fruits[0] = new Fruit();

//        error
//        fruits[0] = new Orange();



    }
}



如果用泛型容器代替数组会怎样:

public class NonCovariantGenerics {

//    error
//    List<Fruit> fruits = new ArrayList<Apple>();


}


Apple的List在类型上不等价于Fruit的List,即使Apple是一种Fruit类型。

那如果我想要在两个类型之间建立某种类型的向上转型关系,这个时候就可以使用通配符了


public class GenericsAndCovariance {

    public static void main(String[] args) {


        List<? extends Fruit> fruits = new ArrayList<Apple>();

//        error
//        fruits.add(new Apple())

//        error
//        fruits.add(new Fruit())

        fruits.add(null);

        Fruit fruit = fruits.get(0);

        System.out.println(fruit);


    }
}

fruits现在是List<? extends Fruit>,你可以将其视作“具有任何从Fruit继承的类型的列表”。

逆变

超类型通配符

声明通配符是由某个特定类的任何基类来界定的,方法是指定<? super MyClass>,甚至或者使用类型参数<? super T>.

例如:

public class SuperTypeWildcards {

    static void writeTo(List<? super Apple> a){
        a.add(new Apple());

        a.add(new Jonathan());

//        error
//        a.add(new Fruit());
    }
}

参数Apple是Apple的某种基类型的List,这样向其中添加Apple或Apple的子类型是安全的。

超类型边界放松了在可以向方法传递的参数上所做的限制:


public class GenericWriting {

//    使用了一个确切参数类型(无通配符)
    static <T> void writeExact(List<T> list,T item){
        list.add(item);
    }

    static List<Apple> apples = new ArrayList<Apple>();

    static List<Fruit> fruits = new ArrayList<Fruit>();

    static void f1(){
        writeExact(apples,new Apple());
        writeExact(fruits,new Apple());
    }

//    使用了通配符,因此这个List将持有从T导出的某种具体类型
    static <T> void
    writrWithWildcard(List<? super T> list,T item){
        list.add(item);
    }

    static void f2(){
        writrWithWildcard(apples,new Apple());
        writrWithWildcard(fruits,new Apple());
    }

    public static void main(String[] args) {
        f1();
        f2();
    }

}


无界通配符

无界通配符<?>看起来意味着“任何事物”,因此使用起来就好像等价于使用原生类型。

public class UnboundedWildcard1 {

    static List list1;

    static List<?> list2;

    static List<? extends Object> list3;

    static void assign1(List list){
        list1 = list;
        list2 = list;
        list3 = list;
    }

    static void assign2(List<?> list){
        list1 = list;
        list2 = list;
        list3 = list;
    }

    static void assign3(List<? extends Object> list){
        list1 = list;
        list2 = list;
        list3 = list;
    }

    public static void main(String[] args) {


        assign1(new ArrayList());

        assign2(new ArrayList<>());

        assign3(new ArrayList<>());

        assign1(new ArrayList<String>());

        assign2(new ArrayList<String>());

        assign3(new ArrayList<String>());

        List<?> wildList = new ArrayList<>();

        wildList = new ArrayList<String>();

        assign1(wildList);

        assign2(wildList);

        assign3(wildList);

    }


}

发布了189 篇原创文章 · 获赞 58 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/Coder_py/article/details/103903525