软件构造随笔4
为了防止个人小博客出现不可访问的状况,故在博客园亦留一份备份!
还请老师不要判为抄袭等,感谢!本人20级HIT学生,学号尾号230
第4篇
在本篇随笔中,我们主要介绍:
- 泛型中的类型擦除
什么是泛型的类型擦除?
我们在使用泛型时,总会在尖括号中输入某个具体的类型,譬如List<String>
,殊不知在编译阶段会将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),我们所规定的<String>
仿佛是规定了个寂寞。这个过程就叫做类型擦除。
类型擦除的原则
进行类型擦除时,需遵循一定的规则[1]:
- 消除类型参数声明,即删除<>及其包围的部分。
- 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
- 为了保证类型安全,必要时插入强制类型转换代码。
- 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。
类型擦除的例子
犹记得在刘老师的第九章课件第50页中,有这么一句话
List<String> is not a subtype of List<Object>
为什么这么说呢?结合类型擦除,我们很容易意识到,由于没有上下界限定,类型擦除后都会被替换成Object,因而两者最终应该是同一类。接下来通过查看List<String>
和List<Integer>
对应对象的getClass()
来验证这一点:
public class Hello {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
List<Integer> integerList = new ArrayList<Integer>();
stringList.add("先入一个字符串");
integerList.add(114514);
System.out.println(stringList.getClass() == integerList.getClass());
}
}
控制台输出true
,说明类型确实被擦除掉了,剩下了原始类型。在这里,根据上面所提到的规则,由于没有上下界限定,都会被替换成Object
::: tip 原始类型
原始类型[1:1],就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。
:::
泛型方法中的类型擦除
无论是声明了一个带泛型的变量、抑或是带泛型的类,其进行泛型擦除的原则已在上面提及。
但是,如果一个方法中带泛型,具体会被处理成什么样子呢?这里先下个结论:方法中的类型擦除仍遵循上面提及的擦除原则,但仍有一些额外的原则[1:2]:
- 在不指定泛型的情况下,泛型变量的类型为该方法中的几种类型的同一父类的最小级,直到Object。
该方法中的几种类型
本人理解为传入的各个参数的各个类型的最小父类,详见下面不远处的例子。 - 在指定泛型的情况下,该方法的几种类型必须是该泛型的实例的类型或者其子类
说的再简洁些:没指定泛型,类型就是最小父类;指定了泛型,类型就是所指定泛型的子类。
注:这里的指定泛型,指的是在调用泛型方法时,指明泛型类型。譬如给出一个泛型方法:
public static <T> T add(T x,T y){
return y;
}
采用如xxx.<Integer>add(1,2)
的方式调用方法,就叫做指定泛型了。
下面给出一个例子[1:3],在指定泛型和不指定泛型的情况下,对于泛型方法是如何进行类型擦除的:
public class Test {
public static void main(String[] args) {
/**不指定泛型的时候*/
int i = Test.add(1, 2); //这两个参数都是Integer,所以T为Integer类型
Number f = Test.add(1, 1.2); //这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number
Object o = Test.add(1, "asd"); //这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object
/**指定泛型的时候*/
int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能为Integer类型或者其子类
int b = Test.<Integer>add(1, 2.2); //编译错误,指定了Integer,不能为Float
Number c = Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float
}
//这是一个简单的泛型方法
public static <T> T add(T x,T y){
return y;
}
}
::: tip 和实验2中泛型方法声明不一样?
咦,这里和我们在实验2中所写的好像有些不一样呀?我们实验2中相关泛型方法在声明时,并没有<T>
嘞?
其实,在实验2中的ConcreteEdgesGraph
和ConcreteVerticesGraph
类的声明中,是包含了<T>
的:
public class ConcreteEdgesGraph<L> implements Graph<L> {
...
}
如果在类实例化时指定了类型(譬如ConcreteVerticesGraph<String>
),那么对于该类中所有返回T类型的方法,我们认为是已经被指定泛型的。
:::
到这里,我又想起来了老师课件里头所提及的java.util.Collections
的例子:
public static <T> void copy(
List<? super T> dest,
List<? extends T> src);
List<Number> source = new LinkedList<>();
source.add(Float.valueOf(3));
source.add(Integer.valueOf(2));
source.add(Double.valueOf(1.1));
List<Object> dest = new LinkedList<>();
Collections.copy(dest,source);
有同学问:注意到copy方法的声明中,dest和src对应的List中都有对泛型上下界的限定。在这个例子中,我们对于Collections.copy(dest,source);
的使用,是如何判断这么用合理的呢?
!注意,以下为个人见解,可能不准确或不正确,仅供参考!
之所以不好分析,是因为这个里头既有?
又有T
!把人给搞乱了!
::: tip 浅浅区分下?
与T
?
是通配符,泛指所有类型。一般用于定义一个引用变量,例如:
SuperClass<?> sup = new SuperClass<String>("lisi");
sup = new SuperClass<People>(new People());
sup = new SuperClass<Animal>(new Animal());
而我们方法上的<T>
代表括号里面要用到泛型参数,参数的类型为T
。
:::
咱们根据上面所介绍的规则,慢慢理下思路:
copy
是个泛型方法,在这个例子中,并没有指定泛型;- 首先根据类型擦除相关原则确定T:变量
dest
为List<Object>
类型的,变量source
为List<Number>
类型的。这里根据不指定泛型
的规则,以及在整个函数的声明中,我们并没有对T
有任何的约束,故再根据本文开头部分提到的类型擦除原则,确定下来T
应该是Object
- 接下来和类型擦除就没有关系了!是能否进行合法引用的问题了!
是看方法中List<? super Object> dest
(注意T
已经换成Object
了)这个引用,能否指向List<Object>
的对象;是看方法中List<? extends Object> src
这个引用,能否指向List<Number>
的对象;
结果是都能,因而合理。