14.1 泛型与集合
JDK1.5增加了泛型支持。增加泛型后的集合,可以让代码更加简洁,程序更加健壮。Java泛型可以保证,如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。
泛型,即参数化类型,允许程序在创建集合时指定集合元素的类型。Java5改写了集合框架中的全部接口和类,为这些接口和类增加了泛型支持。创建这种特殊集合的方法是:在集合接口或集合类后增加尖括号,尖括号里放一个数据类型,表明这个集合接口或集合类只能保存特定类型的对象。
集合自动记住所有集合元素的数据类型,无须对集合元素进行强制类型转换。
Java7开始,Java允许在构造器后不需要完整的泛型信息,只要给出一对尖括号即可,Java可以推断尖括号里应该是什么泛型信息。此方式称为菱形语法。
14.2 声明泛型
泛型允许在定义类、接口、方法时声明类型形参,这个类型形参的将在声明变量、创建对象、调用方法时动态的指定。类型形参可当成类型使用,几乎所有可使用普通类型的地方都可以使用类型形参。
包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成多个逻辑上的子类,但这种子类在物理上并不存在。
当创建带泛型声明的自定义类时,定义构造器时,构造器不要添加泛型声明。
14.3 泛型类派生子类
使用泛型接口或泛型类派生子类时,泛型接口和泛型父类不能再包含类型形参。要么不为类型形参传入实际的类型参数,此时系统会把类里的T形参当成Object类型处理;要么传入一个类型,则所有的T都被当成传入的类型处理。
不存在泛型类:不管为泛型的类型形参传入什么类型实参,它依然被当成同一个类处理,在内存中只占用同一块内存空间。所以静态方法、静态初始化块和静态变量的声明和初始化中不允许使用类型形参,如static T info;或static T func();都是错误的。
由于系统不生成泛型类,所以instanceof运算符后不能使用泛型类。
14.4 类型通配符
如果B继承了A类,Collection<B>不是Collection<A>类的子类。为了表示各种泛型集合的父类,可以使用类型通配符(?),写作Collection<?>、List<?>、Set<?>。
注意:如果一个方法由一个List<?>参数,则此方法传入任何类型的List都可以,方法里List的参数的类型是未知的,但可以认为是Object类型。因为程序无法确定此List集合中元素的类型,所以不能向其中添加元素。因为add方法默认接受一个泛型类型的参数E,这个类型形参将会在定义List时推断出,但是List的类型没有被指定,而是?,所以add不能准确知道E时何种类型。但是使用get方法可以获取某个值,但它的类型依然未知,可以当做Object使用。
设定类型通配符的上限:当程序中不希望List<?>表示所有的List集合的父类,而是只表示一类泛型List类型的父类时,可以使用菱形通配符上限。如List<? extends Number>表示此List是所有数值型List集合的父类。这个Number被称为类型通配符的上限。
设定类型形参的上限:使用类型形参上限,表示传给该类型形参的实际类型要么是该上限类型,要么是上限类型的子类。类型形参最多只能有一个父类上限,可以有多个接口上限,此时类型形参必须是继承父类并且实现了所有接口的类,如class Wang<T extends Number & Comparable>。与定义类规则相同,所有的接口必须位于父类之后。
设定类型通配符下限:如TreeSet的构造方法TreeSet(Comparator < ? super E> c),设定通配符的上限。假设需要创建一个TreeSet,那么Comparator的泛型类型就可以为String或者Object类型,使用方式更加灵活。一个方法可以通过通配符上限和下限两种方式进行重载,但程序调用时会引发混乱。
14.5 泛型方法
定义接口和类时没有使用类型形参,但定义方法时可以自己定义类型形参。泛型方法,即声明方法时定义了一个或多个类型形参的方法,Java5开始提供。泛型方法签名比普通方法签名多了类型形参声明,尖括号包裹类型形参,多个类型形参以逗号分隔,类型形参声明在方法修饰符和返回值类型之间(修饰符 <T, S> 返回值类型 方法名(形参列表) { 方法体 })。
方法声明中定义的形参只能在该方法里使用,而接口和类中声明的类型形参可以在这个接口或类中使用。与类和接口不同的是,方法中的泛型参数无须显式传入实际类型参数,编译器根据实参的类型可以自动推断出最直接的类型。
14.6 泛型方法和类型通配符的区别
泛型方法作用:泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型以来关系。
类型通配符方法的作用:可以在不同的调用点传入不同的实际类型,用来支持灵活的子类化。
综上所述,如果某个方法中一个形参a的类型或返回值的类型依赖于另一个形参b的类型,则形参b的类型声明不可以使用通配符,因为如果形参b的类型无法确定,程序就无法定义形参a的类型。此时应该使用泛型方法。但是,如果形参a,b都是集合,而又无需修改形参a,则可以用通配符来定义形参a的类型。因为,如果为形参a单独指定一个类型,即使形参a的类型依赖形参b的类型,但因为形参a的类型只出现一次,所以用通配符更简洁。
类型通配符可以在方法签名中定义形参的类型,也可以定义引用变量的类型;泛型方法中的类型形参必须在对应方法中显式声明。
14.7 泛型构造器
在构造器的签名中声明类型形参,即为泛型构造器。在调用构造器创建对象时,可以指定类型形参的实际类型,也可以使用菱形语法让编译器自己推断。
但是,如果程序显示指定了泛型构造器中声明的类型形参的实际类型,则不可以使用菱形语法,即需要为构造器后的菱形括号传入实际的类型。
如new Me<>();或new <A> Me<B>();
14.8 Java8改进的类型推断
包含如下两个方面:
- 可以通过调用方法的上下文来推断类型的目标类型。
- 可以在方法调用链中,将推断得到的类型参数传递到最后一个方法。
14.9 擦除和转换
如果没有为这个泛型指定实际的类型参数,则该类型参数被称作原始类型(raw type),默认是生命该类型参数时指定的第一个上限类型。
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被擦除。当把一个没有类型信息的对象赋值给一个带有类型信息的变量时,编译可以通过,但是提示“未经检查的转换”警告,在强制类型转换时,可能引发异常。
14.10 总结
- 泛型类型不能被实例化,因为创建对象在运行时进行,而类型在编译时就被擦除了,所以编译器不知道该使用何种类型。如下方式均不可以:T t = new T(); T[] array = new T[5];
- 不能创建类型特定的泛型引用变量的数组,如A<String>[] a = new A<String>[10];是不允许的。但是可以声明特定的泛型引用变量的数组变量,如A<String>[] a = new A[10];是可以的,编译时会出现“未经检查的转换”警告,运行时可能出现ClassCastException。可以定义带无上限类型通配符的数组,如A<?> a = new A<?>[10];是可以的,但是需要自己使用instanceof控制强制类型转换,避免程序运行时出现错误。
- 不能将基本类型赋给类型形参作为实际的类型,只能使用引用类型。
- 不能抛出(throw)也不能捕获(catch)泛型类的异常对象,如throw new T(); catch(A<T> e) {};是错误的;也不能让泛型类型继承Throwable(AExcep<T> extends Throwable);不能catch(T e);但可以在方法签名中声明抛出泛型异常(throws T)。
- 静态变量和静态方法不能用类型参数,但可以声明静态泛型方法。