JAVA 泛型

为什么需要泛型

想象以下的场景,我们需要编写一个容器类,支持对数据的简单操作,例如增删改查,那么实现可以如下

public MyContainerOfString{
    private String[] container = new String[10];
    private int size;

    public void add(String data){
        container[size++] = data;
    }

    public String get(int index){
        return container[index];
    }

    ...
}

如上述代码所示,我们实现了一个可以存储String类型的容器,如果要实现一个Integer,Double等类型的容器,我们会发现需要写很多重复的代码,于是我们可以修改上述的代码,让其更灵活

public MyContainer{
    private Object[] container = new Object[10];
    private int size;

    public void add(Object data){
        container[size++] = data;
    }

    public Object get(int index){
        return container[index];
    }

    ...
}

上述的实现确实很灵活,可以支持任何类型,但是它也有一个问题,就是类型安全

MyContainer container = new MyContainer();
container.add(1);
container.add("A");

int value = (Interger)container.get(2) //运行失败

上述代码在编译期间不会报错,但是在运行时报错了,原因就是类型转换失败。

通过上述的场景我们不难看出使用泛型的好处

  1. 可以减少样本代码
  2. 可以保证类型安全

泛型类

泛型被用于类的定义则称之为泛型类,泛型类的通用写法

class 类名称<泛型标识>{

}

比较常见的泛型类可见于dao层,例如

class BaseDao<T> {

    public void save(T t){

    }

    public void delete(T t){

    }
}

泛型接口

泛型接口与泛型类相似,只不过用于定义接口,泛型类的通用写法

interface 接口名称<泛型标识>{

}

最常见的泛型接口即是各种容器接口,例如

public interface List<E> extends Collection<E>{

}    

泛型方法

泛型方法相比于泛型类更为复杂,下面是泛型方法的通用写法

public <T> 返回类型 方法名称(泛型标识){

}

其中是用来声明该方法是一个泛型方法,是否是一个泛型方法就看它是否有这个标识,例如

public void save(T t)  //不是泛型方法

public <T> void save(T t) // 泛型方法

下面是一个泛型方法的示例

public <T> List<T> fromArrayToList(T[] a) {   
    return Arrays.stream(a).collect(Collectors.toList());
}

一个泛型方法可以处理多个泛型类型,但是有几个泛型类型就需要声明几个,例如

public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
    return Arrays.stream(a)
      .map(mapperFunction)
      .collect(Collectors.toList());
}    

泛型擦除(Type Erasure)

泛型擦除可以简单理解为泛型只在编译阶段生效,在编译阶段,正确检验泛型结果后,会将泛型的相关信息擦除,在运行阶段是看不到泛型信息的

List<Integer> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
Class<? extends List> list1Class = list1.getClass();
Class<? extends List> list2Class = list2.getClass();

if (list1Class.equals(list2Class)){
    System.out.println("same class");
}

运行程序可以看到输出”same class”,由此可以证明泛型信息在运行间已被擦除。总结来说,泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

那么我们有办法在运行阶段拿到泛型信息吗?答案是有的,但是前提是泛型是类声明的一部分,

public class CatCage implements Cage<Cat>

此时我们可以通过反射获取泛型

(Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];    

泛型边界

在声明泛型时,我们可以指定泛型的边界,所谓边界其实就是子类与父类的关系,例如

class Test<T extends Number> // T必须是Number的子类

class Test<T super Number>  // T必须是Number的父类

我们再来看下定义边界有什么用处

class Cage<T> {

}

Cage<Animal> cage = new Cage<Cat>() //编译失败

可以发现装猫的笼子无法转换为装动物的笼子。因此可以得出结论,虽然cat和animal是存在继承关系,但是泛型Cage和Cage却不存在继承关系,为了能够表示泛型间的继承关系,因此才有了泛型边界,可以把上述例子修改下,即可运行

class Cage<T extend Animal>{}

PECS

我们如何选择使用上边界还是使用下边界呢?原则就是PECS:Producer extend,Consumer super,

所谓的Producer extend是指当我们是”生产”泛型对象时,我们就用extend

public static void makeLotsOfNoise(List<? extends Animal> animals) {
    animals.forEach(Animal::makeNoise);   
}

对于上述例子中,我们不断的从animals中取出(生产)对象,执行makeNoise方法,因此使用extend

public static void addCats(List<? super Animal> animals) {
    animals.add(new Cat());   
}    

对于上述例子中,我们不断的往animals中增加(消费)对象,因此使用super

泛型通配符

 public void print(List<?> list){
    list.forEach(Object::toString);
}

其中?就是通配符,表示未知的类型,?代表的一种具体的类型,例如Number,Integer

总结

  1. 使用泛型可以减少样本代码并保证类型安全
  2. 泛型方法一定需要用进行声明
  3. 所谓泛型擦除是指泛型信息只在编译阶段生效,在运行阶段,已经看不到泛型信息
  4. 使用PECS原则来决定使用上边界还是下边界

参考文献

  1. Java Generics Interview Questions (+Answers)
  2. The Basics of Java Generics
  3. java 泛型详解
  4. 深入理解Java泛型

Reprint please specify: wbl JAVA 泛型

Previous
网路层之IP协议 网路层之IP协议
IP协议提供一种尽力投递(best-effors,即不提供任何保证)的方法将数据从源端传递到目标端,它不关心源机器和目标机器是否在同样的网路中,也不关心他们之间是否还有其他网路。下面来看下IPv4协议头部组成。 IPv4协议头部IPv4的头
Next
Floyd算法 Floyd算法
算法概述弗洛伊德算法,是解决任意两点间的最短路径的一种算法。它的时间复杂度为 O(N^3)空间复杂度为 O(N^2)。 算法描述弗洛伊德算法其实采用的是动态规划 设F(i,j,k)表示节点i到节点j只以(1,k)之间的节点作为中间节点的最短