Java 中的泛型
一、泛型的定义与作用
泛型,又叫 参数多态,在c++里又叫模板。是指声明与定义函数或变量时不指定其具体的类型,而把这部分类型作为参数使用,使得该定义对各种具体类型都适用。通常意义的多态是将这里不特定的类型参数替换成父类型,将具体类型的实现替换成子类型。故而泛型可看作一种特殊的多态(概念角度)。
泛型在强类型的编程语言中普遍作用是:
1. 可以显式地使用参数注入,实现代码复用(避免粗暴地使用 Object)
2. 加强编译时的类型安全(类型检查)
3. 减少类型转换的次数
二、Java 中的 泛型
编译时进行类型擦除生成与泛型类同名的原始类,但类型参数都被删去了。类型变量由类型限界代替(通常为 Object)。
三、协变
Java 泛型不支持协变,数组是协变的。
注:当我们用型別构造出更复杂的型別,原本型別的子型別性质可能被保持、反转、或忽略。比如 Cat 之于 Animal 与
IEnumerable<Cat> 之于
IEnumerable<Animal>其
子型別关系被保持,因此称 IEnumerable<T> 具有协变性质。
四、泛型擦除
问题1: 什么时候需要避免泛型擦除?
使用泛型,程序在编译时,类型会完全擦除或保留部分(如果定义了上下限)。
涉及到具体类型的操作时会自动加上强制转换。比如:
1 2 3 4 5 | public static void main(String[] args) { List<Integer> m = new ArrayList<>(); m.add( 3 ); System.out.println(m.get( 0 ) + 5 ); } |
编辑器会替换为:
1 2 3 4 5 | public static void main(String[] args) { List m = new ArrayList(); m.add( 3 ); System.out.println((Integer)m.get( 0 ) + 5 ); } |
类型擦除还会造成以下编译时错误:
错误 ① “Erasure of method xyz(…) is the same as another method in type Abc”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class App { public int process(List<Person> people) { for (Person person : people) { log.info( "Processing person: " + person.toString()); } return person.size(); } public int process(List<Employee> employees) { for (Employee employee : employees) { log.info( "Processing employee: " + employee.toString()); } return employees.size(); } } |
以上代码由于类型擦除,会被编译成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class App { public int process(List people) { for (Person person : people) { log.info( "Processing person: " + person.toString()); } return person.size(); } public int process(List employees) { for (Employee employee : employees) { log.info( "Processing employee: " + employee.toString()); } return employees.size(); } } |
那么存在的错误显而易见。两个具有相同签名 ( process(List) ) 的方法无法在同一个类中共存,简单的解决方法是修改某一个方法的签名。
错误 ② “The method xyz(Foo) in the type Abc is not applicable for the arguments (Foo)”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class App { public int processPeople(List<Person> people){ for (Person person : people) { log.info( "Processing person: " + person.toString()); } return person.size(); } .. } .. List<Employee>employees; employees = new ArrayList<>(); employees.add(employee1); employees.add(employee2); App app = new App(); // ERROR ON NEXT LINE! app.processPeople(employees);.. |
这个错误的原因是泛型集合不具有协变性。无法强制转换。
类型擦除有时还会影响序列化性能(因为不知道类型信息,从而无法针对性地选择序列化方式,因此影响了序列化的性能)。
问题2: 如何避免泛型擦除?
第一,使用泛型边界。可以有限地保留类型信息。如:
1 | LinkedList<? extends Building>,LInkedList<? super House> |
分别设置了上界和下界。在这种情况下编译器不会将泛型信息完全擦除,比如上述两个List经过编译后分别编程了LinkedList<Building> 和 LinkedList<House>。
举例:
使用泛型边界解决Java中集合泛型不支持协变的问题
定义接口 PvItem
1 2 3 4 5 6 7 8 | public interface PvItem { Long getId(); void setPv( long pv); void setUv( long uv); } |
错误做法(该方法签名只能接收 List<PvItem> 作为参数)
1 | void processItems(String itemType, List<PvItem> items); |
正确做法(可接收各种实现了 PvItem 接口的 List 类作为参数)
1 | <T extends PvItem> void processItems(String itemType, List<T> items); |
第二,借助类型推断隐式确定具体参数类型,常用于函数式编程类型推断(需要编程语言支持)。
第三,不直接使用泛型。比如定义一个新的类型,继承或实现原泛型类或接口,并且注入具体的参数类型,则不会发生类型擦除。如:
1 2 3 4 5 6 7 8 9 10 | env.fromElements( 1 , 2 , 3 ) .map(i -> new DoubleTuple(i, i)) .print(); public static class DoubleTuple extends Tuple2<Integer, Integer> { public DoubleTuple( int f0, int f1) { this .f0 = f0; this .f1 = f1; } } |
233
参考
https://www.ibm.com/developerworks/cn/java/java-language-type-erasure/index.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix