步步理解 JAVA 泛型编程

步步理解 JAVA 泛型编程

转载自:


步步理解 JAVA 泛型编程(一)

JDK 1.5 相对于 JDK 1.4 来说变化最大的部分就是泛型,甚至可以说自 Java 1.0 发布以来也是最大的一次语言变化,因为要涉及到编译器的大改动。很早的时候大家对泛型的呼声很大,正如 C++ 的模板,C# 的泛型确实是个值得借鉴的好特性。JDK1.5 这前,很多人对于泛型是急不可耐,在 JDK1.4 下搭配一个外挂的 Generic Java 编译器,通老实 -Xbootclasspath、-bootclasspath 同样能让你在 1.4 的 JDK 上使用泛型:

  1. javac -J-Xbootclasspath/p:JSR14HOME\gjcrt.jar -bootclasspath JSR14HOME\collect.jar;JDK141HOME\jre\lib\rt.jar -source 1.5 FileName.java
  2. java -Xbootclasspath/p:JSR14HOME\gjc-rt.jar FileName

JDK 1.5 虽说出来这么久了,JDK 7 眼看都要见得天日了,不过可能我们很多人也只是知道怎么使用支持泛型的集合,也许还没有写过自己的泛型类,更不用说泛型的一些更高阶规范了。正如我们会在 Hibernate、Spring 里很熟练的使用零配置的注解,好像也很少有机会让我们去写处理注解的代码似的。因为毕竟我们不是书写框架的,多数时候是在应用它们。多看JDK 的源代码固然是个好办法,但除看之外,练手帮助理解是必不可少的。如果你熟悉 C++,可以与 C++ 的模板类比。现在来一步步了解泛型。首先泛型这一称谓听来有点抽象,换作参数化类型,或者借用模板类的叫法会好理解的多。比如说这样一个泛型类:

  1. package com.unmi;
  2. public class Demo<T> {
  3. private T first;
  4. public T getFirst() {
  5. return first;
  6. }
  7. public void setFirst(T first) {
  8. this.first = first;
  9. }
  10. }

声明部分 public class Demo 为 Demo 类指定了一个类型参数 T,那么在该方法就可以使用该类型 T,如作为实例、局部变量,方法参数或返回值。实际使用该类时 T 要被替换为具体的类型,如类型为 String,用 Demo demo= new Demo() 声明构造,那么上面的变量及方法的原型分别是:

  1. String first;
  2. String getFirst();
  3. void setFirst(String first);

如果具体类型为 Integer,用 Demo demo = new Demo() 声明构造,则上面的变量及方法的原型分别是:

  1. Integer first;
  2. Integer getFirst();
  3. void setFirst(Integer first);

可以是任何的具体类型,所以可以理解为泛型类是普通类的工厂。其实依照 C++ 的名词,前面类的定义称之为一个模板类,使用的时候是让模板中的类型具体化,这样好理解些。但要记得,Java 的泛型与 C++ 的模板实现的泛型内部机制还是有本质区别的。前面是在类的声明部分定义了一个类型参数,如果有如个类型参数也是可以的,写成public class Demo,多少个都行 public class Demo,在类的变量或方法中可以使声明部分定义的类型。类型用大写形式,一般都是用 T 指代 Type,K 指代 Key,V 指代 Value,其他靠着 T 的 U,S ...... 都随意用了。不妨来看个 C++ 的模板类,以及使用代码:

  1. //类声明前要加个模板声明打头
  2. template <class Type> class Stack{
  3. public:
  4. Type Push(Type item);
  5. };
  6. //每个方法也都要以模板声明打头
  7. template<class Type> Type Stack<Type>::Push(Type item){
  8. return item;
  9. }
  10. int main(int argc, char* argv[]){
  11. Stack<int> fishes;
  12. cout<<fishes.Push(123)<<endl;
  13. return 0;
  14. }

步步理解 JAVA 泛型编程(二)

前面讲了泛型类的定义,你也可以在一个普通类中单单定义一个泛型方法。也就是说类能够带个类型参数,方法也可以带类型参数的。还是来看个例子(包括如何应用),一个获得数组中间元素的方法,因为数组中存储元素的类型是不定的,所以把该方法定义成泛型的。

  1. package com.unmi;
  2. /**
  3. * 泛型方法示例
  4. * @author Unmi
  5. */
  6. public class ArrayAlg {
  7. //这个就是在普通类 ArrayAlg 中定义的泛型方法
  8. public static <T> T getMiddle(T[] a){
  9. return a[a.length/2];
  10. }
  11. public static void main(String[] args) {
  12. String[] names = {"Fantasia","Unmi","Kypfos"};
  13. //String middle = ArrayAlg.<String>getMiddle(names);
  14. //上面那样写是可以,编译器可推断出要调用的方法,所以省去<String>
  15. String middle = ArrayAlg.getMiddle(names);
  16. System.out.println(middle);
  17. }
  18. }

