这几天忙着堆代码,没时间写东西,今天翻到以前写的一篇文章。
说起泛型,做开发的小伙伴可以说是驾轻就熟,可以说已经成为一种编码习惯了。
使用泛型的好处:
- 类型参数化,可以把类型当作参数传递,意义非凡;
- 类型安全;
- 消除类型转换,减少装箱拆箱,提高性能;
- 屏蔽数据细节,开发人员能够更专注于算法;
优秀点的 Java 开发当然会知道得更多一点,比如说 Java 里的泛型机制使用了一种称为类型擦除的技术,听起来很高级的感觉,简单来说是这样:
编译前 编译后
List=>List
编译后的类型参数统一转换为 Object,而真正的泛型参数被丢弃,这就叫类型擦除。 可能聪明的小伙伴已经发现了,这种方式只保证了编译时的类型安全,JVM 并不知道啥叫泛型。
有个小插曲,几年前作为供应商在客户场地带领着一个 Java 团队维护一个古老的系统(没错,我不是 Java 开发,生活就是这么滑稽),每天的工作就是客户沟通、需求分析、任务分配之类的,很是悠闲。 一天团队里一个小伙带着生无可恋的表情找到我,说遇到问题搞不定了,让我去看下。 调试了几次之后定位到问题,问题原因是这样:
系统里有一个这样的方法(之前的供应商已经跑路,代码 404):
List getXXXX();
返回的结果里,有一个 Integer 元素,而 List 强制转型触发了类型转换异常,因为异常是容器里抛出来的,所以会给缺乏经验的人带来一些困扰。
后来问过几个经验丰富的 Java 开发,能否在 List 里放入 Integer ? 得到的答案都是否定的,看来流水线式开发容易犯同样的错误。于是写了这段测试代码来证明:
测试代码利用了 Java 泛型的缺陷,向 List 里插入了 Integer。 可能有人会觉得没啥意义,吃饱了撑的才会这样写代码,但是这是一个风险点,尤其是在大量使用第三方组件或者开源项目的情况,例如你用了组件 A,A 又用到 B,一直到了 X,一旦中间环节出现类似问题,就容易踩到地雷。
总结下 Java 泛型的问题:
- 泛型仅在编译时存在,运行时没有任何意义,类型安全得不到保障;
- 类型转换仍然存在,只是对开发者不可见; (当然 Java 没有值类型的概念,性能损失可以忽略)
- 不能动态的使用泛型,如使用反射的场景;
可能 Java 这么干是为了兼容性,又或者是不想去折腾字节码,但是这么奇葩的泛型机制,有点像是拿糖哄小孩不哭。
再来看看其他语言的泛型是怎么实现的。
- C++: 使用模板实现,编译时自动推导出在目标类型,可以确保类型安全,但存在类型膨胀的问题。
- C#: 泛型参数被编译成元数据,运行时通过 JIT 即时编译为具体类型,因此能实现泛型的所有功能,没有类型膨胀的问题,而且可以对泛型参数安全的向上或向下转型(协变、逆变)。
写此文的目的不只是吐槽,而是提醒 Java 开发者,调用别人的东西时拿到的数据不一定是安全的,一旦遇到这种问题,定位和解决问题会浪费大量的时间。