java泛型的一些笔记

泛型

泛型方法

如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法。另外,如果static方法需要使用泛型的能力,就必需使其成为泛型方法。
定义泛型方法的方式:将泛型参数列表置于返回值之前,如:

public  void f(T x){
    //...
}

类型参数推断

在java se7以前,使用泛型时往往会出现如下的重复代码:

Map map = new HashMap>();

但是在java se7后,可以使用一个空的类型参数集合 <> 来代替重复的类型参数:

Map map = new HashMap<>();

擦除

java中泛型是使用擦除来实现的,使用泛型时,任何具体的类型信息都会被擦除。
虚拟机中没有泛型,只有普通的类和方法,所有参数类型都用它们的类型限定符替换。普通类会被替换为Object,List被替换为List,而中的T则会替换为A。
因此,ListList 在运行时实际上是同一个类型。
在泛型中,所有动作都发生在边界处:对传递进来的值进行编译期的类型检查,并插入对传递出去的值的转型。
由于擦除的存在,泛型代码中无法知道确切的类型信息,因此任何在运行时需要知道具体类型信息的操作都无法工作。但是有时可以引入类型标签来完成擦除的补偿:

public class A{
    Class kind; //用一个Class类存储类型信息
    public A(Class kind){
        this.kind = kind;
    }
    public static void main(Stirng[] args){
        A a = new A<>(String.class);
    }
}

泛型数组

一个简单的泛型数组包装器:

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

    @SuppressWarnings("unchecked")
    public T get(int index){
        return (T) array[index];
    }

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

    public static void main(String[] args){
        TestArray array = new TestArray<>(3);
        array.put(0,"ABC");
        System.out.println(array.get(0));
    }
}

在这个示例中,数组的类型是Object[]而不是T[],当get()方法中返回数组元素时才转型为T。ArrayList的源码正是这样处理的,这样做比起直接将T[]作为数组类型更有一定优势。如果引入类型信息,就可以这样写:

public class TestArray {
    T array[];

    @SuppressWarnings("unchecked")
    public TestArray(Class type,int size) {
        array = (T[]) Array.newInstance(type,size);
    }

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

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

    public static void main(String[] args){
        TestArray array = new TestArray<>(String.class,3);
        array.put(0,"ABC");
        System.out.println(array.get(0));
    }
}

这里直接根据传入的类型信息,使用Array.newTnstance()方法创建一个类型为T的数组。
其中构造函数的入参type类型为Class而不是Class,这很有必要,如此一来倘若传入了一个错误的类型信息,编译器就会报错,而不是运行时才抛出异常。

边界

由于擦除机制,参数类型T都会转换为Object类型,这意味着在泛型类的内部就不能直接调用T的方法(Object类没有的方法)。这时可以借助extends关键字设置参数类型的边界:

class B{
    public void show(){
        //...
    }
}
public class A{
    public void show(){
        T t = new T();
        t.show();
    }
}

协变

Java中,数组是可协变的。协变指的是,如果A类是B类的基类,那么A[]则是B[]的基类。
这种协变性会带来隐患,参考如下代码:

public class Test{
    class A{}
    class B extends A{}
    class C extends A{}
    public static void main(String[] args){
        A[] a = new B[];
        a[0] = new C();  //抛出ArrayStoreException异常
    }
}

上述代码可以通过编译,因为A[]是B[]的基类,而A[]又是C[]的基类。但是运行时却会抛出一个异常,因为实际上a的类型是B[],而B[]并非C[]的基类。
泛型是不可协变性的,List和List是完全无关的两个类,虽然存在擦除机制,但是像List array = new ArrayList()这样的代码是不能通过编译的。

通配符

可以在泛型参数表达式中引入问号作为通配符,用法如下:

//存在Animal、Cat类,Cat继承Animal
List list = new ArrayList();

这里List意味着list持有的对象必须是某种具体的、基类为Animal的类。值得一提的是,这种方法定义的list,由于编译器并不知道“?”代表的到底是什么类型,如果你想用add()方法向其中添加元素,编译器会报错,因为这种添加行为并不安全。但是你依然可以调用list的get()方法来返回一个对象,因为说明了容器里的对象至少是Animal类型。如下:

List list1 = new ArrayList<>();
list1.add(new Cat());
List list2 = list1;
//下一条语句无法通过编译
//list2.add(new Cat()) 
//get()方法可以使用
Animal a = list2.get(0);

另外一种用法是超类型通配符:List list = new ArrayList()

代表Cat的某个超类,这时向其中添加Cat或其子类的行为就是安全的。但是添加B的父类(如Animal)又会被编译器阻止。因为编译器只知道List持有对象的类型是Cat的某个具体的超类,它有可能是Animal,也有可能是Animal的某个父类,无论是哪个类型,存入Cat或Cat的子类是一定没有问题的;但是如果可以存入Animal,就意味着它也允许其他Animal的子类(如Dog)存入其中,这显然并不安全。

你可能感兴趣的:(Java)