Java泛型內各种参数的异同

Posted by W-M on July 22, 2017

先说下本篇随笔主要涉及到的东西(参考Java编程思想一书):

  1. 说明 List< Fruit > 与 List< Apple > 之间为什么是非继承关系。
  2. 由 1 引出的问题说明 List<? extends Fruit> 存在的必要性。
  3. List<? super Fruit> 与 List<? extends Fruit> 的区别及 List<? super Fruit> 存在的必要性。
  4. 说明 < ? extends Fruit > 与 < T extends Fruit > 区别。
  5. 说明 原生List 与 List<?> 区别。
  6. 解释自限定泛型 class SelfBound< T extends SelfBound< T > >{ }。

下面将会用到的三个有继承关系的类:

    class Fruit {
    }
    class Apple extends Fruit {
    }
    class Orange extends Fruit{
    }


1. 说明 List < Fruit > 与 List < Apple >之间为什么是非继承关系。

  我认为以下两个原因可以解释这个问题:

  1、Java中泛型是后来引入的特性,为了兼容之前的代码,泛型是存在擦除机制的,List< Fruit > 与 List< Apple > 在擦除后的class中均为List,并不存在继承关系。

  2、从逻辑上解释不能有继承关系的原因:

     public void test(List< Fruit > list) {
        list.add(new Orange());
     };

  在上面的代码中,test方法接收一个List< Fruit > 类型的list,并在此list 中插入了一个Orange对象,这个方法是没有任何问题的。现在假设List< Fruit > 与 List< Apple > 间存在继承关系,那么此方法可以接收一个List< Apple > 类型的list 参数作为方法的参数,然而之后在方法中就会在一个声明是List< Apple > 的list 中插入一个 Orange 对象,这显然是不符合逻辑的。所以 List< Fruit > 与 List< Apple >之间应该是非继承关系。


2. List<? extends Fruit> 存在的必要性。

  由一的介绍我们可以知道 test 方法只能只能接受 List< Fruit > 而不能接受 List< Apple > 类型的 list, 现在我想写一个方法,既能接受List< Fruit > 又能接收 List< Apple > 类型的 List,应该怎么做呢? 这时就要用到 List<? extends Fruit> 来实现这个功能了。

  List<? extends Fruit> 表示此 list持有的对象类型是 Fruit 或者从 Fruit 导出的类型(Apple 或者 Orange),相当于为 List 持有的对象类型规定了一个上界。

  我们只需将上面的test 方法改为 :

    public class TestGen {

        public void test(List<? extends Fruit> list) {
            /*由于传入的参数是 List<? extends Fruit> 参数的list,这个list是不能进行add 操作的。
            list.add(new Fruit());
            list.add(new Orange());
            list.add(new Apple());*/
        }

        public static void main(String[] args) {

            List<Apple> list = new ArrayList<Apple>();
            List<Orange> list1 = new ArrayList<Orange>();
             List<Fruit> list2 = new ArrayList<Fruit>();

             TestGen tg = new TestGen();

             tg.test(list);
             tg.test(list1);
             tg.test(list2);

        }
    }

  现在test 方法里的参数 变为了 List<? extends Fruit> list, 在 main 方法里可以看到此方法可以接受 List< Apple >,List< orange >,List< Fruit >多种类型的 list,实现了我们想要的功能。

  但是在上述代码的 test 方法当中我们也可以看到,作为参数传入的 list 是不能进行 add 操作的,无论 add 的是什么类型,这是为什么呢?

  原来由于传入的 List 类型是 List<? extends Fruit> list, 在JDK源码中可以看到 List的 add 方法的泛型参数就变为了 <? extends Fruit>,编译器并不能了解这里需要 add 的是哪个具体子类型,因此它

  不会接受任何类型的Fruit,编译器将直接拒绝对参数列表中涉及通配符的方法的调用。

  那么我们怎么才能做到向含有通配符的泛型限制的list 中做插入操作呢? 这时我们就要用到 <? super Fruit>参数。


3. List<? super Fruit> 与 List<? extends Fruit> 的区别及 List<? super Fruit> 存在的必要性。

  由二可知, 要想向含有通配符的泛型限制的list 中做插入操作, 此泛型限制必须为 <? super someClass>。 前面已经说到,List<? extends Fruit> 为此List 可以持有的对象类型规定了一个上界,即持有的对象只能为 Fruit 或 Fruit 的子类型。 相应地, List<? super Fruit> 则为 List 可以持有的对象类型规定了一个下界,即持有的对象只能为 Fruit 或 Fruit 的超类。

  因此若将 test 方法改为如下所示,则 add 操作可以进行。

    public void test(List<? super Fruit> list) {
             list.add(new Fruit());
             list.add(new Orange());
             list.add(new Apple());
    }

  因为我们可以确定 传入 test 方法的参数至少也是一个 持有 Fruit 类型的 List,所以我们向此 list 中加入 Fruit 及Fruit 的子类型的对象是没有任何问题的。


