泛型擦除到底擦除了啥

前言

通过对比其他语言中的泛型机制,简单了解一下 Java 泛型擦除会有什么影响。

Java 泛型擦除

我们知道 Java 通过泛型机制,实现了参数化类型。使用泛型可以写出更通用的代码,Java 集合类就是最好的范例,泛型使我们写出的代码不再依赖具体的类型,甚至不是 Object ,而是更具有约束性的类型,可以说是非常强大的特性。

但是,在我们学习 Java 泛型的时候,所有教材都会提及 Java 泛型擦除这个概念。

Java 泛型是使用擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都 被擦除了,你唯一知道的就是你在使用一个对象。

坦白说初学者看到这样的解释是很懵逼的,任何具体的类型信息都被擦除了 ?那我用泛型岂不是用了个寂寞。对于没有使用过其他语言中泛型机制的同学,这样的解释让本就对泛型机制一知半解的同学更加迷惑了。总得知道一个东西是什么样子的,才能感受到没有了之后的感觉吧。

当然,在日常开发中随着逐渐使用 Java 集合或者是 RxJava 这类通过泛型机制封装的框架,也开始逐渐感受到了泛型机制的魅力和强大之处,对于类和方法之上声明的各类 <T>,<R,T> 也不再感到别扭,对于 PECS 机制的理解也逐渐加深。但是,对于泛型还有一个疑问,没有擦除机制的泛型又是什么样的呢?

Dart 泛型

这里以 Dart 泛型为例,来感受一下未经擦除的泛型是什么样的,有何神奇之处。

我们看下面这段代码。

 

dart

复制代码

import 'dart:collection'; class Holder { HashMap<dynamic, dynamic> map = HashMap(); void putValue<T>(T value) { print("put a $T"); map[T.hashCode] = value; print(map); } T getResult<T>() { print("getResult Type is $T"); return map[T.hashCode]; } } class People {} class Animal {} void main() { Holder demo = Holder(); demo.putValue(People()); demo.putValue(Animal()); People p = demo.getResult(); print(p); Animal m = demo.getResult(); print(m); }

可以用 dartpad 直接运行看一下输出结果。

 

shell

复制代码

put a People {32079754: Instance of 'People'} put a Animal {32079754: Instance of 'People', 442803939: Instance of 'Animal'} getResult Type is People Instance of 'People' getResult Type is Animal Instance of 'Animal'

  1. 在 Holder 类中创建了 key-value 均为 dynamic 类型的 Map ,意味着这是一个可以存放任何类型数据的 Map。
  2. 这里 putValue 和 getResult 两个泛型方法中,可以直接把泛型参数当做普通的类型使用了,T 不在只是一个符号,而是一个具体的类型,输出其类型,甚至获取其 hash 值。
  3. 从输出结果可以看到,在泛型方法中获取到的运行时的真实类型。

Kotlin 泛型

Kotlin 作为一种可以兼容 Java 的语言,其泛型的实现也是擦除实现的。我们直接仿照 Dart 的语法去实现相同的逻辑会怎样呢?

 

kotlin

复制代码

class EasyHolder { val map: HashMap<Any?, Any?> = HashMap() fun <T> putValue(t: T) { println(T::class.java) // lint error } fun <T> getResult(): T? { T::class // lint error return null } }

很明显编辑器会报错,提示 Cannot use 'T' as reified type parameter. Use a class instead. 。也就是说这里 T 只是一个符号而已,他并不能代表一个类型,不是一个类。那么有什么办法解决这个问题吗?当然可以,IDE 会提示我们添加 inline 和 reified 关键字。

 

kotlin

复制代码

class SuperHolder { val map: HashMap<Any?, Any?> = HashMap() inline fun <reified T> put(t: T) { println(T::class.java) map[T::class.java.hashCode()] = t } inline fun <reified T> getResult(): T { return map[T::class.java.hashCode()] as T } fun printAll() { println(map) } }

可以看到这里已经可以把 T 当做一个普通的 Java/Kotlin 类使用了,他保留了自己作为参数的原始类型。我们可以验证一下。

 

kotlin

复制代码

data class People(val name: String) data class Animal(val age: Int) fun main() { val superHolder = SuperHolder() superHolder.put(People("mike")) superHolder.put(Animal(1)) superHolder.printAll() val people: People = superHolder.getResult() val animal: Animal = superHolder.getResult() println(people) println(animal) }

output

 

shell

复制代码

class com.generic.People class com.generic.Animal {1637070917=Animal(age=1), 1848402763=People(name=mike)} People(name=mike) Animal(age=1)

通过结果我们可以得出以下结论

