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