4. < ? extends Fruit > 与 < T extends Fruit > 区别。

  两个东西应用的场景是不同的,< T extends Fruit >作用于方法或者类上,而 <? extends Fruit> 则不可以。下面举例说明。

    public class TestGen<T extends Fruit> {
        private T value;

        public TestGen(T value) {
            this.value = value;
        }

        public T getValue() {
            return value;
        }

        public void setValue(T value) {
            this.value = value;
        }

        public <E extends Fruit> void test1(E e) {
            System.out.println(e.getClass().getName());
        }

        public static void main(String[] args) {

            TestGen<? extends Fruit> tg = new TestGen<Fruit>(new Fruit());
            //由于 setValue 方法参数列表中涉及通配符(setValue方法中的  T 相当于 ? extends Fruit),setValue方法不能调用。
            //tg.setValue(new Fruit());

            tg.test1(new Fruit());
            //tg.test1(new Object()); Object并不是Fruit的子类型,并不能作为参数传入test1方法。
        }
    }

  在代码中可以看到,类上的限定 < T extends Fruit > 及方法上的限定 < E extends Fruit > 使得类和方法能接受的类型均为 Fruit 及 Fruit 的子类型。


5. 原生List 与 List<?> 区别。

  无界通配符 ? 与 原生类型看似好像是等价的,但无界通配符可以有以下含义:“我是想用Java的泛型来编写这段代码,我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型”。

  使用无界通配符的 List 要比 原生 List 要安全些。 在原生 list 中,我们可以随意进行插入操作,可以向同一个 list 中既插入 object 对象,又插入 Fruit,Apple 对象,而 List 是不允许进行 add 操作的。


6. 带有通配符的泛型强制类型转换问题

  强制类型转换:编译器在编译时只会检查原类型与强转后类型之间是否存在继承关系,有则通过;而在运行时就会检查它的真实类型,是则通过,否则抛出ClassCastException异常。
  来看代码:

public static void testGeneric(Reference<? extends Object> ref) {
	Reference<Object> ref0 = (Reference<Object>) ref;
    Reference<String> ref1 = (Reference<String>) ref;
    //Reference<Object> ref2 = (Reference<String>) ref;编译错误
}
public static void testGeneric2(Reference<? extends String> ref) {
    Reference<String> ref1 = (Reference<String>) ref;
    //Reference<Integer> ref2 = (Reference<Integer>) ref;编译错误
    //Reference<Object> ref2 = (Reference<Object>) ref;编译错误
}

  可以看到Reference<? extends Object> ref可以被强转为泛型参数为Object及Object的子类的引用,并且需要保证强转前与强转后Reference<>内泛型参数相同,Reference<? extends String>也是同理。


7. 解释自限定泛型 class A< T extends A< T > >{ }。

  在 Java 泛型中,这算是一种经常出现的惯用法,这种用法意味着 基类 (这里的类A) 使用其导出类代替其参数,泛型基类变成了一种其所有导出类的公共公能的模板,但是这些功能对于其所有参数和返回值,将使用导出类型。

  下面举例说明。

	//< T extends A< T > >即限制T代表的类必须继承自类A,且A泛型内参数为T
    class A < T extends A< T > > {
        void set(T arg) {
            System.out.println("in A");
        }
    }
    //泛型基类变成了一种其所有导出类的公共公能的模板
    //导出类B和C均可调用基类的set方法,且只接受导出类及其子类对应的类型
    class B extends A<B> {

    }
    class C extends A<C> {

    }
	class D extends B {

	}
    public class TestGen {
        void test2(B b1, B b2, C c1, A a1, D d1) {
            b1.set(b2);
			b1.set(d1);
            //使用自限定泛型时,在导出类中调用的set方法只接受对应的导出类型,不接受基类型和其他的导出类型
            //b1.set(c1); 不接受其他的导出类型
            //b1.set(a1); 不接受基类型,编译器不能识别将类型传递给set的尝试,因为没有任何方法有这样的
            //签名,实际上,这个参数已经被覆盖
        }
    }