我们之所以说上面的 ArrayAlg 是个普通类,是因为没有在类声明部分引入类型参数(比如声明为 public class ArrayAlg)。同时在理解上面的泛型方法 getMiddel() 时应联想到泛型类是如何定义的。

对比前面泛型类的定义 public class Demo{.......},那么在类的变量、方法参数、返回值等处就可以使用参数类型 T。这里定义了泛型方法 public static T getMiddle(T[] a){......},同样是用 的形式为方法引入了一个类型参数,那么这个类型 T 可用作该方法的返回值、参数、或局部变量。注意这里的 T,前部分 是定义泛型方法的类型参数,后部 T 是该方法的返回值。泛型类的类型参数() 是紧贴着类名的后面,而泛型方法的类型参数() 是紧贴着方法声明的返回类型的前面。我们在使用泛型类,也是在构造的时候类紧贴类名后加上具体的参数类型,如 Demo demo = new Demo();类似的,我们在使用泛型方法时,从代码语法是在紧贴方法名的前面加代换上具体的参数类型,如ArrayAlg.getMiddle(names),调用方法时不能有返回类型了,所以具体参数类型 靠紧了方法名。前面代码中,我们说既可以用 ArrayAlg.getMiddle(names); 来调用定义的泛型方法 public static T getMiddle(T[] a),也可省写为
ArrayAlg.getMiddle(names); 来调用该方法。通常我们是这么做的,原因是 Java 编译器通过参数类型、个数等信息能推断出调用哪一个方法。但 Java 编译器也不是完全可靠的,有时候你必须显式的用 ArrayAlg.getMiddle(names); 这种形式去调用明确的方法。例如,我们在 ArrayAlg 中多定义一个 public static String getMiddle(String[] a){......} 方法,完整代码如下:

  1. package com.unmi;
  2. /**
  3. * 泛型方法示例,泛型方法的显式调用
  4. * @author Unmi
  5. */
  6. public class ArrayAlg {
  7. //这个就是在普通类 ArrayAlg 中定义的泛型方法
  8. public static <T> T getMiddle(T[] a){
  9. return a[a.length/2];
  10. }
  11. public static String getMiddle(String[] a){
  12. return "Not Generic Method.";
  13. }
  14. public static void main(String[] args) {
  15. String[] names = {"Fantasia","Unmi","Kypfos"};
  16. //必须显式的用 <String> 去调用定义的泛型方法
  17. String middle1 = ArrayAlg.<String>getMiddle(names);
  18. System.out.println(middle1); //输入 Unmi,调用了泛型方法
  19. //不指明参数类型 <String> 则调用的是那个普通方法
  20. String middle2 = ArrayAlg.getMiddle(names);
  21. System.out.println(middle2); //输出 Not Generic Method
  22. }
  23. }

这也有些像我们的 C++ 的模板类,在模板具体化的时候存在 隐式实例化、显式实例化、显式具体化、部分具体化的情况,怎么看 C++ 的模板类还是要比 Java 的泛型复杂。

当然,上面代码只是说明 Java 的泛型方法在语法上会出现这种情况,倘若谁真写出的泛型代码需要用 ArrayAlg.getMiddle(names); 显式的去调用泛型方法,那一定要考虑重构它了。明白了这一点难道就没有半点实际的意义吗,自然也不是,我们可以把它牢记为潜在的 Bug 容身之所。

进一步联系到前一篇,泛型类在定义的时候可以指定多个类型参数(用 形式),在定义泛型方法时同样用 的形式,调用的时候与一个参数时类似,如 ArrayAlg.getByIdx(names, new Date())。也不怕浪费几个字,大致浏览一下多类型参数时泛型方法的定义与使用的代码:

  1. package com.unmi;
  2. import java.util.*;
  3. /**
  4. * 泛型方法示例,多类型参数的情况
  5. * @author Unmi
  6. */
  7. public class ArrayAlg {
  8. //由索引获得
  9. public static <T,U> T getByIdx(T[] a, U b){
  10. //依照 HashMap 实现的算法,由 b 得到一个不越界的索引
  11. int h = b.hashCode();
  12. h ^= (h >>> 20) ^ (h >>> 12);
  13. h = h ^ (h >>> 7) ^ (h >>> 4);
  14. int index = h & (a.length-1);
  15. return a[index];
  16. }
  17. public static void main(String[] args) {
  18. String[] names = {"Fantasia","Kypfos","Unmi"};
  19. //显式的用 <String, Object> 去调用定义的泛型方法
  20. String name = ArrayAlg.<String, Date>getByIdx(names,new Date());
  21. //隐式调用泛型方法
  22. String name1 = ArrayAlg.getByIdx(names,"GameOver");
  23. //会输出 Unmi:Fantasia,或 Fantasia:Fantasia
  24. System.out.println(name + ":" + name1);
  25. }
  26. }

