多角度看 Java 中的泛型
【IT168技术文档】
泛型是 Sun 公司发布的 JDK 5.0 中的一个重要特性,它的最大优点是提供了程序的类型安全同可以向后兼容。为了帮助读者更好地理解和使用泛型,本文通过一些示例从基本原理,重要概念,关键技术,以及相似技术比较等多个角度对 Java 语言中的泛型技术进行了介绍,重点强调了泛型中的一些基本但又不是很好理解的概念。
为了避免和 C++ 中的模板混淆,本文简要介绍了 Java 中的泛型和 C++ 中的模板的主要区别,希望这种比较能够帮助读者加深对泛型的理解。
引言
很多 Java 程序员都使用过集合(Collection),集合中元素的类型是多种多样的,例如,有些集合中的元素是 Byte 类型的,而有些则可能是 String 类型的,等等。Java 语言之所以支持这么多种类的集合,是因为它允许程序员构建一个元素类型为 Object 的 Collection,所以其中的元素可以是任何类型。
当使用 Collection 时,我们经常要做的一件事情就是要进行类型转换,当转换成所需的类型以后,再对它们进行处理。很明显,这种设计给编程人员带来了极大的不便,同时也容易引入错误。
在很多 Java 应用中,上述情况非常普遍,为了解决这个问题,使 Java 语言变得更加安全好用,近些年的一些编译器对 Java 语言进行了扩充,使 Java 语言支持了"泛型",特别是 Sun 公司发布的 JDK 5.0 更是将泛型作为其中一个重要的特性加以推广。
本文首先对泛型的基本概念和特点进行简单介绍,然后通过引入几个实例来讨论带有泛型的类,泛型中的子类型,以及范化方法和受限类型参数等重要概念。为了帮助读者更加深刻的理解并使用泛型,本文还介绍了泛型的转化,即,如何将带有泛型的 Java 程序转化成一般的没有泛型的 Java 程序。这样,读者对泛型的理解就不会仅仅局限在表面上了。考虑到多数读者仅仅是使用泛型,因此本文并未介绍泛型在编译器中的具体实现。Java 中的泛型和 C++ 中的模板表面上非常相似,但实际上二者还是有很大区别的,本文最后简单介绍了 Java 中的泛型与 C++ 模板的主要区别。
泛型概览
泛型本质上是提供类型的"类型参数",它们也被称为参数化类型(parameterized type)或参量多态(parametric polymorphism)。其实泛型思想并不是 Java 最先引入的,C++ 中的模板就是一个运用泛型的例子。
GJ(Generic Java)是对 Java 语言的一种扩展,是一种带有参数化类型的 Java 语言。用 GJ 编写的程序看起来和普通的 Java 程序基本相同,只不过多了一些参数化的类型同时少了一些类型转换。实际上,这些 GJ 程序也是首先被转化成一般的不带泛型的 Java 程序后再进行处理的,编译器自动完成了从 Generic Java 到普通 Java 的翻译。具体的转化过程大致分为以下几个部分:
将参数化类型中的类型参数"擦除"(erasure)掉;
将类型变量用"上限(upper bound)"取代,通常情况下这些上限是 Object。这里的类型变量是指实例域,本地方法域,方法参数以及方法返回值中用来标记类型信息的"变量",例如:实例域中的变量声明 A elem;,方法声明 Node (A elem){};,其中,A 用来标记 elem 的类型,它就是类型变量。
添加类型转换并插入"桥方法"(bridge method),以便覆盖(overridden)可以正常的工作。
转化后的程序和没有引入泛型时程序员不得不手工完成转换的程序是非常一致的,具体的转化过程会在后面介绍。GJ 保持了和 Java 语言以及 Java 虚拟机很好的兼容性,下面对 GJ 的特点做一个简要的总结。
类型安全。 泛型的一个主要目标就是提高 Java 程序的类型安全。使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果没有泛型,那么类型的安全性主要由程序员来把握,这显然不如带有泛型的程序安全性高。
消除强制类型转换。泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。
向后兼容。支持泛型的 Java 编译器(例如 JDK5.0 中的 Javac)可以用来编译经过泛型扩充的 Java 程序(GJ 程序),但是现有的没有使用泛型扩充的 Java 程序仍然可以用这些编译器来编译。
层次清晰,恪守规范。无论被编译的源程序是否使用泛型扩充,编译生成的字节码均可被虚拟机接受并执行。也就是说不管编译器的输入是 GJ 程序,还是一般的 Java 程序,经过编译后的字节码都严格遵循《Java 虚拟机规范》中对字节码的要求。可见,泛型主要是在编译器层面实现的,它对于 Java 虚拟机是透明的。
性能收益。目前来讲,用 GJ 编写的代码和一般的 Java 代码在效率上是非常接近的。 但是由于泛型会给 Java 编译器和虚拟机带来更多的类型信息,因此利用这些信息对 Java 程序做进一步优化将成为可能。
以上是泛型的一些主要特点,下面通过几个相关的例子来对 Java 语言中的泛型进行说明。
【IT168技术文档】
带有泛型的类
为了帮助大家更好地理解 Java 语言中的泛型,我们在这里先来对比两段实现相同功能的 GJ 代码和 Java 代码。通过观察它们的不同点来对 Java 中的泛型有个总体的把握,首先来分析一下不带泛型的 Java 代码,程序如下:
代码如下:
下面再来分析一下在 GJ 中是如何对这个类进行操作的,程序如下:
类型参数的作用域是定义这个类型参数的整个类,但是不包括静态成员函数。这是因为当访问同一个静态成员函数时,同一个类的不同实例可能有不同的类型参数,所以上述提到的那个作用域不应该包括这些静态函数,否则就会引起混乱。
【IT168技术文档】
泛型中的子类型
在 Java 语言中,我们可以将某种类型的变量赋值给其父类型所对应的变量,例如,String 是 Object 的子类型,因此,我们可以将 String 类型的变量赋值给 Object 类型的变量,甚至可以将 String [ ] 类型的变量(数组)赋值给 Object [ ] 类型的变量,即 String [ ] 是 Object [ ] 的子类型。
上述情形恐怕已经深深地印在了广大读者的脑中,对于泛型来讲,上述情形有所变化,因此请广大读者务必引起注意。为了说明这种不同,我们还是先来分析一个小例子,代码如下所示:
如果上述赋值是合理的,那么上面代码的第三行的操作将是可行的,因为 lo 是 List<Object>,所以向其添加 Integer 类型的元素应该是完全合法的。读到此处,我们已经看到了第二行的这种赋值所潜在的危险,它破坏了泛型所带来的类型安全性。
一般情况下,如果 A 是 B 的子类型,C 是某个泛型的声明,那么 C<A> 并不是 C<B> 的子类型,我们也不能将 C<A> 类型的变量赋值给 C<B> 类型的变量。这一点和我们以前接触的父子类型关系有很大的出入,因此请读者务必引起注意。
泛化方法和受限类型参数
在这一部分我们将讨论有关泛化方法(generic method )和受限类型参数(bounded type parameter)的内容,这是泛型中的两个重要概念,还是先来分析一下与此相关的代码。
第 14 行到第 25 行的代码定义了一个类 Collections,这个类包含一个静态方法 max(Collection<A> xs),它用来在一个非空的 Collection 中寻找最大的元素并返回这个元素。这个方法的两个特点就是它是一个泛化方法并且有一个受限类型参数。
之所以说它是泛化了的方法,是因为这个方法可以应用到很多种类型上。当要将一个方法声明为泛化方法时,我们只需要在这个方法的返回类型(A)之前加上一个类型参数(A),并用尖括号(< >)将它括起来。这里的类型参数(A)是在方法被调用时自动实例化的。例如,假设对象 m 的类型是 Collection<Byte>,那么当使用下面的语句:
Byte x = Collections.max(m);
调用方法 max 时,该方法的参数 A 将被推测为 Byte。
根据上面讨论的内容,泛化方法 max 的完整声明应该是下面的形式:
< A > A max (Collection<A> xs) {
max 的方法体
}
但是,我们见到的 max 在 < A > 中还多了 "implements Comparable<A>" 一项,这是什么呢?这就是我们下面将要谈到的"受限的类型参数"。在上面的例子中,类型参数 A 就是一个受限的的类型参数,因为它不是泛指任何类型,而是指那些自己和自己作比较的类型。例如参数可以被实例化为 Byte,因为程序中有 Byte implements Comparable<Byte> 的语句,参见第 5 行。这种限制(或者说是范围)通过如下的方式表示,"类型参数 implements 接口",或是 "类型参数 extend 类",上面程序中的"Byte implements Comparable<Byte>"就是一例。
泛型是 Sun 公司发布的 JDK 5.0 中的一个重要特性,它的最大优点是提供了程序的类型安全同可以向后兼容。为了帮助读者更好地理解和使用泛型,本文通过一些示例从基本原理,重要概念,关键技术,以及相似技术比较等多个角度对 Java 语言中的泛型技术进行了介绍,重点强调了泛型中的一些基本但又不是很好理解的概念。
为了避免和 C++ 中的模板混淆,本文简要介绍了 Java 中的泛型和 C++ 中的模板的主要区别,希望这种比较能够帮助读者加深对泛型的理解。
引言
很多 Java 程序员都使用过集合(Collection),集合中元素的类型是多种多样的,例如,有些集合中的元素是 Byte 类型的,而有些则可能是 String 类型的,等等。Java 语言之所以支持这么多种类的集合,是因为它允许程序员构建一个元素类型为 Object 的 Collection,所以其中的元素可以是任何类型。
当使用 Collection 时,我们经常要做的一件事情就是要进行类型转换,当转换成所需的类型以后,再对它们进行处理。很明显,这种设计给编程人员带来了极大的不便,同时也容易引入错误。
在很多 Java 应用中,上述情况非常普遍,为了解决这个问题,使 Java 语言变得更加安全好用,近些年的一些编译器对 Java 语言进行了扩充,使 Java 语言支持了"泛型",特别是 Sun 公司发布的 JDK 5.0 更是将泛型作为其中一个重要的特性加以推广。
本文首先对泛型的基本概念和特点进行简单介绍,然后通过引入几个实例来讨论带有泛型的类,泛型中的子类型,以及范化方法和受限类型参数等重要概念。为了帮助读者更加深刻的理解并使用泛型,本文还介绍了泛型的转化,即,如何将带有泛型的 Java 程序转化成一般的没有泛型的 Java 程序。这样,读者对泛型的理解就不会仅仅局限在表面上了。考虑到多数读者仅仅是使用泛型,因此本文并未介绍泛型在编译器中的具体实现。Java 中的泛型和 C++ 中的模板表面上非常相似,但实际上二者还是有很大区别的,本文最后简单介绍了 Java 中的泛型与 C++ 模板的主要区别。
泛型概览
泛型本质上是提供类型的"类型参数",它们也被称为参数化类型(parameterized type)或参量多态(parametric polymorphism)。其实泛型思想并不是 Java 最先引入的,C++ 中的模板就是一个运用泛型的例子。
GJ(Generic Java)是对 Java 语言的一种扩展,是一种带有参数化类型的 Java 语言。用 GJ 编写的程序看起来和普通的 Java 程序基本相同,只不过多了一些参数化的类型同时少了一些类型转换。实际上,这些 GJ 程序也是首先被转化成一般的不带泛型的 Java 程序后再进行处理的,编译器自动完成了从 Generic Java 到普通 Java 的翻译。具体的转化过程大致分为以下几个部分:
将参数化类型中的类型参数"擦除"(erasure)掉;
将类型变量用"上限(upper bound)"取代,通常情况下这些上限是 Object。这里的类型变量是指实例域,本地方法域,方法参数以及方法返回值中用来标记类型信息的"变量",例如:实例域中的变量声明 A elem;,方法声明 Node (A elem){};,其中,A 用来标记 elem 的类型,它就是类型变量。
添加类型转换并插入"桥方法"(bridge method),以便覆盖(overridden)可以正常的工作。
转化后的程序和没有引入泛型时程序员不得不手工完成转换的程序是非常一致的,具体的转化过程会在后面介绍。GJ 保持了和 Java 语言以及 Java 虚拟机很好的兼容性,下面对 GJ 的特点做一个简要的总结。
类型安全。 泛型的一个主要目标就是提高 Java 程序的类型安全。使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果没有泛型,那么类型的安全性主要由程序员来把握,这显然不如带有泛型的程序安全性高。
消除强制类型转换。泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。
向后兼容。支持泛型的 Java 编译器(例如 JDK5.0 中的 Javac)可以用来编译经过泛型扩充的 Java 程序(GJ 程序),但是现有的没有使用泛型扩充的 Java 程序仍然可以用这些编译器来编译。
层次清晰,恪守规范。无论被编译的源程序是否使用泛型扩充,编译生成的字节码均可被虚拟机接受并执行。也就是说不管编译器的输入是 GJ 程序,还是一般的 Java 程序,经过编译后的字节码都严格遵循《Java 虚拟机规范》中对字节码的要求。可见,泛型主要是在编译器层面实现的,它对于 Java 虚拟机是透明的。
性能收益。目前来讲,用 GJ 编写的代码和一般的 Java 代码在效率上是非常接近的。 但是由于泛型会给 Java 编译器和虚拟机带来更多的类型信息,因此利用这些信息对 Java 程序做进一步优化将成为可能。
以上是泛型的一些主要特点,下面通过几个相关的例子来对 Java 语言中的泛型进行说明。
【IT168技术文档】
带有泛型的类
为了帮助大家更好地理解 Java 语言中的泛型,我们在这里先来对比两段实现相同功能的 GJ 代码和 Java 代码。通过观察它们的不同点来对 Java 中的泛型有个总体的把握,首先来分析一下不带泛型的 Java 代码,程序如下:
接口 Collection 提供了两个方法,即添加元素的方法 add(Object x),见第 2 行,以及返回该 Collection 的 Iterator 实例的方法 iterator(),见第 3 行。Iterator 接口也提供了两个方法,其一就是判断是否有下一个元素的方法 hasNext(),见第 8 行,另外就是返回下一个元素的方法 next(),见第 7 行。LinkedList 类是对接口 Collection 的实现,它是一个含有一系列节点的链表,节点中的数据类型是 Object,这样就可以创建任意类型的节点了,比如 Byte, String 等等。上面这段程序就是用没有泛型的传统的 Java 语言编写的代码。接下来我们分析一下传统的 Java 语言是如何使用这个类的。interface Collection ...{
2 public void add (Object x);
3 public Iterator iterator ();
4 }
5
6 interface Iterator ...{
7 public Object next ();
8 public boolean hasNext ();
9 }
10
11 class NoSuchElementException extends RuntimeException ...{}
12
13 class LinkedList implements Collection ...{
14
15 protected class Node ...{
16 Object elt;
17 Node next = null;
18 Node (Object elt) ...{ this.elt = elt; }
19 }
20
21 protected Node head = null, tail = null;
22
23 public LinkedList () ...{}
24
25 public void add (Object elt) ...{
26 if (head == null) ...{ head = new Node(elt); tail = head; }
27 else ...{ tail.next = new Node(elt); tail = tail.next; }
28 }
29
30 public Iterator iterator () ...{
31
32 return new Iterator () ...{
33 protected Node ptr = head;
34 public boolean hasNext () ...{ return ptr != null; }
35 public Object next () ...{
36 if (ptr != null) ...{
37 Object elt = ptr.elt; ptr = ptr.next; return elt;
38 } else throw new NoSuchElementException ();
39 }
40 };
41 }
42 }
代码如下:
从上面的程序我们可以看出,当从一个链表中提取元素时需要进行类型转换,这些都要由程序员显式地完成。如果我们不小心从 String 类型的链表中试图提取一个 Byte 型的元素,见第 15 到第 16 行的代码,那么这将会抛出一个运行时的异常。请注意,上面这段程序可以顺利地经过编译,不会产生任何编译时的错误,因为编译器并不做类型检查,这种检查是在运行时进行的。不难发现,传统 Java 语言的这一缺陷推迟了发现程序中错误的时间,从软件工程的角度来看,这对软件的开发是非常不利的。接下来,我们讨论一下如何用 GJ 来实现同样功能的程序。源程序如下:class Test ...{
2 public static void main (String[] args) ...{
3 // byte list
4 LinkedList xs = new LinkedList();
5 xs.add(new Byte(0)); xs.add(new Byte(1));
6 Byte x = (Byte)xs.iterator().next();
7 // string list
8 LinkedList ys = new LinkedList();
9 ys.add("zero"); ys.add("one");
10 String y = (String)ys.iterator().next();
11 // string list list
12 LinkedList zss = new LinkedList();
13 zss.add(ys);
14 String z = (String)((LinkedList)zss.iterator().next()).iterator().next();
15 // string list treated as byte list
16 Byte w = (Byte)ys.iterator().next(); // run-time exception
17 }
18 }
程序的功能并没有任何改变,只是在实现方式上使用了泛型技术。我们注意到上面程序的接口和类均带有一个类型参数 A,它被包含在一对尖括号(< >)中,见第 1,6 和 13 行,这种表示法遵循了 C++ 中模板的表示习惯。这部分程序和上面程序的主要区别就是在 Collection, Iterator, 或 LinkedList 出现的地方均用 Collection<A>, Iterator<A>, 或 LinkedList<A> 来代替,当然,第 22 行对构造函数的声明除外。interface Collection<A> ...{
2 public void add(A x);
3 public Iterator<A> iterator();
4 }
5
6 interface Iterator<A> ...{
7 public A next();
8 public boolean hasNext();
9 }
10
11 class NoSuchElementException extends RuntimeException ...{}
12
13 class LinkedList<A> implements Collection<A> ...{
14 protected class Node ...{
15 A elt;
16 Node next = null;
17 Node (A elt) ...{ this.elt = elt; }
18 }
19
20 protected Node head = null, tail = null;
21
22 public LinkedList () ...{}
23
24 public void add (A elt) ...{
25 if (head == null) ...{ head = new Node(elt); tail = head; }
26 else ...{ tail.next = new Node(elt); tail = tail.next; }
27 }
28
29 public Iterator<A> iterator () ...{
30 return new Iterator<A> () ...{
31 protected Node ptr = head;
32 public boolean hasNext () ...{ return ptr != null; }
33 public A next () ...{
34 if (ptr != null) ...{
35 A elt = ptr.elt; ptr = ptr.next; return elt;
36 } else throw new NoSuchElementException ();
37 }
38 };
39 }
40 }
下面再来分析一下在 GJ 中是如何对这个类进行操作的,程序如下:
在这里我们可以看到,有了泛型以后,程序员并不需要进行显式的类型转换,只要赋予一个参数化的类型即可,见第 4,8 和 12 行,这是非常方便的,同时也不会因为忘记进行类型转换而产生错误。另外需要注意的就是当试图从一个字符串类型的链表里提取出一个元素,然后将它赋值给一个 Byte 型的变量时,见第 16 行,编译器将会在编译时报出错误,而不是由虚拟机在运行时报错,这是因为编译器会在编译时刻对 GJ 代码进行类型检查,此种机制有利于尽早地发现并改正错误。class Test ...{
2 public static void main (String [] args) ...{
3 // byte list
4 LinkedList<Byte> xs = new LinkedList<Byte>();
5 xs.add(new Byte(0)); xs.add(new Byte(1));
6 Byte x = xs.iterator().next();
7 // string list
8 LinkedList<String> ys = new LinkedList<String>();
9 ys.add("zero"); ys.add("one");
10 String y = ys.iterator().next();
11 // string list list
12 LinkedList<LinkedList<String>>zss=
newLinkedList<LinkedList<String>>();
13 zss.add(ys);
14 String z = zss.iterator().next().iterator().next();
15 // string list treated as byte list
16 Byte w = ys.iterator().next(); // compile-time error
17 }
18 }
类型参数的作用域是定义这个类型参数的整个类,但是不包括静态成员函数。这是因为当访问同一个静态成员函数时,同一个类的不同实例可能有不同的类型参数,所以上述提到的那个作用域不应该包括这些静态函数,否则就会引起混乱。
【IT168技术文档】
泛型中的子类型
在 Java 语言中,我们可以将某种类型的变量赋值给其父类型所对应的变量,例如,String 是 Object 的子类型,因此,我们可以将 String 类型的变量赋值给 Object 类型的变量,甚至可以将 String [ ] 类型的变量(数组)赋值给 Object [ ] 类型的变量,即 String [ ] 是 Object [ ] 的子类型。
上述情形恐怕已经深深地印在了广大读者的脑中,对于泛型来讲,上述情形有所变化,因此请广大读者务必引起注意。为了说明这种不同,我们还是先来分析一个小例子,代码如下所示:
上述代码的第二行将 List<String> 赋值给了 List<Object>,按照以往的经验,这种赋值好像是正确的,因为 List<String> 应该是 List<Object> 的子类型。这里需要特别注意的是,这种赋值在泛型当中是不允许的!List<String> 也不是 List<Object> 的子类型。1 List<String> ls = new ArrayList<String>();
2 List<Object> lo = ls;
3 lo.add(new Integer());
4 String s = ls.get(0);
如果上述赋值是合理的,那么上面代码的第三行的操作将是可行的,因为 lo 是 List<Object>,所以向其添加 Integer 类型的元素应该是完全合法的。读到此处,我们已经看到了第二行的这种赋值所潜在的危险,它破坏了泛型所带来的类型安全性。
一般情况下,如果 A 是 B 的子类型,C 是某个泛型的声明,那么 C<A> 并不是 C<B> 的子类型,我们也不能将 C<A> 类型的变量赋值给 C<B> 类型的变量。这一点和我们以前接触的父子类型关系有很大的出入,因此请读者务必引起注意。
泛化方法和受限类型参数
在这一部分我们将讨论有关泛化方法(generic method )和受限类型参数(bounded type parameter)的内容,这是泛型中的两个重要概念,还是先来分析一下与此相关的代码。
这里定义了一个接口 Comparable<A>,用来和 A 类型的对象进行比较。类 Byte 实现了这个接口,并以它自己作为类型参数,因此,它们自己就可以和自己进行比较了。1 interface Comparable<A> ...{
2 public int compareTo(A that);
3 }
4
5 class Byte implements Comparable<Byte> ...{
6 private byte value;
7 public Byte(byte value) ...{this.value = value;}
8 public byte byteValue() ...{return value;}
9 public int compareTo(Byte that) ...{
10 return this.value - that.value;
11 }
12 }
13
14 class Collections ...{
15 public static <A implements Comparable<A>>
16 A max (Collection<A> xs) ...{
17 Iterator<A> xi = xs.iterator();
18 A w = xi.next();
19 while (xi.hasNext()) ...{
20 A x = xi.next();
21 if (w.compareTo(x) < 0) w = x;
22 }
23 return w;
24 }
25 }
第 14 行到第 25 行的代码定义了一个类 Collections,这个类包含一个静态方法 max(Collection<A> xs),它用来在一个非空的 Collection 中寻找最大的元素并返回这个元素。这个方法的两个特点就是它是一个泛化方法并且有一个受限类型参数。
之所以说它是泛化了的方法,是因为这个方法可以应用到很多种类型上。当要将一个方法声明为泛化方法时,我们只需要在这个方法的返回类型(A)之前加上一个类型参数(A),并用尖括号(< >)将它括起来。这里的类型参数(A)是在方法被调用时自动实例化的。例如,假设对象 m 的类型是 Collection<Byte>,那么当使用下面的语句:
Byte x = Collections.max(m);
调用方法 max 时,该方法的参数 A 将被推测为 Byte。
根据上面讨论的内容,泛化方法 max 的完整声明应该是下面的形式:
< A > A max (Collection<A> xs) {
max 的方法体
}
但是,我们见到的 max 在 < A > 中还多了 "implements Comparable<A>" 一项,这是什么呢?这就是我们下面将要谈到的"受限的类型参数"。在上面的例子中,类型参数 A 就是一个受限的的类型参数,因为它不是泛指任何类型,而是指那些自己和自己作比较的类型。例如参数可以被实例化为 Byte,因为程序中有 Byte implements Comparable<Byte> 的语句,参见第 5 行。这种限制(或者说是范围)通过如下的方式表示,"类型参数 implements 接口",或是 "类型参数 extend 类",上面程序中的"Byte implements Comparable<Byte>"就是一例。
0
相关文章
