• 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来使用。

  • 相关阅读:
    字符集WideCharToMultiByte
    [HDF]hdf-4.2.6类库的使用
    [GDAL]读取HDF格式的calipso数据
    C#读写BitMap及颜色相乘
    [Slimdx]顶点和索引缓冲,绘制了2个分离的三角形
    [GDAL]写入shp
    几个环境学概念
    MIConvexHull
    几个力学概念
    [转载]如何破解Excel VBA密码
  • 原文地址:https://www.cnblogs.com/bruceChan0018/p/14939876.html
Copyright © 2020-2023  润新知