因为现在还不想涉及到调用类型参数的特定方法,所以参照 HashMap 算法,由第二个类型参数算出数组范围内的索引。留意两种调用泛型方法的方式,应用隐式调用在有些情况下也是会产生二义性的。


步步理解 JAVA 泛型编程(三)

前面介绍的无论是泛型类还是泛型方法,基本上都是把定义的类型参数作为一个整体来操作,放到数组或取出来,顶多就是调用了一下 hashCode() 方法,因为这是 Java 的根对象拥有的方法。比如说一个泛型数组,要得到其中的最小元素:

  1. package com.unmi;
  2. /**
  3. * 泛型,类型变量的限定
  4. * @author Unmi
  5. */
  6. public class ArrayAlg {
  7. public static <T> T main(T[] a){
  8. if(a==null || a.length ==0){
  9. return null;
  10. }
  11. T smallest = a[0];
  12. for(int i=0;i<a.length;i++){
  13. if(smallest.compareTo(a[i])>0){
  14. smallest = a[i];
  15. }
  16. }
  17. return smallest;
  18. }
  19. }

在上面的代码中,要比较大小,所以调用了类型 T 的 compareTo() 方法,我们期望具体类型是一个实现了 compareTo() 方法的类型。不过,很可惜,Java 泛型还是与 C++ 的模板有别,在 C++ 中真的对于类似上面的代码,具体类型有 compareTo() 则可以通过,具体类型没有 compareTo() 则不能通过,是推延至使用代码时确定具体类型是否满足条件。而在 Java 中无论你的具体类型是什么,上面的代码本身就是无法编译通过的,错误为:

The method compareTo(T) is undefined for the type T

也就是说在你编写泛型的时候就该限类型参数的一个更窄的范围,不是所有的 Object,而是那些实现了 compareTo() 方法的类型,比如说实现了 Compareable 接口的类型。为了做到这一点,对于前面方法声明部分就要做如下进一步约束:

public static T min(T[] a){......}

这里只表示到时的具体类型 T 是 Comparable 的一种类型,extends 后是接口,还是类都是用 extends 关键字,不在此区分接口还是类,只表示 Is-A 的关系。这样你在使用该泛型的时候具体类型一定要是实现了 Comparable 接口的类型,比如用这样的代码 ArrayAlg.main(new Object[]{"1","2","3"}); 调用,则会有下面的错误提示:

Bound mismatch: The generic method main(T[]) of type ArrayAlg is not applicable for the arguments (Object[]). The inferred type Object is not a valid substitute for the bounded parameter ArrayAlg.java

因为 Object 并未实现 Comparable,用 ArrayAlg.main(new String[]{"1","2","3"}); 调用则无误,String 是实现了 Comparable 接口的。如果在泛型实现中会调用到多个方法,要求泛型参数同属不同的类型,就 extends 多个接口或类,中间用 & 符号隔开。

  1. public static <T extends Comparable & Serializable> T foo(T[] a){
  2. //a[i].compareTo(a[i-1]);
  3. //xxx.writeObject(a[i]);
  4. }

为什么是用 & 而不用逗号(,) 呢,因为逗号还有作他用,当有多个类型参数时就该写作:

  1. public static <T extends Comparable & Serializable, U extends Runnable> T foo(T a, U b){
  2. //a[i].compareTo(a[i-1]);
  3. //xxx.writeObject(a[i]);
  4. //new Thread(u).start();
  5. }

虽然这里简单的用 extends 统一的表示了原有的 extends 和 implements 的概念,但仍要遵循应用的体系,Java 只能继承一个类,可以实现多个接口,所以你的某个类型需要用 extends 限定,有多种类型的时候,只能存在一个是类,而且参考变参的做法,类写在第一位,接口列在后面,也就是:

这里的例子仅演示了泛型方法的类型限定,对于泛型类中类型参数的限制用完全一样的规则,只是加在类声明的头部,如:

  1. public class Demo<T extends Comparable & Serializable>{
  2. //该泛型类中就可以用 Comparable 声明的方法和 Seriablizable 所拥有的特性了
  3. }

多个类型参数的情况类推就是了。

来自为知笔记(Wiz)

时间: 2024-12-23 01:03:44

步步理解 JAVA 泛型编程的相关文章

【转】深入理解Java:SimpleDateFormat安全的时间格式化

[转]深入理解Java:SimpleDateFormat安全的时间格式化 想必大家对SimpleDateFormat并不陌生.SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调试的问题,因为 DateFormat 和 SimpleDateFormat 类不都是线程安全的,在多线程环境下调用 format() 和 parse() 方法应该使用同步代码来避免问题.下面我们通过一个具体的场景来一步步的深入