  • put 操作时,可以直接获取到类型参数真实的类型。
  • holder 中 map 内存储的也是真实的类型
  • 从 holder 中获取的内容就是想要的内容。

这里 getResult() 方法在没有声明具体的类型时,他是怎么知道该返回什么类型的呢?这里其实用到了类型推导这个特性。通过 = 左边的定义的类型,相当于强行约束了 = 右边的返回类型。上面 Dart 也是一样的,类型推导真是 YYDS 。

 

kotlin

复制代码

val people: People = superHolder.getResult<>() val people: People = superHolder.getResult<People>()

上面两种写法是等价的,而第一种写法更简单。当然,这里相比 dart ,在 Kotlin 中 getResult 方法需要做一次类型转换,这是因为 Map 本身做了擦除。

reified

Kotlin 通过提供 reified 关键字可以说是强化了泛型的能力。通过给方法的泛型参数添加 reified 关键字,就可以在运行时获取到泛型参数真实的类型,这样在很多时候可以解放生产力,写出功能更强大的框架,简化代码逻辑。

我们以上文 通过 hilt 反观依赖注入 中提到的接口注入为例。我们可以通过 reified 提供的便利性,非常方便的实现一个简单的依赖注入框架。

接口的定义与实现
 

kotlin

复制代码

interface IVideoPlayer { fun play() ... } class IVideoPlayerImpl : IVideoPlayer { override fun play() { Log.d(TAG, "play() called") } ... }

接口的注入与获取(单例)
 

kotlin

复制代码

object InjectHolder { val map: HashMap<Any?, Any?> = HashMap() inline fun <reified T> put(t: T) { if (T::class.java.interfaces.size > 1) { throw IllegalArgumentException("${T::class.qualifiedName} implements too many interface") } for (clazz in T::class.java.interfaces) { map[clazz.hashCode()] = t } } inline fun <reified T> getResult(): T? { return map[T::class.java.hashCode()] as T } fun printAll() { println(map) } }

这里 put 方法注入的是接口的具体实现,通过接口实现可以获取到具体的接口。因此,就可以通过 map 存储 接口和接口实现的 key-value 键值对。后续使用时,就可以通过接口的定义类获取到接口的实现类。

应用层使用
 

kotlin

复制代码

fun main() { // 这里只是为了方便,实际上接口实现的注入可以在任意位置 InjectHolder.put(IVideoPlayerImpl()) InjectHolder.printAll() val player: IVideoPlayer = InjectHolder.getResult() player?.play() }

di.png

通过上图可以简单理解一下

  • 应用层 App 只依赖抽象的接口定义和 InjectHoder , InjectHolder 聚合所有接口和其实现类的映射关系,对他来说 map 里存储的内容是黑盒的。
  • 接口注入可以非常灵活的实现,InjectHolder.put(xxxImpl) 可以在任何方便的模块里调用,或者在合适的位置统一注入所有需要的接口,甚至是懒加载使用时再进行注入。
  • 而上层的 App 只需要依赖接口的定义即可。需要什么接口,直接从 InjectHolder 获取就好了,类型信息可以通过定义推导出来。只要之前注入过,就一定可以获取到。这样上层 App 就是完全依赖接口,对接口的实现无感知,从而实现了解耦。

以上思路参考自 Flutter 开发框架 getx,Get 框架是真的强大。

Java 泛型

回过头我们再来看 Java 的泛型。

我们尝试用 Java 实现上面的特性。

 

java

复制代码

class SimpleHolder { private static Map<Object,Object> holder = new HashMap(); public static <T> void setValue(T t) { System.out.println(T);// holder.put(t.hashCode(),t); } public static <T> T getResult() { T t ; System.out.println(""); return null; } }

可以看到在 Java 泛型方法中,类型参数的类型被擦除之后,无法基于其做任何操纵。

Java 中泛型使用最广的无疑是集合了,以我们最熟悉的 ArrayList 为例。

 

java

复制代码

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { transient Object[] elementData; // non-private to simplify nested class access @SuppressWarnings("unchecked") E elementData(int index) { return (E) elementData[index]; } public E get(int index) { Objects.checkIndex(index, size); return elementData(index); } public E set(int index, E element) { Objects.checkIndex(index, size); E oldValue = elementData(index); elementData[index] = element; return oldValue; } ... }

通过将类型参数化,ArrayList 在编译期就可以保证这个集合中类型的一致性。放进去的什么类型,取出来的一定是什么类型。但是,这里要注意的时 elementData 也是通过类型强转返回了结果。

小结

至此,我们知道 Java 泛型中所谓的泛型擦除到底是把什么信息给抹掉了。至于 Java 泛型这样做的理由,教科书上也解释过了,纯粹了为了做向前兼容。作为开发者,觉得这也是一个痛苦的选择吧。一个生命周期较长的语言,在发展的过程中必然有很多历史包袱,在各类语言百花齐放,各自争相吸取别人优点的时候,老语言势必会显得更笨重,有些特性只能是阉割过的特性。

编程语言中泛型出现的初衷是通过解耦类或方法与所使用的类型之间的约束,使得类或方法具备最宽泛的表达力。通过对比不同语言中泛型的特点,可以让我们更好的理解泛型,知道在每一个语言中,泛型能做什么,不能做什么。

 

<think>好的,我现在要回答用户关于Java擦除的问题。首先,我需要整理一下已有的引用资料,确保理解正确。用户提供了五个引用,内容涉及擦除的原理、实现方式、擦除规则、影响以及相关的限制和解决方案。 首先,擦除的原理是什么?根据引用2和引用3,Java的在编译时会被擦除,替换为原生类型,并在需要的地方插入强制转换。这意味着在运行时,类型信息不存在,比如ArrayList<String>和ArrayList<Integer>在运行时都是同一个类。 接下来,擦除的规则。引用1和引用4提到有无界擦除、上界擦除和下界擦除。例如,无界的参数会被替换为Object,有上界的替换为上界类型,有下界的可能替换为Object。需要确认这些规则的具体情况,以及如何验证这些规则,比如通过反射或者查看字节码。 然后是影响部分。用户可能关心擦除带来的问题,比如类型信息在运行时不可用,导致无法直接实例化类型,或者无法进行类型检查。引用4中提到了两个经典问题:在类中获取实际类型和在接口回调中获取类型,可能需要通过反射技巧来解决,比如获取父类的参数化类型。 另外,引用5提到原始类型不支持,比如不能使用List<int>,而必须用包装类,这会带来自动装箱的开销。同时,项目Valhalla可能在未来解决这个问题,但现在还是限制。 现在需要将这些信息组织成结构清晰的回答,分点解释原理、影响,并举例说明。同时,确保使用正确的中文表达和LaTeX格式,比如代码块用反引号,数学公式用$...$,独立公式用$$...$$。不过根据用户提供的示例,似乎数学公式的例子并不直接相关,可能在这个问题中不需要使用数学公式,但需要遵循格式要求。不过用户的问题是关于Java,可能涉及代码示例,所以需要正确使用代码块的格式。 最后生成相关问题,可能包括擦除的优缺点,如何绕过擦除限制,桥接方法的作用,类型擦除类型安全的冲突,以及原始类型不支持的原因等。 需要检查引用是否正确标注,比如引用[^1]到[^5]是否正确对应提供的资料。根据用户提供的引用,引用1到5分别对应不同的内容,需要确保在回答中正确引用,比如提到桥接方法时引用,提到擦除原理时引用[^2][^3],原始类型限制引用[^5]等。 现在组织回答的结构: 1. 擦除的原理:编译时擦除,替换为原生类型,插入强制转换。 2. 擦除规则:无界、上界、下界擦除的具体情况。 3. 主要影响: - 运行时类型信息丢失 - 类型检查限制 - 桥接方法的作用 - 原始类型不支持 4. 解决方案和技巧:反射获取信息,桥接方法等。 5. 示例代码:比如类型擦除后的代码示例。 6. 相关问题。 在回答过程中,需要确保每个引用的位置正确,比如提到桥接方法时引用,提到擦除实现引用[^2],原始类型问题引用。 另外,用户可能对类型擦除带来的类型安全问题有疑问,比如为什么Java选择这种方式,可能与兼容性有关,保持与旧版本字节码的兼容性,引用2提到这是为了作为语法糖实现,确保兼容性。 可能还需要提到类型擦除导致的一些常见错误,比如无法创建数组,或者instanceof无法使用类型,这些可以结合引用中的例子说明。 检查是否有遗漏的重要点,比如桥接方法在保持多态性的作用,引用1中的演示案例可能需要解释,但在回答中可能需要简化。 最后,确保回答符合用户的要求:结构清晰,分步骤解释,真实可靠,引用正确,使用中文,代码块正确,生成相关问题。</think>### Java擦除原理及影响解析 #### 一、擦除原理 Java通过**类型擦除(Type Erasure)**实现,其核心规则为: 1. **编译期擦除**:类型信息在编译后被替换为原始类型(如`Object`或指定上界类型),并插入强制类型转换代码。 - 无界类型(如`<T>`)擦除为`Object`,例如`List<T>`变为`List`。 - 有上界的类型(如`<T extends Number>`)擦除为上界类型`Number`。 - 有下界的类型(如`<? super Integer>`)擦除为`Object`。 2. **运行时无信息**:类在运行时会丢失类型参数信息,例如`List<String>`与`List<Integer>`在JVM中均为`List`。 ```java // 编译前 List<String> list = new ArrayList<>(); // 编译后(通过反编译查看) List list = new ArrayList(); // 类型参数被擦除 String s = (String) list.get(0); // 插入强制转换 ``` #### 二、主要影响 1. **运行时类型限制** - **无法通过反射获取类型**:例如`new ArrayList<String>().getClass()`无法得知`String`类型参数[^4]。 - **无法直接实例化对象**:`new T()`会编译报错,因为实际类型未知[^3]。 2. **类型安全检查局限性** - 类型转换错误可能在运行时才暴露。例如将`List<Integer>`强制转换为`List<String>`时,编译期无法检测。 3. **桥接方法(Bridge Method)** - 为实现多态性,编译器会生成桥接方法。例如类继承时,子类需通过桥接方法覆盖父类方法[^1]。 4. **原始类型不支持** - 如`List<int>`不合法,需使用包装类`Integer`,导致自动装箱/拆箱的性能开销。 #### 三、解决方案示例 1. **获取实际类型** 通过反射获取父类或接口的参数: ```java public class MyList<T> { public Class<T> getType() { return (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; } } ``` 2. **绕过实例化限制** 使用工厂方法传递`Class<T>`参数: ```java public static <T> T createInstance(Class<T> clazz) throws Exception { return clazz.getDeclaredConstructor().newInstance(); } ``` ####
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值