java中的泛型(1)

java中的泛型(1)

 本博客参考 << thinking in java >> 第15章泛型,记录整理我的学习笔记,结合了自己的思考和理解.

泛型使用

  1. 使用类型参数,用尖括号括住,放在类名/接口后面.如果有多个参数,只需要用逗号隔开即可.
public class Holder<T>{
    private T a;
    public void set(T a){ this.a = a;}
}
  1. 基本类型不能作为类型参数,但是java具备自动打包和自动拆包的功能,可以很方便地在基本类型和其相应的包装器类型之间的转换.
  2. 泛型方法:只需要将泛型参数列表置于返回值之前.例如public <T> void f(T x){...},但是,是否拥有泛型方法和这个类是否是泛型没有关系.使用泛型类的时候,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指定参数类型,编译器会找出具体的类型.如果调用泛型参数时传入基本类型,那么自动打包机制就会介入.
  3. 泛型方法的显式类型说明:在点操作符与方法名之间插入尖括号,然后把类型置于尖括号中,如果是定义在该方法的类的内部,必须在点操作符之前使用this关键字,如果是static方法,必须在点操作符之前加上类名.但一般在编写非赋值语句时才会使用到这种语法.
  4. 泛型还可以用于内部类和匿名内部类,假设有一个使用了泛型的类Generator<T>, 那么考虑一个方法返回一个Generator<Customer>的对象:
    public static Generator<Customer> G
    generator(){
        return new Generator<Customer>(){
            public Customer next(){
                return new Customer();
            }
        }

擦除

  1. 疑问引入:考虑一下代码:
    Class c1 = new ArrayList<String>().getClass();
    Class c2 = new ArrayList<Integer>().getClass();
    System.out.println(c1 == c2); // output : true

发现输出居然为true!原来:在泛型代码内部,无法获得任何有关泛型参数类型的信息.java泛型是用擦除来实现的,这意味着在你是用泛型的时候,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象.故上面的两种类型都被擦除为”原生”类型.

  1. 由于擦除,下面的ObjectOne.java代码就无法编译通过:
    ObjectOne.java
    public class ObjectOne<T> {
    private T obj;
    ObjectOne(T obj){
        this.obj = obj;
    }
    void test(){
        obj.f(); // can't resolve method f()
    }

    public static void main(String[] args) {
        ObjectTwo t = new ObjectTwo();
        ObjectOne<ObjectTwo> o = new ObjectOne<>(t);
    }
}

    ObjectTwo.java
    public class ObjectTwo {
    void f(){

    }
}

java无法将test()中能够在obj上调用f()这一需求映射到ObjectTwo中拥有f()的事实,为了调用f(),我们必须协助泛型类,给定泛型类的界限,我们需要使用extends关键字,将ObjectOne<T>替换为ObjectOne<T extends ObjectTwo>就可以了,这表明T必须具有类型ObjectTwo或者是从ObjectTwo导出的类型(同理,如果在泛型类中使用属性,例如T.attr,那么这个也必须说明<T extends X>, 其中X类中含有属性x,这个X也是相当于协助泛型类).但是你可以看出,这并没有使用到泛型的好处,要真是这样,为什么干脆把类中的T直接替换为ObjectTwo的类型呢?当然,如果这个类中是要返回一个T类型,那么泛型才能够派上用场,因为它可以返回一个确切是T类型的而不是T的导出类型.
3. 泛型类型只有在静态类型检查期间才会出现,在此之后,程序中的所有泛型类型都会被擦除,替换为他们的非泛型上界,例如LIst被擦除为List,而普通的类型变量在没有指定边界的情况下将会被擦除为Object.

擦除的补偿(针对java中泛型的限制的一些解决方法)

  1. 擦除丢失了在泛型代码中执行某些操作的能力,任何在运行时需要知道的确切类型信息的操作都将无法进行:
    public class ObjectOne<T> {
        private final int SIZE = 100;
        public void f(Object obj){
            if (obj instanceof T){ } // error
            T var = new T(); // error
            T[] array = new T[SIZE]; // error
            T[] array2 = (T[]) new Object[SIZE]; // unchecked case
        }

    }

上面的实例中instanceof的尝试失败了,原因是它的类型信息已经被擦除了,如果引入类型标签,那么就可以动态使用isInstance()方法了.(即你需要将T换成Class<T> obj, 引入一个obj对象)

    public class ClassTypeCapture<T>{
        Class<T> kind;
        public ClassTypeCapture(Class<T> kind){
            this.kind = kind;
        }
        public boolean f(Object arg){
            return kind.isInstance(arg);
        }
  1. 上述可以看到new T()将因为擦除而无法实现,而且编译器不能验证T具有默认的(无参)构造器,虽然在c++中这种操作是很自然和安全的.解决方法:传递一个Class类型的工厂对象,使用getConstrutor().newInstance()方法创建实例.
    //c++ version : work perfectly
    template<class T> class Foo{
        T x;
        T* y;
        public:
            Foo(){
                y = new T();
            }
    };

    class Bar(){};

    int main(){
        Foo<Bar> fb; // ok!
        Foo<int> fi; // 即使是基本类型也没毛病

    // java version 
    public class ClassAsFactory<T>{
        T x;
        public ClassAsFactory(Class<T> kind){
            try{
                x = kind.getConstructor().newInstance();
            } catch (Exception e){
                throw new RuntimeException();
            }
        }
    }

但是上述java的代码还是存在问题,如果调用new ClassAsFactory<Integer>(),会由于Integer没有默认的构造函数而失败,但这个错误不是在编译器捕获到的,sun建议使用显式的工厂,并限制其类型,使得只能够实现了这个工厂的类.(我的理解就是,你不能要生产什么类型的对象就把这个对象的Class对象扔过来,有可能这个对象像Integer一样没有默认的无参构造器,因此这个工厂只能够是一个接口的存在,你需要生产什么对象就要实现这个工厂接口,定义你自己的生成方法,所以你可以发现,其实你的对象在你定义的工厂中已经创建出来了)解决方法2:

    public interface Factory<T> {
        T create();
    }

    public class Foo2<T> {
        private T x;
        public <F extends Factory<T>> Foo2(F factory){
            x = factory.create();
        }
    }

    public class IntegerFactory implements Factory<Integer> {
        @Override
        public Integer create() {
            return new Integer(0);
        }
    }

    public class Widget {
        public static class Factory implements general.Factory<Widget>{
            @Override
            public Widget create() {
                return new Widget();
            }
        }
    }

于是想要生成你的对象,只需要先写一个工厂类实现了Factory接口,实现你这个类的create方法,然后将这个工厂类传递给Foo2类,这个Foo2类就包含你想要生成的对象.(此时此刻我已经懵逼了,c++几行的东西在java下搞居然要这么麻烦).另外一种解决方法使用设计模式中的模板方法:

    abstract class GenericWithCreate<T> {
        final T element;
        GenericWithCreate(){
            element = create();
        }
        abstract T create();
    }

    class X{}

    public class Creator extends GenericWithCreate<X> {
        @Override
        X create() {
            return new X();
        }
    }

这种方法将create步骤分离出来了,使用一个指定类型的Creator来生成对象.

泛型数组

  1. 正如上面所看到的,不能创建泛型数组.一般的解决方法是使用ArrayList.
    public class ListOfGenerics<T>{
        private List<T> array = new ArrayList<T>;
        public void add(T item){ array.add(item);}
        public T get(int index){ array.get(index);}
    }
  1. 先考虑如下代码:
    public class ArrayOfGeneric {

    static final int SIZE = 100;
    static Generic<Integer>[] gia;

    public static void main(String[] args) {
        // gia = (Generic<Integer>[]) new Object[SIZE]; // uncheck cast. Produce classCastException.
        // gia = new Generic<Integer>[SIZE]; error
        gia = (Generic<Integer>[]) new Generic[SIZE]; // uncheck cast
        System.out.println(gia.getClass().getSimpleName());
        gia[0] = new Generic<>();
    }
}

有一点让人疑惑,就是数组无论他们的持有类型如何,都具有相同的结构(数组槽位的尺寸和数组布局),看起来你可以通过创建一个Object数组并将其转型为希望的数组类型,事实上这可以编译但是不可以运行,会出现classCastException.这个问题在于数组将会跟踪他们的实际类型,而这个类型是在数组创建时确定的,在运行时,它们仍然是Object数组,而这将会引发问题.成功创建泛型数组的唯一方法是创建一个被擦除类型的新数组,然后对其转型,正如gia = (Generic<Integer>[]) new Generic[SIZE]这一句
还有一种实现方法,利用泛型数组包装器,而且最好在集合内部使用Object[]

    public class GenericArray<T>{
        private Object[] array;
        public GenericArray(int sz){
            array = new Object[sz];
        }
        public void put(int index, T item){
            array[index] = item;
        }
        public T get(int index){ return (T)array[index];}
        public T[] rep(){ return (T[]) array;} // warning : unchecked cast

现在内部表示是Object[]而不是T[],get()被调用时,它将对象转型为T,这实际上是正确的类型.但是如果你调用rep(),他还是尝试将Object[]转型为T[],这仍然是不正确的,将在编译器发生警告,在运行时发生异常.因此,没有任何方式可以推翻底层的数组类型,他只能是Object[]
对于新代码,应该传递一个类型标记:

    public class GenericArrayWithToken <T>{
    private T[] array;
    public GenericArrayWithToken(Class<T> type, int sz){
        array = (T[]) Array.newInstance(type, 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) {
        GenericArrayWithToken<Integer> gai = new GenericArrayWithToken<>(Integer.class, 10);
        Integer[] ia = gai.rep();
        ia[0] = 1;
        System.out.println(ia[0]);
    }
}

类型标记Class 被传递到构造器中,以便从擦除中恢复.

边界

  1. 边界使得你可以用于泛型的参数类型上设置限制条件.由于擦除移除了类型信息,所有用无边界泛型参数调用的方法只是那些可以用Object调用的方法,但如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法.
  2. 使用extends和super关键字进行参数类型的限制<T extends ClassName>表示类型是ClassName的类型或者是它的导出类型,super意义同样易得.如果需要extends/super多个ClassName,使用&符号进行连接.
  3. 通配符:
    先展示数组的一种特殊行为:可以向导出类型的数组中赋予基类型的数组引用:
    //definition
    class Fruit{}
    class Apple{}
    class Joanthan extends Apple{}
    class Orange extends Fruit{}

在考虑一下语句:

    Fruit[] f = new Apple[10];
    f[0] = new Apple();
    f[1] = new Joanthan();
    try {
        f[0] = new Fruit();
    } catch (Exception e} {System.out.println(e);}
    try {
        f[1] = new Orange()'
    } catch (Exception e){System.out.println(e);}

结果是编译没有问题,但是运行起来两个try块都会抛出异常.原因也很简单:实际的数组类型是Apple[],你应该只能够在其中放置Apple或者Apple的子类,这在编译器或者是运行时都是没有问题的.但是编译器也允许你将Fruit或者它的导出类(如Orange)放入这个数组中,由于它是一个Fruit的引用.但是运行时的数组机制知道它处理的实际是Apple[],因此会在数组中放置异构类型时抛出异常.
然后,我们试图用泛型容器来代替泛型数组List<Fruit> f = new ArrayList<Apple>()编译器就会报出错误,Apple的list根本就不是Fruit的list,即使Apple是Fruit的子类.
于是我们想引入通配符,List<? extends Fruit> f = new ArrayList<Apple>,但是注意,这实际上并不意味着可以持有任何类型的Fruit,通配符引用的是明确的类型,但是现在你并不知道它的实际类型,既然如此,那就不能安全地向其中添加对象,所以现在你连Apple,Fruit也加不进去.但是如果你创建了一个Holder<Apple>,虽然不可以向上转型为Holder<Fruit>,但是可以向上转型为Holder<? extends Fruit?.如果调用get(),它就会返回一个Fruit.

    public class Holder<T>{
        private T value;
        public Holder(){}
        public Holder(T val){ value = val;}
        public void set(T val){ value = val;}
        public T get() { return value;}
        public boolean equals(Object obj){ return value.equals(obj);}
        public static void main(String[] args){
            Holder<Apple> apple = new Holder<Apple>(new Apple());
            Holder<? extends Fruit> fruit = apple; // ok
            Fruit p = fruit.get();
            Apple d = (Apple) fruit.get();
        }
    }
  1. 使用超类型通配符super,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定<? extends MyClass>.
        List<? super Apple> a = new ArrayList<>();
        a.add(new Apple());
        a.add(new LittleApple()); // littleApple为Apple的子类
        //a.add(new Fruit()); error

a是Apple的某种基类型的list,那么可以知道向其中添加Apple或者LittleApple是安全的,但是添加Fruit是不安全的,因为你无法确定Fruit就是这个基类或者这个基类的导出类.
5. 先看书本上一段代码:


public class GeneralReading {
    static <T> T readExact(List<T> list){
        return list.get(0);
    }
    static List<Apple> apples = Arrays.asList(new Apple());
    static List<Fruit> fruits = Arrays.asList(new Fruit());
    static void f1(){
        Apple a = readExact(apples);
        Fruit f = readExact(fruits);
        f = readExact(apples);
    }

    static class Reader<T>{
        T readExact(List<T> list){
            return list.get(0);
        }
    }

    static void f2(){
        Reader<Fruit> fruitReader = new Reader<>();
        Fruit f = fruitReader.readExact(fruits);
        //Fruit a = fruitReader.readExact(apples); error
    }

    static class CovariantReader<T>{
        T readCovariant(List<? extends T> list){
            return list.get(0);
        }
    }

    static void f3(){
        CovariantReader<Fruit> fruitCovariantReader = new CovariantReader<>();
        Fruit f = fruitCovariantReader.readCovariant(fruits);
        Fruit a = fruitCovariantReader.readCovariant(apples);
    }
}

可以看到:①f1()是一个静态泛型方法,它的返回值可以有效适应每一个方法调用.②Reader是一个泛型类,创建这个类的实例时要为这个类确定参数,f2()中创建的时候确定的参数是Fruit,虽然传入的List<Apple>可以产生Fruit,但是FruitReader不允许这么做.③CovariantReader类的方法将接受List<? extends T>,因此在列表中读取一个T就是安全的.
下面先本人根据理解稍微总结一下
6. (add)List,List<?>,List<? extends Object>:①首先要明确,正如之前所提到的,’?’并不代表容器可以容纳任意类型,它是确定的,只是我们不知道它的准确类型(或者说不知道容器的下界).②本人实践得出,它们三者是可以相互赋值的,而且它们的get方法都是正常运行的.③但是,正如前面所提到,既然涉及’?’的我们都不知道它的具体类型信息(或者说它的下界),所以List<?>,List<? extends Object>都不可以add任何的对象,但是List相当于List<Object>,它可以添加任何的对象.(相反,由于List<? super ClassType>我们知道它的下界为ClassType,那么它至少可以添加ClassType类型或者ClassType的子类型)
7. (get)对于List,List<?>,List<? super ClassType>,我们都不知道容器的上界是什么类型的,所以调用get方法返回值为Object;而对于List<ClassType>,List<? extends ClassType>,我们都清楚容器的上界,所以调用get方法返回值为ClassType.

猜你喜欢

转载自blog.csdn.net/qq_37993487/article/details/80052399