Java 中的泛型
一、泛型的定义与作用
泛型,又叫 参数多态,在c++里又叫模板。是指声明与定义函数或变量时不指定其具体的类型,而把这部分类型作为参数使用,使得该定义对各种具体类型都适用。通常意义的多态是将这里不特定的类型参数替换成父类型,将具体类型的实现替换成子类型。故而泛型可看作一种特殊的多态(概念角度)。
泛型在强类型的编程语言中普遍作用是:
1. 可以显式地使用参数注入,实现代码复用(避免粗暴地使用 Object)
2. 加强编译时的类型安全(类型检查)
3. 减少类型转换的次数
二、Java 中的 泛型
编译时进行类型擦除生成与泛型类同名的原始类,但类型参数都被删去了。类型变量由类型限界代替(通常为 Object)。
三、协变
Java 泛型不支持协变,数组是协变的。
注:当我们用型別构造出更复杂的型別,原本型別的子型別性质可能被保持、反转、或忽略。比如 Cat 之于 Animal 与
IEnumerable<Cat> 之于
IEnumerable<Animal>其
子型別关系被保持,因此称 IEnumerable<T> 具有协变性质。
四、泛型擦除
问题1: 什么时候需要避免泛型擦除?
使用泛型,程序在编译时,类型会完全擦除或保留部分(如果定义了上下限)。
涉及到具体类型的操作时会自动加上强制转换。比如:
public static void main(String[] args) { List<Integer> m = new ArrayList<>(); m.add(3); System.out.println(m.get(0) + 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”
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(); } }
以上代码由于类型擦除,会被编译成:
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)”
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: 如何避免泛型擦除?
第一,使用泛型边界。可以有限地保留类型信息。如:
LinkedList<? extends Building>,LInkedList<? super House>
分别设置了上界和下界。在这种情况下编译器不会将泛型信息完全擦除,比如上述两个List经过编译后分别编程了LinkedList<Building> 和 LinkedList<House>。
举例:
使用泛型边界解决Java中集合泛型不支持协变的问题
定义接口 PvItem
public interface PvItem { Long getId(); void setPv(long pv); void setUv(long uv); }
错误做法(该方法签名只能接收 List<PvItem> 作为参数)
void processItems(String itemType, List<PvItem> items);
正确做法(可接收各种实现了 PvItem 接口的 List 类作为参数)
<T extends PvItem> void processItems(String itemType, List<T> items);
第二,借助类型推断隐式确定具体参数类型,常用于函数式编程类型推断(需要编程语言支持)。
第三,不直接使用泛型。比如定义一个新的类型,继承或实现原泛型类或接口,并且注入具体的参数类型,则不会发生类型擦除。如:
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