前端视角 Java Web 入门手册 2.4.1:集合框架——泛型

255 阅读5分钟

什么是范型

C#、Java、TypeScript、Swift 等多种语言都支持范型,范型的定义有多种表述

  • 泛型指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性
  • 泛型允许在强类型程序语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型
  • 泛型把类型当作是参数一样传递,类型明确的工作推迟到创建对象或调用方法的时候

Java 集合大量的使用了范型,ArrayList 在定义时候并没有限定类型

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

在实例化时候通过 <string><int>传入具体类型

List<String> strList = new ArrayList<String>;
List<int> numList = new ArrayList<int>;

为什么使用范型

在 Java 1.5 支持范型之前,集合框架(如 ListSetMap 等)只能存储 Object 类型的元素,范型程序设计的需求通过继承实现

public class ArrayList {
    private Object[] elementData;
    
    public Object get(int i) {}
    
    public void add(Object o) {}
}

这样的设计会引发几个问题

  1. 添加值时候没有类型检查,可以添加任意类型
  2. 当获取值时候必须进行强制类型转换
  3. 代码阅读时候不清楚具体类型,可读性比较差

使用范型可以在不确定具体的数据类型时,以类型变量来代替具体的数据类型。这样一方面可以提高代码的复用性和可读性,另一方面也可以减少代码中的类型转换,提高程序的运行效率

List<String> list = new ArrayList<>();
list.add("Java");
// list.add(123); // 编译错误
String s = list.get(0); // 无需类型转换

定义范型类

把 <T>放在 class 名称后面可以定义一个范型类

public class Animal<T> {
    private T animal;

    public T getAnimal() {
        return this.animal;
    }

    public void SetAnimal (T t) {
        this.animal = animal;
    }
}

通常变量 E 表示集合的元素类型,K 和 V 分别表示关键字与值的类型。T(U、S 等)表示任意类型

范型方法

在普通类中把 <T>放在方法返回值之前,可以声明范型方法

class Test {
    public static <T> T getMiddle(T... t) {
    	return t[t.length / 2];
    }
}

在大多数情况下,实际调用范型方法时可以省略类型参数,编译器有足够的信息能够推断出所调用的方法

int avgNumber = Test.getMiddle(2, 5, 9, 7, 3);

类型限定

很多时候范型类或方法并不能支持所有的类型,需要对类型进行限定

class ArrayAlg {
    public static <T> T min(T[] a) {
        if (a == null || a.length == 0) {
            return null;
        }

        T smallest = a[0];

        for (int i = 1; i < a.length; i++) {
            if (smallest.compareTo(a[i]) > 0) {
                smallest = a[i];
            }
        }
        return smallest;
    }
}

目前代码有一个缺陷,实例化的类型 T 不一定有 compareTo 方法,因此需要把 T 限定为实现了 Comparable 接口的类

class ArrayAlg {
    public static <T extends Comparable<T>> T min(T[] a) {
        if (a == null || a.length == 0) {
            return null;
        }

        T smallest = a[0];

        for (int i = 1; i < a.length; i++) {
            if (smallest.compareTo(a[i]) > 0) {
                smallest = a[i];
            }
        }
        return smallest;
    }
}

<T extends BoundingType> 表示 T 应该绑定类型的子类型,类或接口都使用 extends 关键字

类型擦除

在 JVM 中没有范型对象,所有对象都是普通类。因此范型设计的程序在编译时类型信息被擦除,转而使用原始类型来代替范型类型,比如 String 类型数据被转换为 Object 类型,使用了上述类型限定的会被转换为 extends 的类型

public class MyList<T> {
    private List<T> list;

    public void add(T t) {
        list.add(t);
    }

    public T get(int index) {
        return list.get(index);
    }
}

在编译时,Java 编译器会将范型类型 T 转换为 Object 类型,并在代码中插入必要的类型转换

public class MyList {
    private List list;

    public void add(Object t) {
        list.add(t);
    }

    public Object get(int index) {
        return list.get(index);
    }
}

可以看到范型类型 T 被转换为了 Object 类型,并且对应的方法的参数和返回值类型也被转换为了 Object。类型擦除确保了泛型代码的向后兼容性,但也带来了一些限制:

无法使用泛型类型参数创建实例

public class GenericClass<T> {
  // 无法创建 T 类型的数组
  // T[] array = new T[10]; // 编译错误
}

无法进行 instanceof 检查

public void checkInstance(List<String> list) {
  // if (list instanceof List<String>) {} // 编译错误
  if (list instanceof List) {
      // 合法
  }
}

无法创建泛型类型数组

public class GenericArrayExample<T> {
  // 私有构造
  private T[] array;

  public GenericArrayExample(Class<T> clazz, int size) {
      // array = new T[size]; // 编译错误
      array = (T[]) java.lang.reflect.Array.newInstance(clazz, size);
  }
}

范型只能使用对象类型

基本数据类型没有属性和方法,而范型是在编译期间进行类型检查的,需要通过对象的属性和方法来确定类型,因此必须使用对象类型

此外,基本类型也不能作为泛型类型参数,因为泛型类型参数必须是对象类型或者是接口类型。如果需要使用基本数据类型,可以使用对应的包装类作为对象类型进行包装,例如使用 Integer 来代替 int

在 Oragle Java Documentation Why use Generics 中可以了解更多关于范型的知识

泛型与继承

泛型与继承的结合使用需要注意类型参数的兼容性,Java 的泛型是不变的,这意味着 List<Cat> 不是 List<Animal> 的子类型,即使 Cat 是 Animal 的子类

List<Cat> catList = new ArrayList<>();
List<Animal> animalList = catList; // 编译错误

通过使用通配符,可以在一定程度上实现泛型的灵活性:

  • 上界通配符允许读取数据:
public void processAnimals(List<? extends Animal> animals) {
    for (Animal animal : animals) {
        animal.eat();
    }
}
  • 下界通配符允许写入数据:
public void addAnimal(List<? super Dog> animals) {
    animals.add(new Dog());
}