Java泛型简介二

  1.泛型擦除的理解

  关于泛型,我们先看一个示例:

public class TypeErasure {

    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        List<Integer> integersList = new ArrayList<>();
        System.out.println(stringList.getClass() == integersList.getClass());
    }


}

  它的结果是true,这是因为在程序运行时期,这个泛型参数已经被擦除,返回类型都是List.class。泛型信息只存在于编译阶段,在进入JVM之前,该信息即会被擦除掉。虚拟机对泛型其实一无所知,所有的工作都是编译器做的,编译器把类型<T>视为Object,并进行了安全的强制转型。大家可以将List<XXX>的代码进行编译,而后将生成的class文件进行反编译,即可看到反编译的文件中已经没有了类型信息。

  为了匹配JDK1.5以下的版本,这里必须要在进入JVM之前对这个类型进行擦除。想象一下,如果在老版本的ArrayList中存放了A,B,C三种类型,都以Object处理,运行时存和放都合理地判断并处理了相应的逻辑,但JDK1.5在引入泛型后编译后不对类型擦除,而是生产了新的类型(类似ArrayList@Object@...),这就意味着,这个泛型在进行编译之后就变成了新的类型,老代码中的A,B,C是无法向上转型为ArrayList@Object@...的。如果此时运行环境升级为JDK1.5,那么老版本的代码就会报错。此时,为了匹配新版的JDK,开发者们就不得不对源代码做出更改,这样的代价既不利于JDK的推广,也是各个服务商所无法承受的。

  另外,编译器会阻止一个实际上会变成覆写的泛型方法定义。例如:

  2.通配符与边界

  参考1:https://www.cnblogs.com/wxw7blog/p/7517343.html

  参考2:https://www.liaoxuefeng.com/wiki/1252599548343744/1265104600263968

  注意通配符并不能用来定义接口、类或者方法,而是用于针对已经定义好的泛型方法参数,进行一个灵活的类型范围选取。即只能用在参数接收上,很多时候也会搭配extends ,super进行类型上下边界的划定。这里先看几个例子:

import java.util.List;

public class GenericCommonTest<T> {
    private T param;

    public void commonParam(GenericCommonTest<?> commonParam) {
        System.out.println(commonParam.getClass().getName());
    }

    public void commonExtendsParam(GenericCommonTest<? extends Number> commonExtendsParam) {
        System.out.println(commonExtendsParam.getClass().getName());
    }

    public void commonSuperParam(GenericCommonTest<? super Number> commonSuperParam) {
        System.out.println(commonSuperParam.getClass().getName());
    }

    public void commonParamList(List<?> commonParamList) {
        commonParamList.add(null);
    }

    public static void main(String[] args) {
        GenericCommonTest<?> numberGenericCommonTest = new GenericCommonTest<>();
        numberGenericCommonTest.commonParam(numberGenericCommonTest);
        numberGenericCommonTest.commonParam(null);
    }
}

  其中,“?”被称为无边界的通配符,它的作用是让泛型能够接受未知类型的数据。

  这里引入一个问题,如果List<T>型的参数,被定义为List<Number>,那么作为Number子类的Integer,可否使用List<Integer>进行参数传递呢?答案是不可以。因为List<Integer>不是List<Number>的子类。为了解决这个问题,我们可以把参数定义为List<? extends Number>。

  通配符的使用,增加了泛型的灵活性,可以在设置类型参数时,不必只指定一个类型,而是符合某个继承关系的一组类型。要注意List<? extends Number>还是不能调用add或者类似含义的set方法,因为编译器不知道到底装入的是Integer还是Double(这里说明一下,基本类型是不能作为类型参数放入泛型的)。唯一的,只有null例外。而函数返回的接收参数可以设置为Number,因为这里所有填入的类型都是Number的子类,所以这个限制即方法参数签名set(List<? extends Number> list)无法传递任何Number的子类给方法。同样地原理,List<?>中的add只能加入null,此时即便是Object的加入也会编译报错。因为不确定List泛型是什么类型的,所以任何类型的放入都是错误的,只有null可以放入。而List<?>的get方法的返回类型只能是Object,因为不知道里面放的什么类型,而Object是所有类型的父类,可以作为接收参数。此时List<?>与List<? extends Object>含义是相同的。

  所以,基于以上的限制,方法参数类型List<? extends Number>表明了该方法内部只会读取List内部的元素,不会修改List的元素。

  另外,在定义泛型类,接口时,<T extends Number>表示泛型类型限定为Number以及Number的子类。

  "? super E"表示下边界。即传入的类型,必须是E的父类或者本身。如List<? super Integer>,那么传入的类型可能为List<Object>,List<Integer>,List<Number>。可以看一个JDK中的实例:

    /**
     * Copies all of the elements from one list into another.  After the
     * operation, the index of each copied element in the destination list
     * will be identical to its index in the source list.  The destination
     * list must be at least as long as the source list.  If it is longer, the
     * remaining elements in the destination list are unaffected. <p>
     *
     * This method runs in linear time.
     *
     * @param  <T> the class of the objects in the lists
     * @param  dest The destination list.
     * @param  src The source list.
     * @throws IndexOutOfBoundsException if the destination list is too small
     *         to contain the entire source List.
     * @throws UnsupportedOperationException if the destination list's
     *         list-iterator does not support the <tt>set</tt> operation.
     */
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

   类比List<? extends Number>,使用super允许set<? super Integer>方法传入Integer的引用,但是无法通过get获取。唯一例外是可以get到Object类型。对比结论:

<? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
<? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。

  对比上面的copy示例,完美展示了两个边界使用的意图。

  OEIS原则:需要获取T,对方法来说,就是要OUT,使用extends;如果要写入T,那么就是IN,使用super。(PECS还是会记混)。

  无限定通配符<?>既不允许set(null除外),也不允许get(Object除外),所以使用场景比较少。大多数情况下,都可以使用T对?进行消除。?一般都是搭配extends或者super来使用。

原文地址:https://www.cnblogs.com/bruceChan0018/p/14939876.html