9.3 泛型
9.3.1 泛型的定义以及实例化
Java泛型( generic)等同于模板或参数多态,很多语言都支持泛型,如C++、Microsoft的C#,泛型是JDK 1.5引人的影响最大的特性。泛型本质上就是数据类型参数化,允许将任意数据类型指定为一个参数。泛型的目的是通过为类或者方法声明一种通用模式,使类中的某些数据成员或者成员方法的参数、返回值可以取任意类型,从而采用统一的方法或者类即可处理不同的数据类型。泛型把数据类型作为参数传递,就像把数值作为函数的参数传递一样。通俗地说,泛型就如同一种形参,只不过它表示的不是数值,而是某种数据类型。需要注意的是,泛型的数据类型必须是引用类型,如果是基本数据类型,就需要转化为对应的封装类类型。
泛型是以什么样的方式实现的?在程序中可以定义泛型类、泛型方法,甚至泛型接口。首先,在定义类、接口、方法时用一个通用名称(如type, E)代替操作的具体数据类型,即把具体数据类型指定为一一个参数,在实例化时再把这个参数用实际的类型名称替换。这就是所谓的参数化类型,这和C++的Template class (类模板)具有异曲同工之功效。这样做的结果是简化代码并且增强安全性。
定义泛型通常有以下两种格式:;
●泛型类,定义在类名后面:
public class TestClassName <T,S extends T>{}
●泛型方法,定义在方法修饰符后面:
public <T,S extends T> T testMethod(T t, S s){}
T和S为通配符,以上表示定义泛型T、S,并且S继承于T。实例化泛型类时,以实际类名替代通配符,如TestClassName<String> list ;实例化泛型方法时,编译器会自动对类型参数进行赋值,当不能成功赋值时,报编译错误。以下示例为泛型类和泛型方法的定义及实例化。
示例:在Product类中用T和V代替具体数据类型String和Integer,直到声明对象时,才赋予实际的类型名称。
代码如下:
class Product<T,V>{
private T type;
private V id;
Product(T type, V id){
this.type=type;
this.id = id;
}
public T getType() {
return type;
}
public V getId() {
return id;
}
}
public class GenericTest {
public static void main(String[] args) {
Product<String,Integer> apple = new Product<>("ipad",12);
System.out.println("Product type: "+apple.getType());
System.out.println("Product id: "+apple.getId());
}
}
运行结果如下:
Product type: ipad
Product id: 12
此例中定义泛型类Product时,类名后面必须紧跟着通配符<T,V>,以代替实际的数据类型。Product 类的数据成员形参类型用T和V表示,用于接收外部传人的类型实参。构造方法是一个形参为T和V的泛型方法,泛型方法getType()和getID返回值分别以T和V表示。在定义数据成员和成员方法时,并不知道T、V表示的数据类型是什么,直到实例化一个Product对象apple时,才指明T真正替代的是String类型,V表示Integer类型集合,通过apple对象调用方法时,方法返回值会自动被具体化,被赋予实际的数据类型。
接下来解释泛型类定义格式public class TestClassName<T, S extends T>{}, 其中extends T表示对泛型类范围加以限制,限定范围通过extends关键字实现,这种方式称为限定泛型,比如< S extends Collection>表示S限定范围为Collection接口的具体类 ,实例化时,如果传人非Collection接口的具体类,则编译会报错。关键字extends其后可以是类或者接口,因此extends已不仅仅是继承的含义了,对上例而言应该理解为S类型是实现Collection接口的任意一个具体类,如果S后面是一个类名,那么S类型就是该类的子类。
另外,实例化泛型类或方法时,赋值的都是指定的具体类型,当赋值的类型不确定时,用通配符(?) 代替。例如:
List<?> aList;
List<? extends Number> aNumberList;
若没有extends,只指定了<?>,即为默认情况,意味着凡是Object及其子类,都可以,用来实例化。通配符既可以向下限制(通配符下限),例如,<? extends Collection>表示接受Collection及其实现的具体类;通配符也可向上限制(通配符上限),例如,<? super Double>表示类型只能接受Double及其上层父类类型如Number、Object的实例。
泛型编程通常遵循以下3个步骤:
1)定义一个用通配符作为类型参数的泛型类或泛型方法。
2)实例化泛型类的对象,把实际数据类型赋予对象。
3)实例化泛型方法,编译器自动对类型参数赋值,赋值不成功,则编译报错。
按照惯例,类型通配符命名为一个大写字母,Java API中常用的通配符名称如下:
●E: Element, 表示类型形参,可以接受具体的类型实参。 ●K: Key,键。 ●N: Number, 数。 ●T: Type,类型。 ●V: Value,值。 ●S、U、V等:第二、第三、第四个类型等。
9.3.2 泛型在集合中的应用
在实际开发中,集合中大量用到泛型。JDK 1.4.2 和更早版本的集合都有一个共同的安全隐患: 一旦将某个对象添加到一个集合中,该对象便失去了其原有的类型信息,成为Object对象,也就是集合只保存没有任何特定类型特征的对象。这就意味着集合可以容纳任意数据类型的对象,可以将任意类型的对象添加到同一集合中。从集合中取出对象时,不清楚它到底是什么类型,必须显式或强制类型转换,转换要求开发者预先知道实际的数据类型,一旦没有强制类型转换或者转换出错,编译器也不会报错,直到运行时出现异常,这种方式存在潜在的安全隐患。下例是一个需要强制类型转换的例子,此例中包含了ArrayList的使用方法。
示例:将Chicken类对象放入一个ArrayList集合中,再从中取出来
class Chicken {
int chickenNum;
public Chicken(int i) {
chickenNum = i;
}
public void show() {
System.out.println("Chicken id: " + chickenNum);
}
}
public class GenericTest2 {
public static void main(String[] args) {
ArrayList animals = new ArrayList();
for (int i = 0; i < 5; i++) {
animals.add(new Chicken(i));
}
for (int j = 0; j < 5; j++) {
((Chicken) animals.get(j)).show();
}
}
}
此例创建了一个名为animals的ArrayList 集合来存放对象,接着添加5个Chicken对象到集合animals里,这些对象在集合中都转换成了Object类型。当把对象从animals中取出来时,需要强制类型转换恢复成原有类型。若在JDK 1.5 及以上版本运行此程序,如不进.行强制类型转换,则编译无法通过。
针对这种隐患,JDK1.5引入了泛型,其目的在于增强集合使用的灵活性,把隐患控制在编译阶段,编译时自动检测类型安全,若有类型不匹配,则及时给出编译警告,并且自动进行隐式强制类型转换。泛型要求创建一个集合时,给出所能放置对象类型的通用名称,以代替象具体的数据类型,或者直接指明对象的具体类型名称,泛型使程序明确告知编译器,每个集合能够放置的特定类型是什么。下面用泛型修改上例,在创建ArrayList集合时,设定其放置Chicken类型对象。 代码如下:
class Chicken {
int chickenNum;
public Chicken(int i) {
chickenNum = i;
}
public void show() {
System.out.println("Chicken id: " + chickenNum);
}
}
public class GenericTest2 {
public static void main(String[] args) {
ArrayList<Chicken>animals = new ArrayList<Chicken>();
for (int i = 0; i < 5; i++) {
animals.add(new Chicken(i));
}
for (int j = 0; j < 5; j++) {
animals.get(j).show();
}
}
}
此例中在创建ArrayList时,即指定所放置对象的类型为Chicken,若添加对象类型不匹配,编译时会及时报错,把潜在的错误限制在了编译阶段。程序中凡是定义集合的语句,如果没有配合使用泛型,如此例中去掉,编译器将给出警告。
下例是一个使用泛型实现的LinkedList集合示例,此例中创建集合时以通配符作为对象的类型形参,直到向集合里添加对象时,再以实参形式指定其真正类型。
示例:用泛型实现-一个LinkedList集合。
代码如下:
public class GenericTest3<E> {
private LinkedList<E> list = new LinkedList<E>();
public void push(E o) {
list.addFirst(o);
}
public E top() {
return list.getFirst();
}
public E pop() {
return list.removeFirst();
}
@Override
public String toString() {
return list.toString();
}
public static void main(String[] args) {
GenericTest3<String> sl = new GenericTest3<>();
for (int i = 0; i < 5; i++) {
sl.push(String.valueOf(i));
}
System.out.println("s1 = " + sl);
}
}
运行结果如下:
s1 = [4, 3, 2, 1, 0]
总体而言,泛型在集合中使用时,无论是在创建一个集合时给出对象类型通配符,还是直接指明对象真正的类型名称,从集合中取出对象时不再需要强制类型转换,杜绝了运行时可能产生的潜在问题,保证程序运行的稳定性。
从前面讲述的通配符下限可推断出,在继承关系下,如果要把子类的对象添加到一个集合里,只需指定集合存放的对象类型为其父类类型,即只要是父子类关系的类型都允许放入集合中,取出对象时无须再进行强制类型转换。
|