全文分为 视频版 和 文字版 ,
- 文字版 : 文字侧重细节和深度,有些知识点,视频不好表达,文字描述的更加准确
- 视频版: 视频会更加的直观,看完文字版,在看视频,知识点会更加清楚
在之前的文章 Kotlin 和 Java 泛型的缺陷和应用场景 中介绍了:
- 为什么要有泛型
- Kotlin 和 Java 的协变和逆变的区别和应用场景,
- Java 数组协变的缺陷
- 通配符
<? extends>
、<? super>
、<out>
、<in>
的区别和应用场景
而今天这篇文章我们主要介绍泛型擦除和它的局限性,所以通过这篇文章你将学习到以下内容:
<pre class="hljs delphi" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 0.75em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">Object
Class
?
</pre>
泛型擦除我相信对于每个开发者并不陌生,先写一段示例代码,实例化泛型 <T>
,我们花三秒钟思考一下,下面的代码是否可以正常编译。
<pre class="prettyprint hljs groovy" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">// Java
public class GenericPersonJava<T> {
GenericPersonJava() {
T t = new T()
}
}
//Kotlin
class GenericPersonKt<T> {
init {
val t: T = T()
}
}</pre>
编译会出错,因为 <T>
被擦除之后,会将 <T>
编译成 Object
, JVM 指令如下图所示。
如果传入的泛型参数是 String
, 那么泛型被擦除之后,会编译成 Object
类型,类型显然不对,无论是 Java 还是 Kotlin 在编译阶段都无法确定泛型参数的类型,所以为了防止类型问题,编译器不支持直接对泛型实例化, 这也是泛型的局限性,不能直接对泛型实例化 。
如果想实例化泛型,Java 和 Kotlin 通用解决方案,通过 Class
反射的方式来实例化泛型 <T>
,我们修改一下上面的代码,即可正常编译运行。
<pre class="prettyprint hljs kotlin" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">// Java
public class GenericPersonJava<T> {
GenericPersonJava(Class<T> classes) {
T t = classes.newInstance();
}
}
// kotlin
class GenericPersonKt<T>(classes: Class<T>) {
init {
val t: T = classes.newInstance()
}
}</pre>
我们在思考一下泛型 <T>
被擦除之后,一定会编译成 Object
类型吗? 修改一下上面的代码,给泛型添加上界,在花 3 秒钟思考一下编译后的泛型 <T>
是什么类型。
<pre class="prettyprint hljs scala" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">// Java
public class GenericPersonJava<T extends String> {
}
// Kotlin
class GenericPersonKt<T : String>() {
}</pre>
修改后的代码,给泛型添加了上界,即泛型 <T>
继承自 String
,泛型被擦除之后,会被编译成 String
类型,如下图所示。
因此我们可以得出一个结论,无论是 Java 还是 Kotin:
- 如果泛型没有指定上界
<T>
,泛型被擦除之后,会被编译成Object
类型 - 如果泛型指定了上界,例如
<T : String>
,泛型被擦除之后,会被编译成String
类型
无法获取泛型 Class 类型
我们在来介绍一下泛型另外一个局限性 无法获取泛型 Class 类型 ,先写一段代码,我们在花 3 秒钟思考一下,以下代码输出的结果是什么。
<pre class="prettyprint hljs kotlin" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">// Java
public static void main(String... args) {
List<Integer> p1 = new ArrayList<Integer>();
List<Double> p2 = new ArrayList<Double>();
System.out.println(p1.getClass() == p2.getClass());
}
// Kotlin
fun main() {
val p1: List<Int> = ArrayList<Int>()
val p2: List<Double> = ArrayList<Double>()
println(p1.javaClass == p2.javaClass)
}</pre>
上面的代码 Java 和 Kotlin 输出的结果都是 true
,因为泛型被擦除了之后,无论 ArrayList<Integer>
还是 ArrayList<Double>
获取 class
的时候,获取到的都是同一个 ArrayList class
。
所以对于一个泛型 ArrayList<T>
无论 <T>
是什么类型,编译完了之后都会被擦除掉,最后获取到的都是 ArrayList class
而不是 ArrayList<T> class
。
为什么要擦除掉泛型?
泛型是 Java 1.5
之后引入的,在之前的版本是没有泛型这个概念,所以为了兼容之前的版本,因此在生成字节码的时候,将泛型信息擦除掉了。
泛型信息被擦除,真的不存在了吗
我们在思考一下泛型信息被擦除了之后,泛型信息真的不存在了吗?我们先写一段代码,看一下编译后的 JVM 指令。
<pre class="prettyprint hljs java" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">public class GenericJava {
public static void main(String... args) {
ArrayList<Integer> p1 = new ArrayList<Integer>();
}
}</pre>
生成的 JVM 指令如下图所示。
标记 1执行 New
命令创建了 ArrayList
对象,而不是 ArrayList<Integer>
,由此可见泛型信息被擦除了。我们继续往下看有个 LocalVariableTable
和 LocalVariableTypeTable
。
-
LocalVariableTable
就是我们常说的局部变量表,保存方法参数列表和方法内的局部变量,按照声明的顺序存储,它以数组的形式展示,更多关于 局部变量表 和 操作数栈 的知识点,欢迎前往查看另外一个篇文章 CPU 如何记录函数调用过程和返回过程 。 -
LocalVariableTypeTable
它的数据结构和LocalVariableTable
是一样的,只不过它是用来保存泛型信息
正如图中 标记 2 所示,我们在代码中编写的泛型 ArrayList<Integer>
,泛型信息被擦除掉之后,会保存到 LocalVariableTypeTable
中,所以并没有真正意义上的将泛型相关的信息抹除掉。正因为泛型信息被保存了下来,所以我们在运行时,可以通过反射获取到泛型相关的信息。
无论使用 Java 还是 Kotlin, 定义在类型的上的泛型、定义在方法参数的泛型,定义在方法返回值的泛型、定义在局部变量的泛型、还是定义在全局变量中的泛型,它们的泛型信息都会保存下来。如下图所示。
? 号和星投影
Koltin 中的星投影,即泛型 <*>
,其实等效于 Java 中的 <?>
号,用于表示不确定泛型是什么类型的信息。我们来看一段代码。
<pre class="prettyprint hljs php" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">// Java
List<?> list;
// Kotlin
val list: List<*></pre>
上面的代码其实等效于下面的代码。
<pre class="prettyprint hljs php" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">// Java
List<? extends Object> list;
// Koltin
val list: List<out Any></pre>
- Java 中的通配符
<?>
号等效于<? extends Object>
- Kotlin 中的通配符
<*>
号等效于<out Any>
在 Java 中 Object
类是所有类的父类,在 Kotlin 中分为 非空 和 可空 两种类型,因此 Any
是所有非空类型的父类,而 Any?
是所有可空类型的父类。因此我们可以用 Object
和 Any
来表示。
在 Java 中用通配符 ? extends
表示协变,而在 Kotlin 中关键字 out
表示协变,关于协变和逆变更多的知识点,可以前往查看 90%的人都不知道的知识点,Kotlin 和 Java 的协变和逆变 。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。
网友评论