多角度望 Java 中的泛型

多角度看 Java 中的泛型

摘自IBM中文网站

泛型是 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++ 模板的主要区别。


多角度望 Java 中的泛型
多角度望 Java 中的泛型
多角度望 Java 中的泛型
多角度望 Java 中的泛型
回页首


泛型概览

泛型本质上是提供类型的"类型参数",它们也被称为参数化类型(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 语言中的泛型进行说明。


多角度望 Java 中的泛型
多角度望 Java 中的泛型
多角度望 Java 中的泛型
多角度望 Java 中的泛型
回页首


带有泛型的类

为了帮助大家更好地理解 Java 语言中的泛型,我们在这里先来对比两段实现相同功能的 GJ 代码和 Java 代码。通过观察它们的不同点来对 Java 中的泛型有个总体的把握,首先来分析一下不带泛型的 Java 代码,程序如下:


1	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	}

接口 Collection 提供了两个方法,即添加元素的方法 add(Object x),见第 2 行,以及返回该 CollectionIterator 实例的方法 iterator(),见第 3 行。Iterator 接口也提供了两个方法,其一就是判断是否有下一个元素的方法 hasNext(),见第 8 行,另外就是返回下一个元素的方法 next(),见第 7 行。LinkedList 类是对接口 Collection 的实现,它是一个含有一系列节点的链表,节点中的数据类型是 Object,这样就可以创建任意类型的节点了,比如 Byte, String 等等。上面这段程序就是用没有泛型的传统的 Java 语言编写的代码。接下来我们分析一下传统的 Java 语言是如何使用这个类的。

代码如下:


1	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	}

从上面的程序我们可以看出,当从一个链表中提取元素时需要进行类型转换,这些都要由程序员显式地完成。如果我们不小心从 String 类型的链表中试图提取一个 Byte 型的元素,见第 15 到第 16 行的代码,那么这将会抛出一个运行时的异常。请注意,上面这段程序可以顺利地经过编译,不会产生任何编译时的错误,因为编译器并不做类型检查,这种检查是在运行时进行的。不难发现,传统 Java 语言的这一缺陷推迟了发现程序中错误的时间,从软件工程的角度来看,这对软件的开发是非常不利的。接下来,我们讨论一下如何用 GJ 来实现同样功能的程序。源程序如下:


1	interface Collection {
2		public void add(A x);
3		public Iterator iterator();
4	}
5	
6	interface Iterator {
7		public A next();
8		public boolean hasNext();
9	}
10	
11	class NoSuchElementException extends RuntimeException {}
12	
13	class LinkedList implements Collection {
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 iterator () {
30			return new Iterator () {
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	}

程序的功能并没有任何改变,只是在实现方式上使用了泛型技术。我们注意到上面程序的接口和类均带有一个类型参数 A,它被包含在一对尖括号(< >)中,见第 1,6 和 13 行,这种表示法遵循了 C++ 中模板的表示习惯。这部分程序和上面程序的主要区别就是在 Collection, Iterator, 或 LinkedList 出现的地方均用 Collection泛型中的子类型, Iterator, 或 LinkedList 来代替,当然,第 22 行对构造函数的声明除外。

下面再来分析一下在 GJ 中是如何对这个类进行操作的,程序如下:


1	class Test {
2		public static void main (String [] args) {
3			// byte list
4			LinkedList<byte></byte> xs = new LinkedList<byte></byte>();
5			xs.add(new Byte(0)); xs.add(new Byte(1));
6			Byte x = xs.iterator().next();
7			// string list
8			LinkedList<string></string> ys = new LinkedList<string></string>();
9			ys.add("zero"); ys.add("one");
10			String y = ys.iterator().next();
11			// string list list
12			LinkedList>zss=
newLinkedList>();
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	}

在这里我们可以看到,有了泛型以后,程序员并不需要进行显式的类型转换,只要赋予一个参数化的类型即可,见第 4,8 和 12 行,这是非常方便的,同时也不会因为忘记进行类型转换而产生错误。另外需要注意的就是当试图从一个字符串类型的链表里提取出一个元素,然后将它赋值给一个 Byte 型的变量时,见第 16 行,编译器将会在编译时报出错误,而不是由虚拟机在运行时报错,这是因为编译器会在编译时刻对 GJ 代码进行类型检查,此种机制有利于尽早地发现并改正错误。

类型参数的作用域是定义这个类型参数的整个类,但是不包括静态成员函数。这是因为当访问同一个静态成员函数时,同一个类的不同实例可能有不同的类型参数,所以上述提到的那个作用域不应该包括这些静态函数,否则就会引起混乱。


多角度望 Java 中的泛型
多角度望 Java 中的泛型
多角度望 Java 中的泛型
多角度望 Java 中的泛型
回页首


 

在 Java 语言中,我们可以将某种类型的变量赋值给其父类型所对应的变量,例如,String 是 Object 的子类型,因此,我们可以将 String 类型的变量赋值给 Object 类型的变量,甚至可以将 String [ ] 类型的变量(数组)赋值给 Object [ ] 类型的变量,即 String [ ] 是 Object [ ] 的子类型。

上述情形恐怕已经深深地印在了广大读者的脑中,对于泛型来讲,上述情形有所变化,因此请广大读者务必引起注意。为了说明这种不同,我们还是先来分析一个小例子,代码如下所示:


1			List<string></string> ls = new ArrayList<string></string>(); 
2			Listlo = ls; 3 lo.add(new Integer()); 4 String s = ls.get(0); 
 

上述代码的第二行将 List<string></string> 赋值给了 List,按照以往的经验,这种赋值好像是正确的,因为 List<string></string> 应该是 List的子类型。这里需要特别注意的是,这种赋值在泛型当中是不允许的!List<string></string> 也不是 List

1 楼 kanokano 2007-07-06  
很好啊
很详细的!!!!!