一、概述
范型是对类的成员变量的类型进行泛化,使用一个或多个占位符“类型”对代码的类型进行指定,并在调用时对这些代码进行约定,提高安全性。在实例化的对象时,对对象的属性进行规范化,对其指定类型。范型是属于对象的。约定的类型只能是引用类型。若在实例化对象时没有指定类型,则为默认的Object类型。由于范型在使用时能够像传参一样将数据类型作为“参数”传递给对象,故范型又可称为“数据类型的参数化”。
范型可以定义在类、接口和方法中,用于指定成员变量、返回值和参数的类型。
class<T> A {} // 类
A<T> a = new A<T>(); // 创建A的对象,这里T是具体的类型
A<T> a = new A<>(); // 简便的方法
interface<T> B {} // 接口
<T> T test(T a) {} // 方法
当类型被指定后,两个被当成不同的类型,不具有继承关系,故也没有多态的性质。
ArrayList<Object> list = new ArrayList<String>; // 报错,左右不是同一个类型,没有继承关系
范型可以被嵌套:
Map<Integer,String> map = new HashMap<>();
Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
二、通配符
若一个方法中的一个参数的类型拥有范型,则该方法不能被重载。若要对该方法的范型参数指定不同的类型,可以使用通配符“?”。使用了通配符的对象,不能修改,可以遍历。这里的通配符表示任意的引用类型。
void test(ArrayList<?> list) {
list.add(1); // 报错
list.add(null); // 不报错,null是所有对象共有的值
list.add(new Object()); // 报错,list的范型不一定是Object类型
for(Object obj : list) {
System.out.println(obj); // 不报错,可以遍历
}
}
三、上限和下限
如果不想让通配符表示任意类型,而是具有一个上限和下限,即一个边界,则可以使用extends和super关键字界定其边界。可以声明具有范型上限的类、接口、方法和对象;只有对象才能具有范型下限。
Collection<? extends Number> listHasCeiling; // 指定的类型只能是Number及其子类
listHasCeiling = new ArrayList<Integer>();
listHasCeiling = new ArrayList<Double>(); // 可以指向范围内任意明确类型的对象,右边的对象可以修改,左边的引用不可添加,但可以删除和遍历
Collection<? super Number> listHasBottom = new ArrayList<Object>(); // 指定的类型只能是Number及其父类
listHasBottom.add(1); // ok,在这里1是Number类型的引用指向Integer对象
listHasBottom.add(1.5); // ok
listHasBottom.remove(0); // ok,具有下限的对象可以添加、删除和遍历
四、范型擦除
范型在编译后,当类、接口、方法或对象被使用时,范型会被擦除,占位符会变为Object类型。故具有不同范型的相同引用类型参数的方法不可重载:
String Test(ArrayList<String> obj) {}
String Test(ArrayList<Integer> obj) {}
// 范型擦除后
String Test(ArrayList obj) {}
String Test(ArrayList obj) {} // 一模一样,故不可重载
// 替代方式
String Test(ArrayList<?> obj) {}
因此,Java的泛型只存在于编译时期,泛型使编译器可以在编译期间对类型进行检查以提高类型安全,减少运行时由于对象类型不匹配引发的异常。