学习目标
- 创建泛型类、接口
- 使用泛型类、接口
- 泛型的优点
- 创建泛型方法、受限泛型类型
- 使用原类型
- 泛型消除
什么是泛型
- 字面意思是:编写的代码适用于广泛的类型
- 泛型的核心概念是参数化类型
- 所谓的参数化类型是指编写代码时,它所适用的类型并不立即指明,而是使用参数符号来代替,具体的适用类型延迟到用户使用时才指定。
为什么要用泛型
- 能够在编译时而不是运行时检查出错误
- 保证了程序的类型安全并消除了一些繁琐的类型转换
Generic Instantiation
上图左边Runtime error,右边Compile error
在java泛型之前,一般的程序都是多态与继承来提高代码的灵活性和重用性。最常见的用继承来实现泛型的就是List容器。对于List来说,它存放的都是Object类型,由于java中除了基本类型外的所有类都继承自Object,因此,可以添加任何类型到List中。
问题1:当我们从List中取出元素时,必须显示的将其转型成我们需要的类型。
问题2:当试图在只允许存放Integer的List中添加字符串类型时,编译器并不会报错。
针对以上可能出现的情况,泛型机制很好的解决了这些问题。
泛型类
泛型类是指该类使用的参数类型作用于整个类,即在类的内部任何地方(不包括静态代码区域)都可把参数类型当做一个真实类型来使用,比如用它做为返回值、用它定义变量等等。
泛型类的定义
定义泛型类的定义很简单,只需在定义类的时候,在类名后加入
这样一句代码即可,其中T是一个参数,是可变的。
1 | public class Person<T> { |
一个类指明多个类型参数:1
2
3
4
5
6
7
8
9
10
11
12public class Teacher<V,S> extends Person {
protected V v;
private S s;
public Teacher(Object t) {
super(t);
}
public void set(V v, S s){
this.v = v;
this.s = s;
}
}
子类使用父类的类型参数:1
2
3
4
5
6
7
8
9
10
11
12public class Teacher<T,S> extends Person<T> {
protected T t;
private S s;
public Teacher(T t) {
super(t);
}
public void set(T t, S s){
this.t = t;
this.s = s;
}
}
泛型类的使用
泛型类的使用方法也很简单,只需在构造的时候指明参数类型即可
1 | public class GenericTest<T> { |
泛型接口
- 泛型接口的制定方式和泛型类相似
- 如果一个类实现了泛型接口,那么该类也必须泛型,至少接受传递给接口的类型参数
- 如果一个类实现泛型接口的特定类型,那么实现接口的类则不必泛型,如:class MyClass implements Containment
{//OK
1 | public interface Factory<T> { |
泛型方法
1 | public static <E> void print(E[] list) { |
1 | public static void print(Object[] list) { |
泛型边界
- 泛型边界是指为泛型参数指定范围
- Java泛型系统允许使用extends和super关键字设置边界。
- extends设定上行边界,即指明参数类型的顶层类,限定实例化泛型类时传入的具体类型,只能是继承自顶层类的。 super设置下行边界,即指定参数类型的底层类,限定传入的参数类型只能是设定类的父类。
1 | public static void main(String[] args ) { |
泛型边界在需要确保一种类型参数与另一种兼容时特别有用:1
2
3
4
5
6
7
8
9
10class Pair<T, V extends T> {
T first;
V second;
Pair(T a, V b) {
first = a;
second = b;
}
// …
}
通配符
- 有时泛型实例的作用域无法指明具体的参数类型。
- 通配符类型,表示任何类型,通配符类型的符号是“?”,因此通配符类型可应用与所有继承自Object的类上。
- 要为通配符建立一个上行边界:
<? extends superclass>
superclass 用作上行边界的类名
- 还可以指定通配符的下行边界:
<? super subclass>
只有subclass或其超类接受实参
例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37public class Animal {
}
public class Bird extends Animal {
}
public class Fish extends Animal {
}
public class Zoo<T> {
private T t;
public Zoo(T t){
this.t = t;
}
public T pop(){
return this.t;
}
}
public class GenericTest {
public static void main(String[] args) throws Exception{
Zoo<? extends Animal> zoo = new Zoo<Bird>(new Bird());
zoo = new Zoo<Fish>(new Fish());
// zoo = new Zoo<Integer>(5); //不合法
}
}
public class GenericTest {
public static void main(String[] args) throws Exception{
Zoo<? super Bird> zoo = new Zoo<Bird>(new Bird());
zoo = new Zoo<Animal>(new Animal());
// zoo = new Zoo<Fish>(new Fish()); //不合法
}
}
原类型和向后兼容
- 提供从旧的非泛型代码的过渡措施。
- 允许不带任何类型参数使用泛型类,创建一个原类型。
- 使用原类型的缺点是丧失了泛型的类型安全性。
1 | // raw type |
原类型是不安全的
1 | // Max.java: Find a maximum object |
当Max.max("Welcome", 23);
时,上面代码Runtime Error。
泛型擦除
- 泛型擦除是指泛型代码在编译后,都会被擦除成原类型
- Zoo
和Zoo ,实质上在运行时是同一种类型,即原类型Zoo。 - 运行时,java并不存在类型参数这一概念,因此你将无法获取任何相关的参数类型信息。
编译器在先对(a)中泛型的类型进行安全验证,然后再把代码翻译成原类型代码(b),用在运行时
1 | GenericStack<String> stack1 = new GenericStack<String>(); |
尽管 GenericStack
为何要擦除
- 擦除并不是一种语言特性,而是java泛型实现的一种折中办法。因为泛型在jdk5之后才是java的组成部分,因此这种折中是必须的。擦除的核心动机是使得泛化的代码可以使用非泛化的类库,反之依然,这称之为”迁移性兼容”。
- 擦除的实质,将原有的类型参数替换成即非泛化的上界。
例如:
泛型类:1
2
3
4
5
6
7
8
9public class Zoo<T> {
private T t;
public Zoo(T t){
this.t = t;
}
public T pop(){
return this.t;
}
}
编译后(擦除后),没有指明上界,因此被擦除成了Object类型1
2
3
4
5
6
7
8
9
10
11
12public class Zoo {
public Zoo(Object t) {
this.t = t;
}
public Object pop() {
return t;
}
private Object t;
}
泛型类:1
2
3
4
5
6
7
8
9public class Zoo<T extends Animal> {
private T t;
public Zoo(T t){
this.t = t;
}
public T pop(){
return this.t;
}
}
编译后(擦除后),对于指明上界的的泛型,类型参数将被擦除其指明的上界。1
2
3
4
5
6
7
8
9public class Zoo {
public Zoo(Animal t) {
this.t = t;
}
public Animal pop() {
return t;
}
private Animal t;
}
泛型限制
类型参数不能实例化
1 | // Can't create an instance of T. |
因为运行时参数类型信息被擦除,因此无法确定类型参数T所代表的具体类型拥有无参的构造函数,甚至T所代表的具体类型可能不能进行实例化,如抽象类。
instanceof判断类型
1 | Zoo<Bird> birdZoo = new Zoo<Bird>(); |
jvm提示Cannot perform instanceof check against parameterized type X,Use instead its raw form Zoo since generic type information will be erased at runtime错误???
由于泛型采用擦除机制,对于一个泛型类来说,即使其参数类型有多种不同,但在运行时它们都共享着一个原生对象
如果允许编译的话,那下面的代码?
1 | Zoo<Fish> birdZoo = new Zoo<Fish>(); |
并不意味这instanceof不能与泛型同存,下面就是个例外:1
2Zoo<Bird> birdZoo = new Zoo<Bird>();
if(birdZoo instanceof Zoo<?>) {System.out.println("success");}
这个例外就是通配符,instanceof判断中允许使用参数类型为通配符的泛型,因为使用通配符类型并不存在以上争议。
抛出或捕获参数类型信息
泛型类对象是不能被抛出或捕获的,因为泛型类是不能继承或实现Throwable接口及其子类的。
1 | public class GenericException<T> extends Exception {//无法编译 |
类型参数也不能使用在Catch捕获的对象中1
2
3
4
5
6
7
8
9public class GenericException<T> {
public void excetionTest(){
try{
}catch(T t){//提示错误
}
}
}
JVM却允许我们在异常的处理中使用类型参数1
2
3
4
5
6
7
8
9public class GenericException<T> {
public void excetionTest(){
try{
}catch(Exception e){
T t;
}
}
}
Java中不能声明泛型类数组List<Integer>[] list = new ArrayList<String>[2];//编译错误
因为擦除后List
Object[] objects = list;
Objects[0] = new ArrayList<String>();
这样做显然没有问题,编译时将无法检测处错误,但运行时仍然会导致错误
当定义一个泛型类时,泛型类型参数将不允许使用在静态代码中1
2
3
4
5
6public class Generic<T> {
private static T t;//编译错误
public static T get(){
return null;
}
}
然而并不是说不能使用类型参数编写静态代码,当类型编写静态的泛型方法时,java是允许我们使用类型参数的1
2
3
4
5public class Generic{
public static <T> T get(Class<T> clazz) throws Exception{
return clazz.newInstance();
}
}
边界处不能使用基本类型,而且基本类型不能用于指明类型参数1
2
3
4
5public class Generic<T extends int> {
public void get() {
List<int> list = new ArrayList<int>();//编译错误
}
}
这是因为擦除时将使用边界替换参数类型,而边界必须继承自Object。在java中基本类型并不继承Object