从零开始理解JAVA事件处理机制(3)

我们连续写了两小节的教师-学生的例子,必然觉得无聊死了,这样的例子我们就是玩上100遍,还是不知道该怎么写真实的代码.那从本节开始,我们开始往真实代码上面去靠拢. 事件最容易理解的例子是鼠标事件:我们点击鼠标,鼠标发送指令,执行代码. 一:鼠标点击事件处理模型基础版 这个时候,我们必须去查看下JDK中相关类型.对照着上一节<从零开始理解JAVA事件处理机制(2)>中的UML图,我们很快发现,对应HomeworkListener,JDK中有MouseListener,其实我们靠分析也能得知,Mo

深入理解Java虚拟机(类文件结构+类加载机制+字节码执行引擎)

周志明的<深入理解Java虚拟机>很好很强大,阅读起来颇有点费劲,尤其是当你跟随作者的思路一直探究下去,开始会让你弄不清方向,难免有些你说的啥子的感觉.但知识不得不学,于是天天看,反复看,就慢慢的理解了.我其实不想说这种硬磨的方法有多好,我甚至不推荐,我建议大家阅读这本书时,由浅入深,有舍有得,先从宏观去理解去阅读,再慢慢深入,有条不紊的看下去.具体来说,当你看书的某一部分时,先看这部分的章节名,了解这部分这一章在讲什么,然后再看某一章,我拿"类文件结构"这一章来说,我必须

《深入理解Java集合框架》系列文章

Introduction 关于C++标准模板库(Standard Template Library, STL)的书籍和资料有很多,关于Java集合框架(Java Collections Framework, JCF)的资料却很少,甚至很难找到一本专门介绍它的书籍,这给Java学习者们带来不小的麻烦.我深深的不解其中的原因.虽然JCF设计参考了STL,但其定位不是Java版的STL,而是要实现一个精简紧凑的容器框架,对STL的介绍自然不能替代对JCF的介绍. 本系列文章主要从数据结构和算法层面分析

java泛型编程

一般的类和方法都是针对特定数据类型的,当写一个对多种数据类型都适用的类和方法时就需要使用泛型编程,java的泛型编程类似于C++中的模板,即一种参数化类型的编程方法,具体地说就是将和数据类型相关的信息抽象出来,主要提供通用的实现和逻辑,和数据类型相关的信息由使用时参数决定. 一.泛型类: 栈的实现 示例代码: package com.genericity; import org.junit.Test; /** * @Title: LinkedListStack.java * @Package c

深入理解Java:注解(Annotation)--注解处理器

深入理解Java:注解(Annotation)--注解处理器 如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了.使用注解的过程中,很重要的一部分就是创建于使用注解处理器.Java SE5扩展了反射机制的API,以帮助程序员快速的构造自定义注解处理器. 注解处理器类库(java.lang.reflect.AnnotatedElement): Java使用Annotation接口来代表程序元素前面的注解,该接口是所有Annotation类型的父接口.除此之外,Java在java.l

深入理解Java:注解(Annotation)自己定义注解入门

深入理解Java:注解(Annotation)自己定义注解入门 要深入学习注解.我们就必须能定义自己的注解,并使用注解,在定义自己的注解之前.我们就必须要了解Java为我们提供的元注解和相关定义注解的语法. 元注解: 元注解的作用就是负责注解其它注解. Java5.0定义了4个标准的meta-annotation类型.它们被用来提供对其它 annotation类型作说明.Java5.0定义的元注解: [email protected], [email protected], [email pro

深入理解Java PriorityQueue

深入理解Java PriorityQueue PriorityQueue 本文github地址 Java中PriorityQueue通过二叉小顶堆实现,可以用一棵完全二叉树表示.本文从Queue接口函数出发,结合生动的图解,深入浅出地分析PriorityQueue每个操作的具体过程和时间复杂度,将让读者建立对PriorityQueue建立清晰而深入的认识. 总体介绍 前面以Java ArrayDeque为例讲解了Stack和Queue,其实还有一种特殊的队列叫做PriorityQueue,即优先

【转】java提高篇(二)-----理解java的三大特性之继承

[转]java提高篇(二)-----理解java的三大特性之继承 原文地址:http://www.cnblogs.com/chenssy/p/3354884.html 在<Think in java>中有这样一句话:复用代码是Java众多引人注目的功能之一.但要想成为极具革命性的语言,仅仅能够复制代码并对加以改变是不够的,它还必须能够做更多的事情.在这句话中最引人注目的是"复用代码",尽可能的复用代码使我们程序员一直在追求的,现在我来介绍一种复用代码的方式,也是java三大