范型程序设计

学习目标

  • 创建泛型类、接口
  • 使用泛型类、接口
  • 泛型的优点
  • 创建泛型方法、受限泛型类型
  • 使用原类型
  • 泛型消除

什么是泛型

  • 字面意思是:编写的代码适用于广泛的类型
  • 泛型的核心概念是参数化类型
  • 所谓的参数化类型是指编写代码时,它所适用的类型并不立即指明,而是使用参数符号来代替,具体的适用类型延迟到用户使用时才指定。

为什么要用泛型

  • 能够在编译时而不是运行时检查出错误
  • 保证了程序的类型安全并消除了一些繁琐的类型转换


Generic Instantiation

上图左边Runtime error,右边Compile error

在java泛型之前,一般的程序都是多态与继承来提高代码的灵活性和重用性。最常见的用继承来实现泛型的就是List容器。对于List来说,它存放的都是Object类型,由于java中除了基本类型外的所有类都继承自Object,因此,可以添加任何类型到List中。

问题1:当我们从List中取出元素时,必须显示的将其转型成我们需要的类型。
问题2:当试图在只允许存放Integer的List中添加字符串类型时,编译器并不会报错。
针对以上可能出现的情况,泛型机制很好的解决了这些问题。

泛型类

泛型类是指该类使用的参数类型作用于整个类,即在类的内部任何地方(不包括静态代码区域)都可把参数类型当做一个真实类型来使用,比如用它做为返回值、用它定义变量等等。

泛型类的定义

定义泛型类的定义很简单,只需在定义类的时候,在类名后加入这样一句代码即可,其中T是一个参数,是可变的。

1
2
3
4
5
6
7
8
9
public class Person<T> {
private T t;
public Person(T t){
this.t = t;
}
public String toString(){
return "参数的类型是:" + t.getClass().getCanonicalName();
}
}

一个类指明多个类型参数:

1
2
3
4
5
6
7
8
9
10
11
12
public 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
12
public 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
2
3
4
5
6
public class GenericTest<T> {
public static void main(String[] args){
Person<Integer> p = new Person<Integer>(5);
System.out.println(p.toString());
}
}

