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

posted on 2018-03-08 16:24  Lemo_wd  阅读(215)  评论(0编辑  收藏  举报

导航