试读版(第一发布草稿版,作者-王新华) - 1 - 第 11 章 泛型 本章简介: 应广大 Java 的支持者的要求,从 Java 5.0 开始支持泛型这一语法特性,这个借鉴于 C++的语言特性使得 Java 程序的编写更加灵活。 所含章节:  让链表更灵活  泛型  实验 教学建议: 本章的讲述是从上一章中介绍的链表的数据结构开始的,从一个诉求开始,逐渐引入泛型。从一个侧面 揭示了需要泛型的原因。这对于初学者理解并逐渐掌握泛型是非常重要的。 建议本章整体教学课时:3~6 小时。 学习目标: 1) 理解泛型的作用和意义; 2) 能够熟练使用泛型定义泛型类和泛型方法; 3) 理解泛型的替换原则(类型擦除)与机制。 第 11.1 节 让链表更灵活 先来简单回顾一下之前在攻破“学生信息管理系统”时立下汗马功劳的 StudentInfoLink 链表类。它提供 了一种可伸缩的线性存储结构,方便了数据在内存中的临时存储,方便了不同模块之间的数据传递。但这个 StudentInfoLink 却有先天的缺陷。这些缺陷值得程序员们仔细思索并找到解决方案。 11.1.1 之前链表的局限性 StudentInfoLink 这个链表的数据结构的设计以及提供的方法已经能够非常方便的为 StudentInfo 类提供 可伸缩的线性存储了,它的缺陷并不在于那种链表节点的设计方式,而在于:如果此时我们需要对另一个类 进行可伸缩的线性存储呢?比如,做一个针对于 String 类型的链表。你可能会回答:那怕啥,再如法炮制的 做一个 StringLink 类呗。 读者可以尝试一下,再做一个对应的 Link 链表类试试,建议初学编程的人现在就试试。做出来之后再对 比一下看看,发现了什么? 是的,你会发现除了被保存的数据类型不一样以外,其他的数据处理逻辑是一模一样的,就因为一个数 据类型的变化却要将这么多代码重新编写,如果有多种数据类型都需要这种存取逻辑,那不就意味着不断的 重复么?这真是 Bad smell! 有没有什么办法简化一下这种编程呢? 试读版(第一发布草稿版,作者-王新华) - 2 - 11.1.2 企图让链表更灵活 聪明的人一定想到了,做一个针对于 Object 类型的链表不就行了么,因为 Object 是万元归一的根本, Object 代表了所有的数据类型(甚至包括基础数据类型,这是因为自动装箱与拆箱机制的存在),所以如果 针对 Object 编写链表程序,那就意味着这个链表对所有的数据都能进行链表的数据存储。好的,这就开始 编写相关代码吧。参考代码如下: package 第 1 节; /** * 针对 Object 类型的链表存储类 * @author 王新华 */ public class ObjectLink { private Object data; private ObjectLink next; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public ObjectLink getNext() { return next; } public void setNext(ObjectLink next) { this.next = next; } /** * 将新的数据加入到当前链表节点所代表的链表中 * @param data 被添加的数据,而非节点 */ public void add(Object data){ ObjectLink head = this; //调用 add 方法的节点被视为头节点 ObjectLink newNode = new ObjectLink(); //创建将要新增的节点 newNode.setData(data); //设置新节点的数据域 newNode.setNext(head.getNext()); head.setNext(newNode); } /** * 根据下标从 this 所代表的链表中删除对应的链表节点 * @param indexI 被删除的节点的下标 * @return 被删除的节点保存的数据 */ public Object removeAt(int indexI){ ObjectLink cursor = this; ObjectLink removeNode = cursor.getNext(); int i = 0; while(indexI >= 0 && removeNode != null){ //通过遍历链表寻找要被删除的节点 if(i == indexI){ //符合条件则表示该节点是要被删除的节点 cursor.setNext(removeNode.getNext()); //将找到节点从链表中拆出 removeNode.setNext(null); return removeNode.getData(); //将删除的节点的数据以返回值输出 } cursor = cursor.getNext(); removeNode = cursor.getNext(); i++; } return null; //若没有找到要删除的节点,则返回 null 试读版(第一发布草稿版,作者-王新华) - 3 - } /** * 根据传递的 data 从 this 所代表的链表中找到与之对应的节点并将其删除 * @param data 被删除的数据 */ public void remove(Object data){ ObjectLink cursor = this; ObjectLink removeNode = cursor.getNext(); while(removeNode != null){ //通过遍历链表寻找要被删除的节点 if(data == removeNode.getData()){ //符合条件则表示该节点是要被删除的节点 cursor.setNext(removeNode.getNext()); //将找到节点从链表中拆出 removeNode.setNext(null); } cursor = cursor.getNext(); removeNode = cursor.getNext(); } } /** * 得到 this 所代表的链表所拥有的节点数。 * @return 链表的节点个数 */ public int size(){ ObjectLink cursor = this.getNext(); int size = 0; while(cursor != null){ cursor = cursor.getNext(); size++; } return size; } /** * 将链表中各个节点显示出来 */ public void showLink(){ ObjectLink cursor = this.getNext(); while(cursor != null) System.out.println(cursor.getData()); } /** * 根据下标从 this 所代表的链表获取对应节点所保存的数据 * @param indexI 需要得到数据的节点的下标 * @return indexI 对应的节点中保存的数据 */ public Object getAt(int indexI){ ObjectLink cursor = this.getNext(); int i = 0; while(indexI >= 0 && cursor != null){ if(indexI == i) return cursor.getData(); i++; cursor = cursor.getNext(); 试读版(第一发布草稿版,作者-王新华) - 4 - } return null; } /** * 将链表变为 String 对象 */ public String toString(){ String str = ""; ObjectLink cursor = this.getNext(); while(cursor != null){ str = str + cursor.getData() + ","; cursor = cursor.getNext(); } if(str.length() > 0) str = str.substring(0, str.length() - 1); return str; } } 但是,这样真的好么?有没有弊病呢? 针对 Object 编写的链表确实能够管理不同类型的数据,但是却存在一个很大的缺陷。之前探讨的 StudentInfo 或某种数据类型的链表,只可能针对某一种数据类型进行链表存取,因此在使用时,一个链表对 象只能操作某一种数据类型。但此时针对于 Object 的链表的对象中却可以同时存放若干种非常不同的数据。 看如下示例: ObjectLink link = new ObjectLink(); link.add("abc"); link.add(new Integer(12)); link.add(new Object()); 这样的话,一来不符合之前链表设计的一个初衷:一个链表对象只能管理某一种数据类型;二来增加了 这种链表调用的不确定性,可能会带来风险。 那到底用不用 Object 呢?如果要使用 Object 的话,那么使用 Object 的矛盾又该如何调和呢? 第 11.2 节 泛型 其实之前在讲述的时候,已经将解决这个问题的中心点讲述了:存储逻辑是一致的,就只有被管理的数 据类型不一样。也就是说,存在着一种统一的程序逻辑对不同的数据类型进行操作的肯能行。这种情况不同 于函数、方法、结构体、类等等所能描述的情况。这种特殊的情况中被来回替换的数据类型就像是有一个模 板,这个模板被相同的处理流程使用。这个概念在后来的一些语言中被抽象为了模板,例如 C++。 在 Java 5.0 之后也引入了相同概念的语法机制,只是称呼发生了变化,将其称之为泛型,译为用一种代 称泛指所有被允许的类型。 11.2.1 泛型类 这种机制允许在编写一个 Java 类时,用一种特殊的符号来指代任何被允许的可能的数据类型,这个符 号就被称之为泛型,这个 Java 类被称之为泛型类。 试读版(第一发布草稿版,作者-王新华) - 5 - 这个泛型所指代的数据类型并不在定义泛型类时被确切的确定下来,而是在使用这个泛型类时再进行 确定。不同的两次的泛型类使用可以针对泛型进行两次不同的类型确定,两次相互之间是不影响的,但是某 一次使用中将其确定下来之后,那么这个泛型类中的泛型就统一代表了一种数据类型。 接下来看看泛型类的定义与使用,格式: 格式 11-1 泛型类的书写形式 public class 类名称<泛型名称>{ ... } 解释:上述格式中的类名称与泛型名称根据编程者意愿书写,尖括号看起来好像很奇怪,事实上是语法 中的一部分,是必须书写的。 有人会问:这个泛型名称有啥用呢?这个泛型名称就是上面所说的“符号”,它是随意给的并不代表某 种已经存在的类或类型,但在这个类的定义体中却代表了一种数据类型,可以使用它来定义变量。 看如下的泛型类的定义与使用示例: 例 11-1 第一个泛型类 FirstGeneric 以及其测试类 FirstGenericTest package 第 2 节.第 1 小节; /** * FirstGeneric 是一个泛型类 * @author 王新华 * @param G 是泛型符号,在泛型类 FirstGeneric 中当做一种数据类型名称使用 */ public class FirstGeneric { // G 就是泛型符号 private G data; //使用泛型符号定义的一个类的实例域 public void showData(){ System.out.println("This Generic's data is " + this.data); } public void setData(G data){ this.data = data; } public FirstGeneric(G data){ //使用泛型符号定义了方法的形参 this.data = data; } public FirstGeneric(){ } } package 第 2 节.第 1 小节; /** * FirstGeneric 的测试类 * @author 王新华 */ public class FirstGenericTest { public static void main(String[] args) { //在使用泛型类时,才会确定其泛型代表的具体类型 FirstGeneric gS = new FirstGeneric(); //这次使用中 gS 对象中的泛型代表的是 String 类型,gS 中的所有 G 都表示 String 试读版(第一发布草稿版,作者-王新华) - 6 - gS.setData("abc"); //你会发现 gS 对象的 setData 方法的参数只能是 String 类型或与之相容的对象 gS.showData(); FirstGeneric gI = new FirstGeneric(456); //这次使用中 sI 对象中的泛型代表的是 Integer 类型,gI 中的所有 G 都表示 Integer gI.showData(); gI.setData(789); gI.showData(); //你会发现 gI 对象的 setData 方法的参数只能是 Integer 类型或与之相容的对象 } } 11.2.2 替换原则 那么 Java 内部到底是怎么处理泛型的呢?又该如何理解泛型呢? 其实道理非常简单,在泛型类的定义体中的泛型事实上只是一个指代符号,指代任何一种可能的被允许 的类型。在使用泛型类定义一个对象变量时,需要明确说明这个泛型指代什么类型,此时会将这个泛型类此 时定义的对象变量中的泛型统一替换成明确说明的类型。 例如:上例中 FirstGeneric gS = new FirstGeneric(); 这句代码,通过尖括号说明了 gS 中的 G 明确指代 String 类型,此时 gS 对象中的 G 全部被替换成了 String 类型。 需要注意的是,泛型不能代表基础数据类型,也就是说类型替换时,也不能被替换成基础数据类型。 这种机制就如下图所描述一样。 图 11-1 泛型的替换机制与原则 这种机制又被称之为类型擦除。将泛型擦除,用使用时明确指定的类型替代。 11.2.1 泛型遇上继承与实现 此时再提出一个问题:在使用泛型类时能不能不指明泛型的类型呢?就像使用普通类那样,不书写尖括 号。例如:对于 FirstGeneric 类,使用时这样 FirstGeneric g = new FirstGeneric(); 答案是:可以。但这个时候可能会产生一个不能确定的问题:如果真是如此书写的话,那其中的泛型会 被明确指定为某种类型么? 这个问题事实上是可以确定的,这种情况下在 Java 语法机制中将泛型确定为泛型所能代表的顶层父类。 在没有指明泛型的父类的时候,其父类就是 Object。 试读版(第一发布草稿版,作者-王新华) - 7 - 什么?泛型类中的泛型拥有父类?对的,这是理所当然的。在讲解 Java 的类的继承时说到过所有类都 直接或间接继承于 Object,所有类都有父类。既然泛型最终代表的是一个类,那么泛型就一定拥有父类。 泛型的这个父类在没有指明的情况下,就是 Object。那么是不是意味着可以指明泛型的父类呢?答案是 肯定的。具体的写法就是,在定义泛型类时,在尖括号中的泛型后可以使用 extends 关键字,然后书写泛型 所继承的父类,格式如下: public class 类名称<泛型名称 extends 父类或接口>{ ... } 和其他地方使用 extends 关键字不同的地方在于:1)这里表示的并不是泛型类继承于这个类,而是说 这个泛型名称只能被替换为这个父类或接口的子类或子接口;2)这里的 extends 后既可以书写类名称,也 可以书写接口名称。 泛型引入的这个机制可以使得泛型在被使用时能够对泛型进行更强的限定,不至于使其失控。请参看如 下截图: 图 11-2 带有继承的泛型的使用示例 11.2.2 泛型方法 也可以使用泛型单个修饰一个方法,这种方法被称之为泛型方法。与泛型类一样,泛型方法中的泛型在 某一次使用时才确定其类型。 泛型方法存在的一个主要原因就是,有时候并不需要让某一个类整个定义体都要使用某种统一的泛型, 而只是一个方法需要一个泛型。这个时候就需要使用泛型方法了。 泛型方法的语法定义形式与普通方法稍有区别,区别在于需要再方法的返回值前通过尖括号声明泛型 名称,格式如下: 作用范围修饰词 [static] <泛型名称> 返回值类型名称 方法名称(参数列表){ ... } 试读版(第一发布草稿版,作者-王新华) - 8 - 与泛型类一样,泛型方法依然使用的是替换原则,泛型名称只在这个方法的返回值、参数、局部变量中 代表数据类型。当然也可以使用 extends 关键字说明泛型的父类。参看如下使用示例: package 第 2 节.第 1 小节; public class GenericMethod { public static void testGeneric(G data1, G data2){ System.out.println(data1.getClass()); System.out.println(data2.getClass()); } public static void main(String[] args) { testGeneric(123, "abc"); testGeneric("def", "uuu"); testGeneric(33.3, 6); } } 可以看到,泛型方法与泛型类有一个比较大的区别:泛型类中的泛型被替换成了统一的类型,而泛型方 法中的泛型可以同时被替换成不同的类型,这取决于如何使用泛型方法中的泛型。通常都会在方法的参数中 使用泛型,以此可以确定这个参数的具体类型。 第 11.3 节 实验 11.3.1 实验 1 要求:将上一章中讲述的链表改造成针对于 Object 对的链表,并尝试使用。 11.3.2 实验 2 要求:随意编写多个泛型类,为这些泛型类定义足够丰富的方法,并充分使用它们。仔细观察使用过程。 11.3.3 实验 3 要求:定义一个泛型类,在使用时不指定泛型的具体类型,观察有啥变化。看看泛型被替换成了什么类 型? 11.3.4 实验 4 要求:定义一个泛型类,让泛型继承于一个父类或接口,然后再对其进行充分使用。 11.3.5 实验 5 要求:定义多个泛型方法(包括实例的泛型方法与非实例的泛型方法),并充分使用它们。 试读版(第一发布草稿版,作者-王新华) - 9 - 11.3.6 实验 6 要求:使用泛型再一次改造链表类,使其能够更具灵活性。并使用它替换第 10 章的学生信息管理系统 中的 StudentInfoLink 类。 11.3.7 实验 7 要求:Java 中提供了很多针对数据结构的类,这些类大多数都使用到了泛型。请充分查阅 ArrayList 类的 API,并使用 ArrayList 类替换第 10 章的学生信息管理系统中的 StudentInfoLink 类。 第 11.4 节 本章总结 泛型是 Java 中非常有用的一个机制,它使得 Java 程序更加灵活、可伸缩。初次接触可能并不是很好理 解,要快速掌握,其要点就是要理解泛型的替换原则(类型擦除),其次就是要熟悉泛型的写法。 11.4.1 是否达到学习目标 目标1) 是否完成了本章所有的示例与练习? 目标2) 是否根据本章讲述做过逻辑推导? 目标3) 是否理解了泛型的替换原则? 目标4) 是否能熟练定义和使用泛型类? 目标5) 是否能熟练定义和使用泛型方法?
还剩8页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 6 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

saoqin2005

贡献于2015-03-20

下载需要 6 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf