泛型设计的意义
泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用。举个简单的例子,在没有泛型特性之前( JDK 5 以前),ArrayList 只维护一个 Object 引用的数组。
万物皆是 Object,所以啥都可以往里面放。但是这样会有两个问题:
获取值时必须进行强制类型转换:
ArrayList list = new ArrayList(); list.add("xw"); String str = (String) list.get(0);
编译器没有错误类型检查,可以向数组列表中添加任何类的对象。
list.add("xw"); list.add(20); list.add(new Date());
添加对象的时候编译和运行都不会出错。然而在其他地方,如果将 get 的结果强制类型 转换为 String 类型,就会产生一个异常:ClassCastException
。
那么有了泛型之后,可以使用类型变量来明确指明元素的类型,完美的解决了上述问题。出现编译错误比类在运行时出现类的强制类型转换异常要好得多。
ArrayList<String> list = new ArrayList<String>(); // 从 JDK7 开始,构造函数中可以省略泛型类型,可以从变量的类型推断得出 ArrayList<String> list = new ArrayList<>();
还有另外一种情况,如果同一个泛型类的关于不同实例的元素直接存在关系呢?还是以 ArrayList 为例,如果对于存在的 Parent 类,和 Child 类,我们应该保证可以向父类的泛型类中添加子类的元素,但是不允许向子类的泛型类添加父类的元素,如下图所示:
可以很明显的看到,编译器发生了错误,究其根本,这是 Java 中引入了通配符类型。稍后我们会详细学习。
泛型类
泛型类就是具有一个或多个类型变量的类。对于这个类来说,我们只关注泛型,而不需要关注存储细节。
类型变量,用尖括号< >
括起来,并放在类名的后面。泛型类可以有多个类型变量。类型变量可以用来指定方法的返回类型以及域和局部变量的类型。
public class MyClass<T> { private T a; private T b; public MyClass(T a, T b) { this.a = a; this.b = b; } }
按照习惯和规范,类型变量使用大写形式, 且比较短,相当于实际类型的一个占位。通常使用 E 来表示集合的元素类型,K 和 V 表示键值对的元素类型,T用来表示任意类型,当然使用 A、B、C 这些也都可以。
实例化的时候可以用具体的类型替换掉类型变量,如下:
MyClass<Integer> myClass = new MyClass<>(1, 2);
泛型方法
泛型方法还可以定义在普通类中,同样使用类型变量的方式,用尖括号< >
扩起来。不过注意类型变量的位置放在方法返回值的前面。
public static <A> A getMid(A ...values) { return values[values.length / 2]; }
调用的时候可以显示的声明出类型变量。也可以省略书写,由编译器推断。
String midStr = MyClass.<String>getMid("1", "2", "3"); Integer midStr = MyClass.getMid(1, 2, 3);
类型变量的限定类型
通过对类型变量设置限定来加以约束,方便内部逻辑的编写。
// T 需要同时 实现/继承 BoundingType1 和 BoundingType2 < T extends BoundingType1 & BoundingType2〉
表示 T 应该是限定类型的子类型。T 和绑定类型可以是类,也可以是接口。限定类型用&
分隔,逗号用来分隔类型变量。
<T extends Class1, U extends Class2 & Class3>
泛型代码与虚拟机
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型变量都用它们的限定类型替换
- 桥方法被合成来保持多态
- 为保持类型安全性,必要时插人强制类型转换
类型擦除
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型变量后的泛型类型名。擦除类型变量,并替换为限定类型,如果有多个限定类型,则使用限定列表的第一个类型变量进行替换,如果无限定的变量则替换成 Object。
比如上面的 MyClass,因为 T 是一个无限定的变量,所以直接用 Object 替换,进行类型擦除后如下所示:
public class MyClass { private Object a; private Object b; public MyClass(Object a, Object b) { this.a = a; this.b = b; } }
在程序中可以包含不同类型的 MyClass,例如, MyClass 或 MyClass。而擦除类
型后就变成原始的 MyClass 类型了。
翻译泛型表达式
当程序调用泛型方法的时候,如果擦除返回类型,编译器插入强制类型转换。当存取一个泛型域时也要插人强制类型转换。类型擦除也会出现在泛型方法中。
若擦除后泛型方法返回 Object 类型,那么编译器会在字节码中自动插入实际类型的强制类型转换。
约束与局限性
不能用基本类型实例化类型变量
运行时类型查询只适用于原始类型
不能创建参数化类型的数组
不能实例化类型变置
不能使用像 new T(…),newT[…] 或 T.class 这样的表达式中的类型变量。
最好的解决办法是让调用者提供一个构造器表达式(可以使用反射)。
不能构造泛型数组
就像不能实例化一个泛型实例一样,也不能实例化数组。不过原因有所不同,毕竟数组会填充 null 值,构造时看上去是安全的。不过,数组本身也有类型,用来监控存储在虚拟机中的数组。这个类型会被擦除。
最好的解决办法是让调用者提供一个数组构造器表达式(可以使用反射)。
泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量。
public class MyClass<T> { // 错误用法 private static T c; // 错误用法 public static void test(T a) { } }
不能抛出或捕获泛型类的实例
泛型类型的继承规则
泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么区别。例如, ArrayList类实现 List接口。
这意味着, 一个 ArrayList 可以被转换为一个 List。但是,如前面所见,一个 ArrayList 不是一个 ArrayList 或 List。
通配符类型
使用通配符类型可以允许类型变量的变化。例如:
MyClass<? extends Person>
表示上面的泛型的类型变量需要是 Person 的子类。
:上界通配符,类型变量需要是 Class1 的子类,不能往里存,能往外取。
:下界通配符,类型变量需要是 Class1 的父类,不能往外取,能往里存。
一种无限的符号,代表任何类型都可以
PECS原则
- 频繁往外读取内容的,适合用上界 Extends。
- 经常往里插入的,适合用下界 Super。
反射和泛型
反射允许你在运行时分析任意的对象。如果对象是泛型类的实例,关于泛型类型变量则得不到太多信息,因为它们会被擦除。
泛型 Class 类
类型变量十分有用 ,这是因为它允许 Class 方法的返回类型更加具有针对性。
newlnstance 方法返回一个实例,这个实例所属的类由默认的构造器获得。它的返回类型目前被声明为 T,其类型与 Class 描述的类相同,这样就免除了类型转换。
如果给定的类型确实是 T 的一个子类型,cast 方法就会返回一个现在声明为类型 T的对象,否则,抛出一个 BadCastException 异常。
如果这个类不是 enum 类或类型 T 的枚举值的数组,getEnumConstants 方法将返回 null。
getConstructor 与 getDeclaredConstructor 方法返回一个 Constructor对象。Constructor 类也已经变成泛型,以便 newlnstance 方法有一个正确的返回类型。
虚拟机中的泛型类型信息
Java 泛型的卓越特性之一是在虚拟机中泛型类型的擦除。令人感到奇怪的是,擦除的类仍然保留一些泛型祖先的微弱记忆。
可以使用反射 API来确定:
- 这个泛型方法有一个叫做 T 的类型变量。
- 这个类型变量有一个子类型限定,其自身又是一个泛型类型
- 这个限定类型有一个通配符参数
- 这个通配符参数有一个超类型限定
- 这个泛型方法有一个泛型数组参数
为了表达泛型类型声明,使用 java.lang.reflect 包中提供的接口 Type。这个接口包含下列子类型:
- Class类,描述具体类型
- TypeVariable 接口,描述类型变量(如 T extends Comparable )
- WildcardType 接口,描述通配符(如?super T )
- ParameterizedType 接口,描述泛型类或接口类型(如 Comparable )
- GenericArrayType 接口,描述泛型数组(如 T[ ] )
笔记大部分摘录自《Java核心技术卷I》,含有少数本人修改补充痕迹。