第9章 泛型
9.1 泛型入门
9.1.1 编译时不检查类型的异常
List list = new ArrayList();
list.add("abc");
list.add(3);
for(int i = 0; i < list.size(); i++) {
String str = (String) list.get(i); //(1)
}
容器不加泛型,从容器中取元素的时候并进行类型转换时,(1)可能报类型强制转换异常ClassCastException。
9.1.2 手动实现编译时检查类型
创建一个List类包裹ArrayList添加add方法,在方法形参添加可以添加的类型。弊端是要手动创建多个List子类。
9.1.3 使用泛型
List<String> list = new ArrayList<>();
list.add("abc");
list.add(3); //(1)
尖括号中间类型标明该list只能装指定类型为String,装其他类型将报错,(1)编译器将报错。
9.1.4 Java7泛型的“菱形”语法
List<String> list = new ArrayList<String>(); //(1)
List<String> list = new ArrayList<>(); //(2)
(1)为Java7之前的写法,(2)为Java7之后的写法。9.2 深入泛型
9.2.1 定义泛型接口、类
public interface List<E> {
void add(E e);
...
}
public class Apple<T> {
private T info;
public Apple(T info) {
this.info = info;
}
public void setInfo(T info) {
this.info = info;
}
public T getInfo() {
return this.info;
}
public static void main(String[] args) {
Apple<String> apple = new Apple<>("abc");
}
}
如上例子分别声明带泛型的接口和类,使用时将泛型T改为实际类型即可。
9.2.2 从泛型类派生子类
从泛型声明的类或接口派生子类或子接口时,要指明父类泛型的具体类型而不能使用泛型,如下:
public class A extends Apple<T> {
}
编译器将报错。定义类、接口、方法时可以声明类型形参,使用类、接口、方法时应为类型形参传入实际的类型(类或接口可以不传入实际类型的参数)。
从Apple类派生子类,则改为如下代码:
//使用Apple类时为T形参传入String类型
public class A extends Apple<String> {
}
调用方法时必须为所有的数据形参传入参数值,与调用方法不同的是,使用类、接口时可以不为类型形参传入实际的类型参数,即下面代码也是正确的。
public class A extends Apple
从Apple派生子类,重写父类方法时,要注意泛型的实际类型,方法的返回类型和形参要和实际类型一致。
9.2.3 并不存在泛型类
相同泛型类的对象都由同一个类产生。
静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参,因为类加载初始化静态的代码时要确定实际的类型。
instanceof 运算符后面不能使用泛型类,因为泛型为编译期检查,编译后泛型将擦除,所以runtime期间不能保证如下cs泛型为String,因此禁止使用如下错误用法:
Collection cs = nwe ArrayList<String>();
if(cs instanceof List<String>) {...} // 错误
if(cs instanceof List) {...} // 可以使用
9.3 类型通配符
public void test(List c) {
...
}
使用List接口时没有传入实际类型参数,将引起泛型警告。
List<String>对象不能当做List<Object>对象使用,即List<String>类不是List<Object>类的子类。
如果Foo是Bar的一个子类型(子类或者子接口),那么Foo[]是Bar[]的子类型,而G是具有泛型声明的类或接口,G<Foo>并不是G<Bar>的子类型。
Java泛型设计原则:只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。
9.3.1 使用类型通配符
类型通配符为?,元素类型可以匹配任何类型,如下:
public void test(List<?> c) {
...
}
c中元素永远为Object类型,因c是一个List,List运行时只存Object,泛型只是在编译期进行代码提示作用,实际在运行时存储类型为Object。
null为所有引用类型的实例。
9.3.2 设定类型通配符的上限
Java提供了被限制的泛型通配符:
public void drawAll(List<?> shapes) {
for(Object obj : shapes) {
//强制类型转换
Shape s = (Shape) obj;
s.draw();
...
}
}
//标明通配符?表示List的泛型为Shape的所有子类型(包括本身),而不能是其他类型
public void drawAll(List<? extends Shape> shapes) {
//使用类型通配符上限,至少可以知道类型肯定为Shape,不需强转
for(Shape s : shapes) {
s.draw();
...
}
shapes.add(0, new Rectangle()); // (1)代码报错
}
(1)代码将报错,因虽然通配符的上限为Shape但?表示的实际类型并不知道,所以不能将对象放入其中。
9.4 泛型方法
9.4.1 定义泛型方法
static void fromArrayToCollection(Object[] a, Collection<Object> c) {
for(Object obj:a) {
c.add(obj);
}
}
以上方法的局限性为:只能将Object数组的元素复制到Object的Collection集合中,不能使用Collection<String>。使用通配符Collection<?>也不可行,我们不能把对象放进未知类型的集合中(集合中元素类型未知)。
为了解决这个问题,引入泛型方法,语法格式如下:
修饰符 <T, S> 返回类型 方法名(形参列表) {
//方法体...
}
//如:
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for(T o : a) {
c.add(o);
}
}
public static void main(String[] args) {
Integer[] ia = new Integer[100];
Collection<Number> cn = new ArrayList<>();
fromArrayToCollection(ia, cn);
}
泛型方法调用时,编译器根据实参推断类型形参的值,它通常推断出最直接的类型参数。Integer[] ia 使得T[] a知道T的类型为Integer。9.4.2 泛型方法和类型通配符的区别
大多数时候都可以用泛型方法来替换通配符
使用通配符情况:用来支持灵活的子类化
泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。
形参a的类型或返回值的类型依赖于另一个形参b的类型,则b的类型声明不应该使用通配符,因为使用通配符表示类型b不确定,那么a的类型也不能确定,这时候要考虑使用泛型方法。
类型不被依赖时,使用通配符。
9.4.3 Java7的“菱形”语法与泛型构造器
构造器可带泛型
class Foo<E> {
public <T> Foo(T t) {
...
}
public static void main(String[] args) {
new Foo("abc");
new Foo(3);
new <String> Foo("abc");
new <String> Foo(3.2); // (1) 报错
Foo<String> foo = new Foo<>(3); // 菱形语法标明E的类型,“abc”标明T的类型
Foo<String> foo = new <Integer> Foo<String>(3); // 指定泛型构造器中T的参数为Integer,则不能使用菱形语法
Foo<String> foo = new <Integer> Foo<>(3); // (2)报错,此处不能使用菱形语法
}
}
(1)处泛型构造器中T的参数为String类型,而实际传入的是Double类型
(2)显式指定泛型构造器中的泛型类型为Integer则不能使用菱形语法省略构造器后面<>中类的E的类型。
9.4.4 设定通配符下限
通配符的下限语法:<? super T>,只关心子类的具体类型而不关心父类的具体类型时,使用通配符的下限。
9.4.5 泛型方法与重载
允许根据方法参数泛型不同进行方法重载,但是调用时,如果编译器分不清该调用哪个方法则编译报错,(3)报错。
public class MyUtils {
// (1)
public static <T> void copy(Collection<T> dest, Collection<? extends T> src) {...}
// (2)
public static <T> T copy(Collection<? super T> dest, Collection<T> src) {...}
public static void main(String[] args) {
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
copy(ln, li); // (3) 编译报错
}
}
9.5 擦除和转换
如果不指定泛型的实际类型参数,则该类型参数被称作raw type(原始类型),默认是声明该参数时指定的第一个上限类型。
List<String>变量赋值给List时,对List集合中元素的类型检查变成了类型变量的上限即Object。
9.5 泛型与数组
Java 5的泛型设计原则为:代码在编译时期没有提出“[unchecked]未经检查的转换”警告,则程序在运行时不会引发ClassCastException。换句话说,在使用泛型后,不能在没有出现“[unchecked]”警告时报ClassCastException。
数组元素类型不能包含类型变量或类型形参,如:数组元素为List,但不能指定List中元素的类型。
只能声明ArrayList<String>[]形式的数组,不能定义ArrayList<String>[10]这样的数组对象。
可以使用类型通配符?来创建数组对象,如new ArrayList<?>[10],使用通配符后,在进行转换时需要手动添加instanceof类型检查避免ClassCastException,因为此时可以在不出现“[unchecked]”警告时报ClassCastException。