泛型接口

  • 泛型接口的制定方式和泛型类相似
  • 如果一个类实现了泛型接口,那么该类也必须泛型,至少接受传递给接口的类型参数
  • 如果一个类实现泛型接口的特定类型,那么实现接口的类则不必泛型,如:class MyClass implements Containment {//OK
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
37
38
public interface Factory<T> {
public T create();
}

public class Car {
}
public class Computer {
}

public class CarFacotry implements Factory<Car> {
@Override
public Car create() {
System.out.println("装载发动机!");
System.out.println("装载座椅!");
System.out.println("装载轮子!");
return new Car();
}
}
public class ComputerFactory implements Factory<Computer> {
@Override
public Computer create() {
System.out.println("装载主板!");
System.out.println("装载CPU!");
System.out.println("装载内存");
return new Computer();
}
}

public class GenericTest {
public static void main(String[] args) throws Exception{
Factory<Car> carFactory = new CarFacotry();
Factory<Computer> computerFactory = new ComputerFactory();
System.out.println("======开始生产车子!=======");
carFactory.create();
System.out.println("=====开始生产电脑!========");
computerFactory.create();
}
}

泛型方法

1
2
3
4
5
public static <E> void print(E[] list) {
for (int i = 0; i < list.length; i++)
System.out.print(list[i] + " ");
System.out.println();
}
1
2
3
4
5
public static void print(Object[] list) {
for (int i = 0; i < list.length; i++)
System.out.print(list[i] + " ");
System.out.println();
}

泛型边界

  • 泛型边界是指为泛型参数指定范围
  • Java泛型系统允许使用extends和super关键字设置边界。
  • extends设定上行边界,即指明参数类型的顶层类,限定实例化泛型类时传入的具体类型,只能是继承自顶层类的。 super设置下行边界,即指定参数类型的底层类,限定传入的参数类型只能是设定类的父类。
1
2
3
4
5
6
7
8
9
public static void main(String[] args ) {
Rectangle rectangle = new Rectangle(2, 2);
Circle9 circle = new Circle9(2);
System.out.println("Same area? " + equalArea(rectangle, circle));
}

public static <E extends GeometricObject> boolean equalArea(E object1, E object2) {
return object1.getArea() == object2.getArea();
}

泛型边界在需要确保一种类型参数与另一种兼容时特别有用:

1
2
3
4
5
6
7
8
9
10
class 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
37
public 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
2
3
4
5
// raw type
ArrayList list = new ArrayList();

This is roughly equivalent to
ArrayList<Object> list = new ArrayList<Object>();

原类型是不安全的

1
2
3
4
5
6
7
8
9
10
// Max.java: Find a maximum object
public class Max {
/** Return the maximum between two objects */
public static Comparable max(Comparable o1, Comparable o2) {
if (o1.compareTo(o2) > 0)
return o1;
else
return o2;
}
}

Max.max("Welcome", 23);时,上面代码Runtime Error

泛型擦除

  • 泛型擦除是指泛型代码在编译后,都会被擦除成原类型
  • Zoo和Zoo,实质上在运行时是同一种类型,即原类型Zoo。
  • 运行时,java并不存在类型参数这一概念,因此你将无法获取任何相关的参数类型信息。

编译器在先对(a)中泛型的类型进行安全验证,然后再把代码翻译成原类型代码(b),用在运行时

1
2
GenericStack<String> stack1 = new GenericStack<String>();
GenericStack<Integer> stack2 = new GenericStack<Integer>();

尽管 GenericStack 和 GenericStack 是两个类型, 在JVM中只装载了GenericStack 一个类

为何要擦除

  • 擦除并不是一种语言特性,而是java泛型实现的一种折中办法。因为泛型在jdk5之后才是java的组成部分,因此这种折中是必须的。擦除的核心动机是使得泛化的代码可以使用非泛化的类库,反之依然,这称之为”迁移性兼容”。
  • 擦除的实质,将原有的类型参数替换成即非泛化的上界。

例如:

泛型类:

1
2
3
4
5
6
7
8
9
public 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
12
public 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
9
public 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
9
public class Zoo {
public Zoo(Animal t) {
this.t = t;
}
public Animal pop() {
return t;
}
private Animal t;
}

泛型限制

类型参数不能实例化

1
2
3
4
5
6
7
8
// Can't create an instance of T. 
class Gen<T> {
T ob;

Gen() {
ob = new T(); // Illegal!!!
}
}

因为运行时参数类型信息被擦除,因此无法确定类型参数T所代表的具体类型拥有无参的构造函数,甚至T所代表的具体类型可能不能进行实例化,如抽象类。

instanceof判断类型

1
2
Zoo<Bird> birdZoo = new Zoo<Bird>();
if(birdZoo instanceof 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
2
Zoo<Fish> birdZoo = new Zoo<Fish>();
if(birdZoo instanceof Zoo<Bird>) {…}

并不意味这instanceof不能与泛型同存,下面就是个例外:

1
2
Zoo<Bird> birdZoo = new Zoo<Bird>();
if(birdZoo instanceof Zoo<?>) {System.out.println("success");}

这个例外就是通配符,instanceof判断中允许使用参数类型为通配符的泛型,因为使用通配符类型并不存在以上争议。

抛出或捕获参数类型信息

泛型类对象是不能被抛出或捕获的,因为泛型类是不能继承或实现Throwable接口及其子类的。

1
2
public class GenericException<T> extends Exception {//无法编译
}

类型参数也不能使用在Catch捕获的对象中

1
2
3
4
5
6
7
8
9
public class GenericException<T> {
public void excetionTest(){
try{

}catch(T t){//提示错误

}
}
}

JVM却允许我们在异常的处理中使用类型参数

1
2
3
4
5
6
7
8
9
public class GenericException<T> {
public void excetionTest(){
try{

}catch(Exception e){
T t;
}
}
}

Java中不能声明泛型类数组
List<Integer>[] list = new ArrayList<String>[2];//编译错误
因为擦除后List[]将会被擦除成List[],如果允许声明泛型数组的话,我们就可以这样做:

Object[] objects = list;
Objects[0] = new ArrayList<String>();

这样做显然没有问题,编译时将无法检测处错误,但运行时仍然会导致错误

当定义一个泛型类时,泛型类型参数将不允许使用在静态代码中

1
2
3
4
5
6
public class Generic<T> {
private static T t;//编译错误
public static T get(){
return null;
}
}

然而并不是说不能使用类型参数编写静态代码,当类型编写静态的泛型方法时,java是允许我们使用类型参数的

1
2
3
4
5
public class Generic{
public static <T> T get(Class<T> clazz) throws Exception{
return clazz.newInstance();
}
}

边界处不能使用基本类型,而且基本类型不能用于指明类型参数

1
2
3
4
5
public class Generic<T extends int> {
public void get() {
List<int> list = new ArrayList<int>();//编译错误
}
}

这是因为擦除时将使用边界替换参数类型,而边界必须继承自Object。在java中基本类型并不继承Object

热评文章