在程序设计过程中,常出现数据类型不一样、对数据处理的过程和逻辑都一样的情况, 应该如何实现这种程序呢? 如果为每种数据类型编写不同的程序,不仅工作量大,而且不满 足代码重用的原则。这就需要泛型。从字面意思上看,泛型是编写的代码适用于广泛的类 型。如何做到这点呢? 这需要引入参数化类型的概念。所谓的参数化类型,是指编写代码 时,它所适用的类型并不立即指明,而是使用参数符号来代替,具体的适用类型延迟到用户 使用时才指定。由于参数被延迟指定,因此在使用参数类型的时候,可以根据需要指定它适 用的类型。 5.1 概述 JDK5的新特性之一就是支持泛型,因此用Java编写的代码具有更加广泛的表达能力。 当然,Java泛型的作用不止于此,它还保证了程序的类型安全,并消除了一些烦琐的类型转 换。然而,Java的泛型也存在一些限制,大部分是由类型擦除导致的。 5.1.1 使用继承实现代码重用 引入泛型之前,一般的程序都使用多态与继承来提高代码的灵活性和重用性。最常见 的用继承实现泛型的就是List容器:由于List容器不像数组限制得那么严格,允许存放任 何Java定义的类型,因此可以向容器中添加诸如牙刷、房子、课程这些不相关的对象;实现 的方法很简单,List存放的都是Object类型,由于Java中除了基本类型外的所有类都继承 自Object,因此可以添加任何类型到List中,代码如下。 01 package org.ddd.generic.example1; 02 public class GenericTest { 扫一扫 167 03 public static void main(String[] args) { 04 List listInteger = new ArrayList(); 05 List listString = new ArrayList(); 06 List listDate = new ArrayList(); 07 listInteger.add(new Integer(1)); 08 listString.add(new String("字符串")); 09 listDate.add(new Date()); 10 listInteger.add(new String("字符串")); //逻辑不正确,但语法正确, 11 //这很容易引起错误 12 Integer i = (Integer)listInteger.get(0); 13 String str = (String)listString.get(0); 14 Date date = (Date)listDate.get(0); 15 System.out.println("第一个数组中存放的是数字:" + i); 16 System.out.println("第二个数组中存放的是字符串:" + str); 17 System.out.println("第三个数组中存放的是日期:" + date.toString()); 18 } 19 } 运行结果如下。 01 第一个数组中存放的是数字:1 02 第二个数组中存放的是字符串:字符串 03 第三个数组中存放的是日期:Tue Aug 09 11:03:48 CST 2011 这个例子定义了3个List容器list1、list2、list3,并分别向其中存放Integer类型的数 组、String类型的字符串以及Date类型的日期。接着获取这些容器中的元素,并将其打印 在屏幕上。从这个例子可以看出,List容器的设计并没有针对某个具体的类,可以向其中存 放任何继承自Object的对象。 继承可以实现代码的重用,但是,Java满足里氏代换原则(任何父类可以出现的地方, 子类一定可以出现),因此父类出现的地方都可以用子类代替,并且没有办法限制具体使用 哪个子类。在一些场景中容易产生错误,如例子中的第10行,很容易产生类型转换的错误。 Java作为一种强类型的语言,对类型有严格的检查,但是这里失去了作用,这就需要使用 泛型。 5.1.2 泛型代码 使用继承实现的List虽然可以适用于很多类型,但也存在一些问题,最显而易见的就 是当从List中取出元素时,必须显式地将其转型成需要的类型。有时可以确定List中存放 的类型,比如规定在List中只允许存放Integer类型时,明明知道取出的必然是Integer类 型,但是每次获取元素时仍然需要显式转换,显然这些转型的代码很烦琐,而且没有必要。 还存在其他问题,比如当试图在只允许存放Integer的List中添加字符串类型时,编译器并 不会报错,这样错误就会在从List中取出该元素并将其转换成Integer时发生。显然,这种 可能出现的情况是无法接受的。针对以上可能出现的情况,泛型机制很好地解决了这些问 168 题,代码如下。 01 package org.ddd.generic.example2; 02 public class GenericTest { 03 public static void main(String[] args) { 04 List<Integer> list1 = new ArrayList<Integer>(); 05 List<String> list2 = new ArrayList<String>(); 06 List<Date> list3 = new ArrayList<Date>(); 07 list1.add(new Integer(1)); 08 // list1.add(new String("测试")); //编译错误 09 list2.add(new String("字符串")); 10 list3.add(new Date()); 11 Integer i = list1.get(0); 12 String str = list2.get(0); 13 Date date = list3.get(0); 14 System.out.println("第一个数组中存放的是数字:" + i); 15 System.out.println("第二个数组中存放的是字符串:" + str); 16 System.out.println("第三个数组中存放的是日期:" + 17 date.toString()); 18 } 19 } 运行结果如下。 01 第一个数组中存放的是数字:1 02 第二个数组中存放的是字符串:字符串 03 第三个数组中存放的是日期:Tue Aug 09 11:03:48 CST 2011 以上例子定义了Java5出现的类型参数化List:List<Integer>list1、List<String> list2、List<Date>list3。使用参数化类型定义List后,显而易见的变化就是泛型机制限制 了List容器中元素的类型,当试图在list1中添加String类型时,编译器会提示Themethod add(Integer)inthetypeList<Integer>isnotapplicableforthearguments(String)的错 误,提示在List<Integer>容器中添加String类型的元素是非法的。 5.1.3 算法与数据类型解耦 Java语言是一种强类型语言。强类型语言通常是指在编译或运行时,数据的类型有较 为严格的区分,不同数据类型的对象在转型时需要严格的兼容性检查。例如:类型Integer 和类型String是两种不同的类型,编译以下代码时,Java编译器在进行兼容性检查时将提 示错误。 01 Integer age; 02 String ageString = "23"; 03 age = ageString; 169 强类型的语言对提高程序的健壮性和开发效率都有利,但强类型语言导致一个问题: 数据类型与算法在编译时绑定,这意味着必须为不同的数据类型编写相同逻辑的代码,例如 比较两个数,必须为不同的数据类型编写如下代码。 01 public Integer compare(Integer a1,Integer a2) 02 { 03 return a1-a2; 04 } 05 public Float compare(Float a1, Float a2) 06 { 07 return a1-a2; 08 } 09 public Double compare(Double a1, Double a2) 10 { 11 return a1-a2; 12 } 以上代码是丑陋的(任何重复代码,或者重复模式的代码都是丑陋的)。 在软件开发过程中,经常出现这种情况:同一个算法适合所有数据类型,或者几种数据 类型,而不是某一种具体数据类型,即编写通用的代码。继承是解决这个问题的方法之一: 为适用这一算法的多种数据类型,抽象一个基类(或者接口),针对基类(或者接口)编写算 法。但在实际程序设计过程中,专门为特定的算法修改数据类型的设计不是好的习惯。另 外,如果使用已经设计好的数据类型,就没有办法解决问题了。例如上面减法的例子,不可 能再为Integer、Float、Double添加基类或者接口。泛型是解决“数据类型与算法在编译时 绑定”问题的有效方法之一。泛型的最大价值在于:在保证类型安全的前提下,把算法与数 据类型解耦。 5.2 泛型类型 上一节介绍了泛型的概念及特点,本节将介绍如何定义泛型类型。 5.2.1 泛型类 泛型类是指该类使用的参数类型作用于整个类,即在类的内部任何地方(不包括静态代 码区域)都可把参数类型当作一个真实类型来使用,比如用它作为返回值、定义变量等。泛 型类的定义很简单,只需在定义类的时候在类名后加入<T>这样一句代码即可,其中T是 一个参数,是可变的,代码如下。 01 package org.ddd.generic.example4; 02 public class Person<T> { 03 protected T t; 04 public Person(T t){ 扫一扫 170 05 this.t = t; 06 } 07 public String toString(){ 08 return "变量t 的类型是:" + t.getClass().getCanonicalName(); 09 } 10 } 以上例子定义了泛型类Person。定义泛型类与普通类的区别在于:在泛型类中使用的 类型参数必须在类名后指明,指明的方式就是采用<T>这种方式。当然,也可以为一个类 指明多个类型参数,代码如下。 01 package org.ddd.generic.example4; 02 public class Teacher<V,S> extends Person { 03 protected V v; 04 private S s; 05 public Teacher(Object t) { 06 super(t); 07 } 08 public void set(V v, S s){ 09 this.v = v; 10 this.s = s; 11 } 12 public String toString(){ 13 return super.toString()+"\n"+ 14 "变量v 的类型是:" + v.getClass().getCanonicalName()+"\n"+ 15 "变量s 的类型是:" + s.getClass().getCanonicalName()+"\n"; 16 } 17 } 定义方式很简单,但定义的时候会出现一个小问题:定义Teacher类的时候,编译器给 出一个错误提示:由于Person类没有定义无参构造函数,因此要求实现一个与父类参数相 同的构造函数。这本无可厚非,但当尝试定义一个T 类型的构造函数时,发现子类中已经 无法使用类型参数T了,那怎么办呢? 看看实现的代码,使用Object代替T,这样做的原因 将在讨论泛型擦除时说明。 子类可不可以使用父类的类型参数呢? 可以,但需要进行一点修改,代码如下。 01 package org.ddd.generic.example5; 02 public class Teacher<V,S> extends Person<V> { 03 protected V v; 04 private S s; 05 public Teacher(V t) { 06 super(t); 07 } 08 public void set(V v, S s){ 171 09 this.v = v; 10 this.s = s; 11 } 12 public String toString(){ 13 return super.toString()+"\n"+ 14 "变量v 的类型是:" + v.getClass().getCanonicalName()+"\n"+ 15 "变量s 的类型是:" + s.getClass().getCanonicalName()+"\n"; 16 } 17 } 只是将extends后的Person改为了Person<V>,子类就可以与父类一起共享类型参 数V 了,Person的类型参数T被指定为类型参数V 的类型。这时已经不需要使用Object 来定义参数了。 泛型类的使用方法也很简单,只需在构造的时候指明参数类型即可,代码如下。 01 package org.ddd.generic.example6; 02 public class GenericTest<T> { 03 public static void main(String[] args){ 04 Person<Integer> person = new Person<Integer>(5); 05 System.out.println("person======\n"+person); 06 //实际的类型也可以不指定,编译器能自动推断出实际的类型 07 Person<String> person1 = new Person<>("字符串"); 08 System.out.println("person1======\n"+person1); 09 Teacher<String, Date> teacher = new Teacher<>("字符串"); 10 teacher.set("xcy",new Date()); 11 System.out.println("teacher======\n"+teacher); 12 //person = person1; //报类型不兼容错误 13 } 14 } 输出结果如下。 01 person====== 02 变量t 的类型是:java.lang.Integer 03 person1====== 04 变量t 的类型是:java.lang.String 05 teacher====== 06 变量t 的类型是:java.lang.String 07 变量v 的类型是:java.lang.String 08 变量s 的类型是:java.util.Date 以上例子首先定义了一个Person类的对象,并指明其参数类型为Integer,然后在 Person的toString方法中输出了传入的参数类型。然后定义另外一个Person类的对象,并 指明其参数类型为String。这里使用了菱形语法,即在构建泛型类的对象时不指定具体的 172 类型参数,而是由编译器根据上下文进行推断,在这个例子中,编译器根据变量person1的 类型Person<String>可以推断出类型参数的类为String。 例子的最后试图把类型为Person<String>的对象赋值给类型为Person<Integer>的 变量,编译器会报类型不匹配的语法错误,说明Person<String>、Person<Integer>虽然 都是来自于同一个泛型类,但是是不兼容的类型。 5.2.2 泛型方法 泛型方法是在方法上声明类型参数,它只可作用于声明它的方法上。 泛型方法中类型参数的定义与泛型类相同,都是通过< >中加入某一参数,如<T>来 定义,但放的位置有所不同,以下代码定义了一个泛型方法。 01 package org.ddd.generic.example7; 02 public class Factory { 03 public <T> T generator(Class<T> t) throws Exception{ 04 return t.newInstance(); 05 } 06 } 下面来分析这段代码:首先是public后的<T>,作用是为该方法声明一个类型参数 T,声明该类型参数后,方法中就可以使用T作为一种类型使用了;接着是<T>后的T,作 用是声明方法的返回值类型,即参数T 型;该方法的参数为Class<T>t,即T 的类型信 息;最后返回的是通过反射方法newInstance()创建的T类的一个实例。 这个例子充分说明了反射与泛型联合使用时的强大功能,可以使用该方法创建任何需 要的对象,这一点也正是泛型优点的体现。 泛型方法的使用与普通方法一样,你甚至感觉不出使用的是功能强大的泛型。实例代 码如下。 01 package org.ddd.generic.example3_8; 02 public class GenericTest<T> { 03 public static void main(String[] args) throws Exception{ 04 Factory factory = new Factory(); 05 Date date = factory.generator(Date.class); 06 System.out.println(date.toString()); 07 Button button = factory.generator(Button.class); 08 System.out.println(button.toString()); 09 } 10 } 输出结果如下。 01 Tue Aug 09 17:18:55 CST 2011 02 java.awt.Button[button0,0,0,0x0,invalid,slabel=] 173 代码简洁而优美、灵活而强大。可以使用该方法生成任何继承自Object类的实例,当 然前提是该类有无参的构造方法。使用该方法时,你无须为那些烦琐的转型代码而烦恼,泛 型系统确保了类型的正确性。 5.2.3 泛型接口 泛型除了可以作用在类和方法上,还可以应用于接口上,其定义如下。 01 package org.ddd.generic.example9; 02 public interface Factory<T> { 03 public T create(); 04 } 泛型接口的定义与泛型类的定义相似,那么为什么还需要泛型接口呢? 拿工厂的例子 来说,不同工厂的生产方式各不一样,装载的零件也不相同,因此实现的方式也会各不相同, 所以需要在具体的工厂中实现它独有的生产方式,代码如下。 01 package org.ddd.generic.example10; 02 public class Car { 03 } 04 public class Computer { 05 } 06 public class CarFactory implements Factory<Car> { 07 @Override 08 public Car create() { 09 System.out.println("装载发动机!"); 10 System.out.println("装载座椅!"); 11 System.out.println("装载轮子!"); 12 return new Car(); 13 } 14 } 15 public class ComputerFactory implements Factory<Computer> { 16 @Override 17 public Computer create() { 18 System.out.println("装载主板!"); 19 System.out.println("装载CPU!"); 20 System.out.println("装载内存"); 21 return new Computer(); 22 } 23 } 24 public class GenericTest { 25 public static void main(String[] args) throws Exception{ 26 Factory<Car> carFactory = new CarFactory(); 27 Factory<Computer> computerFactory = new ComputerFactory(); 28 System.out.println("======开始生产汽车!======="); 174 29 carFactory.create(); 30 System.out.println("=====开始生产电脑!========"); 31 computerFactory.create(); 32 } 33 } 输出结果如下。 01 ======开始生产汽车!======= 02 装载发动机! 03 装载座椅! 04 装载轮子! 05 =====开始生产电脑!======== 06 装载主板! 07 装载CPU! 08 装载内存 以上例子实现了两个具体的工厂CarFactory和ComputerFactory,它们都实现了泛型 接口Factory<T>,并在实现的时候指明了工厂所生成的具体类型,指明的方式只需将原 有的类型参数T替换为具体的类型。如对CarFactory来说,将原有的Factory<T>替换 成Factory<Car>。这样当覆盖接口中的方法create时,生成的产品就是Car类型了。在 测试类中分别声明了CarFactory和ComputerFactory,并使用这两个工厂分别生产了一件 产品。 5.2.4 泛型与继承 泛型的继承容易给人一个误区,考虑下面的代码合法吗? 01 public class GenericTest { 02 public static void main(String[] args) throws Exception{ 03 Zoo<Animal> zoo = new Zoo<Animal>(new Animal()); 04 Zoo<Bird> birdZoo = new Zoo<Bird>(new Bird()); 05 zoo = birdZoo; 06 } 07 } 直觉可能告诉你合法,因为Animal是Bird的父类,那么Zoo<Animal>就是Zoo <Bird>的父类。但事实并非如此,当试图将birdZoo赋值给zoo时,编译器抛出一个错误: Typemismatch:cannotconvertfromZoo<Bird>toZoo<Animal>,提示这样赋值不合 法。其实Zoo<Animal>与Zoo<Bird>什么关系也没有,为什么要这样设计呢? 是为了 确保泛型的类型安全。假如编译器允许这样赋值,由于Fish也是Animal的子类,因此在 zoo中添加一个Fish完全合理,但是这对birdZoo就不可接受了,因为你通过zoo的引用向 Zoo<Bird>中添加了Fish的实例。这样当运行获取该实例时,把它当作Bird来使用,显然 是不合理的。 175 5.3 通配符 使用泛型实例时,需要为泛型指定具体的类型参数。如当使用List实例时,需要指明 List中需要存放的类型List<Integer>,然而有时泛型实例的作用域可能更加广泛,无法指 明具体的参数类型。那么Java泛型机制是如何解决的呢? Java设计者很聪明地设计了一 种类型:通配符类型。它表示任何类型,通配符类型的符号是"?",因此通配符类型可应用 于所有继承自Object的类上。 5.3.1 通配符的使用 当无法确定泛型类的具体参数类型时,一般使用通配符类型代替,以下代码演示了如何 使用通配符类型。 01 package org.ddd.generic.example11; 02 public class GenericTest<T> { 03 public static void main(String[] args) throws Exception{ 04 Class<? > clazz = Integer.class; 05 System.out.println(clazz.getCanonicalName()); 06 clazz = String.class; 07 System.out.println(clazz.getCanonicalName()); 08 clazz = Date.class; 09 System.out.println(clazz.getCanonicalName()); 10 } 11 } 输出结果如下。 01 java.lang.Integer 02 java.lang.String 03 java.util.Date 以上例子在创建Class对象clazz时使用了通配符类型"?",随后分别对其赋予Integer .class、String.class、Date.class,这3个类型分别为Class<Integer>、Class<String>以及 Class<Date>。可以看出使用通配符类型“?”后,clazz对象就可以表示各种不同类型。使 用通配符类型Class<? >与原生类型Class的区别是:前者表明是因为暂时无法确定参数 类型而使用了通配符类型,表示适合任何类型,而后者则可能是由于程序员疏忽或其他原因 而没有指明参数类型,因此Java编译器会提出警告。 5.3.2 通配符的捕获 考虑下面的问题:现有一个方法用于交换List中的两个元素,由于事先不知道List中 存放的元素类型,所以将参数设置成了含有通配符的实例,如List<?>list。由于并不知 扫一扫