基本概念和原理
为什么使用泛型:
在没有使用泛型之前,一旦把一个对象“丢进”Java集合中,集合就会忘记对象的类型,把所有的对象当成Object类型处理。当程序从集合中取出对象后,就需要进行强制类型转换,这种强制类型转换不仅是代码臃肿,而且容易引起ClassCastException异常。
标题的基本概念:
所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。
使用泛型的好处:
-
更好的安全性
通过使用泛型 ,开发环境和编译器能确保不会用错类型,为程序多设置一道安全防护网。 -
更好的可读性
使用泛型,可以省去繁琐的强制类型转换,再加上明确的类型信息,代码可读性也会更好。
泛型的使用:
public class Test<T> {
T first;
T second;
public Test(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
}
Test是一个泛型类,与普通类的区别:
- 类名后多了一个<T>;
- first和second的类型都是T;
T表示类型参数,泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。
泛型的原理:
Java有Java编译器和Java虚拟机,编译器将Java源代码转换为.class文件,虚拟机加载并运行.class文件。对于泛型类,Java编译器会将泛型代码转换为普通的非泛型代码,就像上面的普通Test类代码及其使用代码一样,将类型参数T擦除,替换Object,插入必要的强制类型转换。Java虚拟机实际执行的时候,它是不知道泛型这回事的,只知道普通的类及代码。
泛型擦除:
Java泛型是通过擦除实现额,类定义中的类型参数如T会被替换为Object,在程序运行过程中,不知道泛型的实际类型参数,比如Test<Integer>,运行中只知道Test,而不知道Integer。
深入泛型
//定义接口时指定了一个泛型形参,该形参名为E
public interface Test<E> {
//在接口方法里,E可作为类型使用
//下面方法可以使用E作为类型参数
void add(E x);
Iterable<E> iterator();
//在接口里,E完全可以作为类型使用
E next();
}
public interface Map<K, V> {
//在接口里K、V完全可以作为类型使用
Set<K> keySet();
V put(K key, V value);
}
解释:
允许在定义接口、类时声明泛型形参,泛型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通方法类型的地方都可以使用这种泛型形参。
注:
当创建泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。 例如,为Test<T>类定义构造器,其构造器名依然是Test,而不是Test<T>;!调用该构造器时却可以使用Test<T>的形式,当然应该为T形参传入实际的类型参数。Java7提供了“菱形”语法,允许省略<>中的类型实参。
从泛型类派生子类:
方法中的形参代表变量、常量、表达式等数据,本文把它们直接称为形参,或者称为数据形参。定义方式时可以声明数据形参,调用方法(使用方法)时必须为这些数据形参传入实际的数据;于此类似的是,定义类、接口、方法时可以使用声明泛型形参,使用类、接口、方法时应该为泛型形参传入实际的类型。
//定义类A继承Apple类,Apple类不能跟泛型形参
public class A extends Apple<T> {} //错误
//使用Apple类时为T形参传入String类型
public class A extends Apple<String> //正确
调用方法时必须为所有的数据形参传入参数值, 与调用方法不同的是,使用类、接口时也可以不为泛型形参传入实际的类型参数,即下面代码也是正确的。
public class A extends Apple //正确
像这种使用Apple类时省略泛型的形式被称为原始类型(raw type)。
如果使用Apple类时没有传入实际的类型(即使用原始类型),Java编译器可能发出警告:使用了未经检查或不安全的操作 - - 就是泛型检查的警告。
并不存在泛型类:
看如下代码:
List<String> list = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
//调用getClass()方法来比较list和list2的类是否相等
System.out.println(list.getClass() == list2.getClass());
运行上面的代码片段,可能有读者认为应该输出false,但实际输出true。因为不管泛型的实际类型参数是什么,它们在运行时总有同样的类(Class)。
不管为泛型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类来处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量(它们都是类相关的)的声明和初始化中不允许使用泛型形参。
public class Test<T> {
//下面代码错误,不能在静态变量声明中使用泛型形参
static T info;
//正确
T age;
public void foo(T msg) { }
//下面代码错误,不能再静态方法声明中使用使用泛型形参
public static void bar(T msg) {}
}
由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。
java.util.Collection<String> cs = new java.util.ArrayList<String>();
//下面代码编译时引起错误:instanceof运算符后不能使用泛型
if(cs instanceof java.util.ArrayList<String>) {}
使用类型通配符:
为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?>(意思是元素类型未知的List)。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。
看如下代码:
public void test(List<?> c) { }
设定类型通配符的上限(协变):
指定通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型或其子类),不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型)。
设定类型通配符的下限(逆变):
除可以指定通配符的上限之外,Java也允许指定通配符的下限,通配符的下限用<? super 类型>的方式类指定,通配符下限的作用与通配符上限的作用恰好相反。
Foo是Bar的子类,当程序需要一个A<? super Foo>变量时,程序可以将A<Bar>、A<Object>赋值给A<? super Foo>类型的变量,这种方式称为逆变。
对于逆变的泛型来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种逆变的泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类),从集合中取元素时只能被当成Object类型处理(编译器无法确定取出的到底是哪个父类的对象)。