前言
在学习Java的时候,泛型作为Java的一个重大特性,是一个不可避及的知识点.但在学习过程中发现子类对象存到ArrayList<父类>里面,读出来的时候,子类特有属性还在
,这也就是说,当元素存储进ArrayList时,并不是将子类对象强制转换成父类对象(舍弃特有属性存储的).而Java支持<? super A类>
,<? extends A类>
(这是一个A类泛型或者其子类泛型)也加深了我这一疑惑.
验证猜测
脚本
主类
1 | import java.util.ArrayList; |
Father类
1 | public class Father { |
Son1类
1 | public class Son1 extends Father{ |
Son2类
1 | public class Son2 extends Father{ |
测试结果
1 | 1 2 |
将ArrayList换成LinkedList类测试,结果一样
承上启下
在学习途中,我突然感觉这一现象并不是泛型引起的,而是Java的特性.
将子类对象存到ArrayList里面时,他并没有进行copy操作,而是简单的映射了地址,如果我们修改对象的值,ArrayList中的值也会改变.
验证一下
1 | import java.util.ArrayList; |
输出结果
1 | 1 2 |
对于产生这种误解的原因,大概是C++写多了.C++中将变量加到vector<类>
时,是通过复制来操作的.
验证
1 |
|
输出结果:
1 | 1 |
虽然问题解决了,但是C++和Java不同处理方式还有泛型原理我觉得学习一下还是很有意义的.
泛型分析
Java
Java的泛型是伪泛型。在编译期间,所有的泛型信息都会被擦除掉。正确理解泛型概念的首要前提是理解类型擦出(type erasure)。
泛型擦除
先看一个例子
1 | import java.util.ArrayList; |
输出结果
1 | class java.util.ArrayList |
可以看到虽然他们的泛型不一样,但class1和class2还是属于同一个类型,在运行时我们传入的类型变量String和Integer都被丢掉了。Java语言泛型在设计的时候为了兼容原来的旧代码,Java的泛型机制使用了“擦除”机制。
我们再来看一个例子
1 | import java.util.*; |
输出结果
1 | [E] |
我们构造了几个类并将他们实例化,但在获取这些对象类的类型参数时,我们发现打印的结果都是”形参”,没有任何关于它类型参数的信息.
编译器虽然会在编译过程中移除参数的类型信息,但是会保证类或方法内部参数类型的一致性。
泛型参数将会被擦除到它的第一个边界(边界可以有多个,重用 extends 关键字,通过它能给与参数类型添加一个边界)。编译器事实上会把类型参数替换为它的第一个边界的类型。如果没有指明边界,那么类型参数将被擦除到Object。下面的例子中,可以把泛型参数T当作HashF类型来使用。
1 | import java.util.*; |
输出结果
1 | [T] |
extend关键字后的类型信息决定了泛型参数能保留的信息。Java类型擦除只会擦除到HashF类型。
擦除原理
这边直接引用dreamGong写的博客了
我们通过例子来看一下,先看一个非泛型的版本:
1 | // SimpleHolder.java |
下面我们给出一个泛型的版本,从字节码的角度来看看:
1 | //GenericHolder.java |
在编译过程中,类型变量的信息是能拿到的。所以,set方法在编译器可以做类型检查,非法类型不能通过编译。但是对于get方法,由于擦除机制,运行时的实际引用类型为Object类型。为了“还原”返回结果的类型,编译器在get之后添加了类型转换。所以,在GenericHolder.class文件main方法主体第18行有一处类型转换的逻辑。它是编译器自动帮我们加进去的。
所以在泛型类对象读取和写入的位置为我们做了处理,为代码添加约束。
C++
函数模板
C++为我们提供了函数模板机制。所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。
凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现了不同函数的功能。
在c++中为每个模板的实例化产生不同的类型,而这样会产生大量的重复代码,这一现象又叫”模板代码膨胀”,感性趣的可以自行了解.