一、概述

范型是对类的成员变量的类型进行泛化,使用一个或多个占位符“类型”对代码的类型进行指定,并在调用时对这些代码进行约定,提高安全性。在实例化的对象时,对对象的属性进行规范化,对其指定类型。范型是属于对象的。约定的类型只能是引用类型。若在实例化对象时没有指定类型,则为默认的Object类型。由于范型在使用时能够像传参一样将数据类型作为“参数”传递给对象,故范型又可称为“数据类型的参数化”。
范型可以定义在类、接口和方法中,用于指定成员变量、返回值和参数的类型。

  1. class<T> A {} // 类
  2. A<T> a = new A<T>(); // 创建A的对象,这里T是具体的类型
  3. A<T> a = new A<>(); // 简便的方法
  4. interface<T> B {} // 接口
  5. <T> T test(T a) {} // 方法

当类型被指定后,两个被当成不同的类型,不具有继承关系,故也没有多态的性质。

  1. ArrayList<Object> list = new ArrayList<String>; // 报错,左右不是同一个类型,没有继承关系

范型可以被嵌套:

  1. Map<Integer,String> map = new HashMap<>();
  2. Set<Map.Entry<Integer, String>> entrySet = map.entrySet();

二、通配符

若一个方法中的一个参数的类型拥有范型,则该方法不能被重载。若要对该方法的范型参数指定不同的类型,可以使用通配符“?”。使用了通配符的对象,不能修改,可以遍历。这里的通配符表示任意的引用类型。

  1. void test(ArrayList<?> list) {
  2. list.add(1); // 报错
  3. list.add(null); // 不报错,null是所有对象共有的值
  4. list.add(new Object()); // 报错,list的范型不一定是Object类型
  5. for(Object obj : list) {
  6. System.out.println(obj); // 不报错,可以遍历
  7. }
  8. }

三、上限和下限

如果不想让通配符表示任意类型,而是具有一个上限和下限,即一个边界,则可以使用extends和super关键字界定其边界。可以声明具有范型上限的类、接口、方法和对象;只有对象才能具有范型下限。

  1. Collection<? extends Number> listHasCeiling; // 指定的类型只能是Number及其子类
  2. listHasCeiling = new ArrayList<Integer>();
  3. listHasCeiling = new ArrayList<Double>(); // 可以指向范围内任意明确类型的对象,右边的对象可以修改,左边的引用不可添加,但可以删除和遍历
  4. Collection<? super Number> listHasBottom = new ArrayList<Object>(); // 指定的类型只能是Number及其父类
  5. listHasBottom.add(1); // ok,在这里1是Number类型的引用指向Integer对象
  6. listHasBottom.add(1.5); // ok
  7. listHasBottom.remove(0); // ok,具有下限的对象可以添加、删除和遍历

四、范型擦除

范型在编译后,当类、接口、方法或对象被使用时,范型会被擦除,占位符会变为Object类型。故具有不同范型的相同引用类型参数的方法不可重载:

  1. String Test(ArrayList<String> obj) {}
  2. String Test(ArrayList<Integer> obj) {}
  3. // 范型擦除后
  4. String Test(ArrayList obj) {}
  5. String Test(ArrayList obj) {} // 一模一样,故不可重载
  6. // 替代方式
  7. String Test(ArrayList<?> obj) {}

因此,Java的泛型只存在于编译时期,泛型使编译器可以在编译期间对类型进行检查以提高类型安全,减少运行时由于对象类型不匹配引发的异常。